Merge remote-tracking branch 'kbx81/20250915-wifi-info-use-callbacks' into integration

This commit is contained in:
J. Nick Koston
2025-11-24 18:35:31 -06:00
41 changed files with 1198 additions and 292 deletions

View File

@@ -26,7 +26,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
with:
category: "/language:${{matrix.language}}"

View File

@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org>

View File

@@ -484,6 +484,7 @@ esphome/components/template/datetime/* @rfdarter
esphome/components/template/event/* @nohat
esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse
esphome/components/thermopro_ble/* @sittner
esphome/components/thermostat/* @kbx81
esphome/components/time/* @esphome/core
esphome/components/tinyusb/* @kbx81

View File

@@ -85,6 +85,7 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue"
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
def validate_encryption_key(value):
@@ -537,9 +538,24 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
return var
@automation.register_condition("api.connected", APIConnectedCondition, {})
API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(APIServer),
cv.Optional(CONF_STATE_SUBSCRIPTION_ONLY, default=False): cv.templatable(
cv.boolean
),
}
)
@automation.register_condition(
"api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA
)
async def api_connected_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg)
var = cg.new_Pvariable(condition_id, template_arg)
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
cg.add(var.set_state_subscription_only(templ))
return var
def FILTER_SOURCE_FILES() -> list[str]:

View File

@@ -559,7 +559,18 @@ void APIServer::request_time() {
}
#endif
bool APIServer::is_connected() const { return !this->clients_.empty(); }
bool APIServer::is_connected(bool state_subscription_only) const {
if (!state_subscription_only) {
return !this->clients_.empty();
}
for (const auto &client : this->clients_) {
if (client->flags_.state_subscription) {
return true;
}
}
return false;
}
void APIServer::on_shutdown() {
this->shutting_down_ = true;

View File

@@ -150,7 +150,7 @@ class APIServer : public Component, public Controller {
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
#endif
bool is_connected() const;
bool is_connected(bool state_subscription_only = false) const;
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription {
@@ -254,8 +254,11 @@ class APIServer : public Component, public Controller {
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
TEMPLATABLE_VALUE(bool, state_subscription_only)
public:
bool check(const Ts &...x) override { return global_api_server->is_connected(); }
bool check(const Ts &...x) override {
return global_api_server->is_connected(this->state_subscription_only_.value(x...));
}
};
} // namespace esphome::api

View File

@@ -132,7 +132,11 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
void get_bluetooth_mac_address_pretty(std::span<char, 18> output) {
const uint8_t *mac = esp_bt_dev_get_address();
format_mac_addr_upper(mac, output.data());
if (mac != nullptr) {
format_mac_addr_upper(mac, output.data());
} else {
output[0] = '\0';
}
}
protected:

View File

@@ -47,18 +47,20 @@ MULTI_CONF = True
def _bus_declare_type(value):
if CORE.is_esp32:
return cv.declare_id(IDFI2CBus)(value)
if CORE.using_arduino:
return cv.declare_id(ArduinoI2CBus)(value)
if CORE.using_esp_idf:
return cv.declare_id(IDFI2CBus)(value)
if CORE.using_zephyr:
return cv.declare_id(ZephyrI2CBus)(value)
raise NotImplementedError
def validate_config(config):
if CORE.using_esp_idf:
return cv.require_framework_version(esp_idf=cv.Version(5, 4, 2))(config)
if CORE.is_esp32:
return cv.require_framework_version(
esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1)
)(config)
return config
@@ -67,12 +69,12 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): _bus_declare_type,
cv.Optional(CONF_SDA, default="SDA"): pins.internal_gpio_pin_number,
cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All(
cv.only_with_esp_idf, cv.boolean
cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All(
cv.only_on_esp32, cv.boolean
),
cv.Optional(CONF_SCL, default="SCL"): pins.internal_gpio_pin_number,
cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All(
cv.only_with_esp_idf, cv.boolean
cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All(
cv.only_on_esp32, cv.boolean
),
cv.SplitDefault(
CONF_FREQUENCY,
@@ -151,7 +153,7 @@ async def to_code(config):
cg.add(var.set_scan(config[CONF_SCAN]))
if CONF_TIMEOUT in config:
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
if CORE.using_arduino:
if CORE.using_arduino and not CORE.is_esp32:
cg.add_library("Wire", None)
@@ -248,14 +250,16 @@ def final_validate_device_schema(
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"i2c_bus_arduino.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.RP2040_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
"i2c_bus_esp_idf.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
}
)

View File

@@ -1,4 +1,4 @@
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
#include "i2c_bus_arduino.h"
#include <Arduino.h>
@@ -15,16 +15,7 @@ static const char *const TAG = "i2c.arduino";
void ArduinoI2CBus::setup() {
recover_();
#if defined(USE_ESP32)
static uint8_t next_bus_num = 0;
if (next_bus_num == 0) {
wire_ = &Wire;
} else {
wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory)
}
this->port_ = next_bus_num;
next_bus_num++;
#elif defined(USE_ESP8266)
#if defined(USE_ESP8266)
wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory)
#elif defined(USE_RP2040)
static bool first = true;
@@ -54,10 +45,7 @@ void ArduinoI2CBus::set_pins_and_clock_() {
wire_->begin(static_cast<int>(sda_pin_), static_cast<int>(scl_pin_));
#endif
if (timeout_ > 0) { // if timeout specified in yaml
#if defined(USE_ESP32)
// https://github.com/espressif/arduino-esp32/blob/master/libraries/Wire/src/Wire.cpp
wire_->setTimeOut(timeout_ / 1000); // unit: ms
#elif defined(USE_ESP8266)
#if defined(USE_ESP8266)
// https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h
wire_->setClockStretchLimit(timeout_); // unit: us
#elif defined(USE_RP2040)
@@ -76,9 +64,7 @@ void ArduinoI2CBus::dump_config() {
" Frequency: %u Hz",
this->sda_pin_, this->scl_pin_, this->frequency_);
if (timeout_ > 0) {
#if defined(USE_ESP32)
ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000);
#elif defined(USE_ESP8266)
#if defined(USE_ESP8266)
ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_);
#elif defined(USE_RP2040)
ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000);
@@ -275,4 +261,4 @@ void ArduinoI2CBus::recover_() {
} // namespace i2c
} // namespace esphome
#endif // USE_ESP_IDF
#endif // defined(USE_ARDUINO) && !defined(USE_ESP32)

View File

@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
#include <Wire.h>
#include "esphome/core/component.h"
@@ -29,7 +29,7 @@ class ArduinoI2CBus : public InternalI2CBus, public Component {
void set_frequency(uint32_t frequency) { frequency_ = frequency; }
void set_timeout(uint32_t timeout) { timeout_ = timeout; }
int get_port() const override { return this->port_; }
int get_port() const override { return 0; }
private:
void recover_();
@@ -37,7 +37,6 @@ class ArduinoI2CBus : public InternalI2CBus, public Component {
RecoveryCode recovery_result_;
protected:
int8_t port_{-1};
TwoWire *wire_;
uint8_t sda_pin_;
uint8_t scl_pin_;
@@ -49,4 +48,4 @@ class ArduinoI2CBus : public InternalI2CBus, public Component {
} // namespace i2c
} // namespace esphome
#endif // USE_ARDUINO
#endif // defined(USE_ARDUINO) && !defined(USE_ESP32)

View File

@@ -1,4 +1,4 @@
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#include "i2c_bus_esp_idf.h"
@@ -299,4 +299,4 @@ void IDFI2CBus::recover_() {
} // namespace i2c
} // namespace esphome
#endif // USE_ESP_IDF
#endif // USE_ESP32

View File

@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#include "esphome/core/component.h"
#include "i2c_bus.h"
@@ -53,4 +53,4 @@ class IDFI2CBus : public InternalI2CBus, public Component {
} // namespace i2c
} // namespace esphome
#endif // USE_ESP_IDF
#endif // USE_ESP32

View File

@@ -120,7 +120,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
bool has_psk = api::global_api_server->get_noise_ctx()->has_psk();
bool has_psk = api::global_api_server->get_noise_ctx().has_psk();
const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED;
txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)});
#endif

View File

@@ -2,6 +2,7 @@
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include "esphome/components/display/display_buffer.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -38,6 +39,14 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
Color color(rgba[0], rgba[1], rgba[2], rgba[3]);
decoder->draw(x, y, w, h, color);
// Feed watchdog periodically to avoid triggering during long decode operations.
// Feed every 1024 pixels to balance efficiency and responsiveness.
uint32_t pixels = w * h;
decoder->increment_pixels_decoded(pixels);
if ((decoder->get_pixels_decoded() % 1024) < pixels) {
App.feed_wdt();
}
}
PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) {

View File

@@ -25,9 +25,13 @@ class PngDecoder : public ImageDecoder {
int prepare(size_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override;
void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; }
uint32_t get_pixels_decoded() const { return this->pixels_decoded_; }
protected:
RAMAllocator<pngle_t> allocator_;
pngle_t *pngle_;
uint32_t pixels_decoded_{0};
};
} // namespace online_image

View File

@@ -53,6 +53,18 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
this->lock_row_(stream, obj, area, node, friendly_name);
#endif
#ifdef USE_EVENT
this->event_type_(stream);
for (auto *obj : App.get_events())
this->event_row_(stream, obj, area, node, friendly_name);
#endif
#ifdef USE_TEXT
this->text_type_(stream);
for (auto *obj : App.get_texts())
this->text_row_(stream, obj, area, node, friendly_name);
#endif
#ifdef USE_TEXT_SENSOR
this->text_sensor_type_(stream);
for (auto *obj : App.get_text_sensors())
@@ -547,6 +559,100 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso
}
#endif
// Type-specific implementation
#ifdef USE_TEXT
void PrometheusHandler::text_type_(AsyncResponseStream *stream) {
stream->print(ESPHOME_F("#TYPE esphome_text_value gauge\n"));
stream->print(ESPHOME_F("#TYPE esphome_text_failed gauge\n"));
}
void PrometheusHandler::text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node,
std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_)
return;
if (obj->has_state()) {
// We have a valid value, output this value
stream->print(ESPHOME_F("esphome_text_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\"} 0\n"));
// Data itself
stream->print(ESPHOME_F("esphome_text_value{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",value=\""));
stream->print(obj->state.c_str());
stream->print(ESPHOME_F("\"} "));
stream->print(ESPHOME_F("1.0"));
stream->print(ESPHOME_F("\n"));
} else {
// Invalid state
stream->print(ESPHOME_F("esphome_text_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\"} 1\n"));
}
}
#endif
// Type-specific implementation
#ifdef USE_EVENT
void PrometheusHandler::event_type_(AsyncResponseStream *stream) {
stream->print(ESPHOME_F("#TYPE esphome_event_value gauge\n"));
stream->print(ESPHOME_F("#TYPE esphome_event_failed gauge\n"));
}
void PrometheusHandler::event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node,
std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_)
return;
if (obj->get_last_event_type() != nullptr) {
// We have a valid event type, output this value
stream->print(ESPHOME_F("esphome_event_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\"} 0\n"));
// Data itself
stream->print(ESPHOME_F("esphome_event_value{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",last_event_type=\""));
stream->print(obj->get_last_event_type());
stream->print(ESPHOME_F("\"} "));
stream->print(ESPHOME_F("1.0"));
stream->print(ESPHOME_F("\n"));
} else {
// No event triggered yet
stream->print(ESPHOME_F("esphome_event_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\"} 1\n"));
}
}
#endif
// Type-specific implementation
#ifdef USE_NUMBER
void PrometheusHandler::number_type_(AsyncResponseStream *stream) {
@@ -620,7 +726,7 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",value=\""));
stream->print(obj->state.c_str());
stream->print(obj->current_option());
stream->print(ESPHOME_F("\"} "));
stream->print(ESPHOME_F("1.0"));
stream->print(ESPHOME_F("\n"));

View File

@@ -123,6 +123,22 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
std::string &friendly_name);
#endif
#ifdef USE_EVENT
/// Return the type for prometheus
void event_type_(AsyncResponseStream *stream);
/// Return the event values state as prometheus data point
void event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node,
std::string &friendly_name);
#endif
#ifdef USE_TEXT
/// Return the type for prometheus
void text_type_(AsyncResponseStream *stream);
/// Return the text values state as prometheus data point
void text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node,
std::string &friendly_name);
#endif
#ifdef USE_TEXT_SENSOR
/// Return the type for prometheus
void text_sensor_type_(AsyncResponseStream *stream);

View File

@@ -0,0 +1,97 @@
import esphome.codegen as cg
from esphome.components import esp32_ble_tracker, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_BATTERY_LEVEL,
CONF_EXTERNAL_TEMPERATURE,
CONF_HUMIDITY,
CONF_ID,
CONF_MAC_ADDRESS,
CONF_SIGNAL_STRENGTH,
CONF_TEMPERATURE,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_DECIBEL_MILLIWATT,
UNIT_PERCENT,
)
CODEOWNERS = ["@sittner"]
DEPENDENCIES = ["esp32_ble_tracker"]
thermopro_ble_ns = cg.esphome_ns.namespace("thermopro_ble")
ThermoProBLE = thermopro_ble_ns.class_(
"ThermoProBLE", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ThermoProBLE),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_BATTERY,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema(
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(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await esp32_ble_tracker.register_ble_device(var, config)
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature(sens))
if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE):
sens = await sensor.new_sensor(external_temperature_config)
cg.add(var.set_external_temperature(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity(sens))
if battery_level_config := config.get(CONF_BATTERY_LEVEL):
sens = await sensor.new_sensor(battery_level_config)
cg.add(var.set_battery_level(sens))
if signal_strength_config := config.get(CONF_SIGNAL_STRENGTH):
sens = await sensor.new_sensor(signal_strength_config)
cg.add(var.set_signal_strength(sens))

View File

@@ -0,0 +1,204 @@
#include "thermopro_ble.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32
namespace esphome::thermopro_ble {
// this size must be large enough to hold the largest data frame
// of all supported devices
static constexpr std::size_t MAX_DATA_SIZE = 24;
struct DeviceParserMapping {
const char *prefix;
DeviceParser parser;
};
static float tp96_battery(uint16_t voltage);
static optional<ParseResult> parse_tp972(const uint8_t *data, std::size_t data_size);
static optional<ParseResult> parse_tp96(const uint8_t *data, std::size_t data_size);
static optional<ParseResult> parse_tp3(const uint8_t *data, std::size_t data_size);
static const char *const TAG = "thermopro_ble";
static const struct DeviceParserMapping DEVICE_PARSER_MAP[] = {
{"TP972", parse_tp972}, {"TP970", parse_tp96}, {"TP96", parse_tp96}, {"TP3", parse_tp3}};
void ThermoProBLE::dump_config() {
ESP_LOGCONFIG(TAG, "ThermoPro BLE");
LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "External temperature", this->external_temperature_);
LOG_SENSOR(" ", "Humidity", this->humidity_);
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
}
bool ThermoProBLE::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
// check for matching mac address
if (device.address_uint64() != this->address_) {
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
return false;
}
// check for valid device type
update_device_type_(device.get_name());
if (this->device_parser_ == nullptr) {
ESP_LOGVV(TAG, "parse_device(): invalid device type.");
return false;
}
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
// publish signal strength
float signal_strength = float(device.get_rssi());
if (this->signal_strength_ != nullptr)
this->signal_strength_->publish_state(signal_strength);
bool success = false;
for (auto &service_data : device.get_manufacturer_datas()) {
// check maximum data size
std::size_t data_size = service_data.data.size() + 2;
if (data_size > MAX_DATA_SIZE) {
ESP_LOGVV(TAG, "parse_device(): maximum data size exceeded!");
continue;
}
// reconstruct whole record from 2 byte uuid and data
esp_bt_uuid_t uuid = service_data.uuid.get_uuid();
uint8_t data[MAX_DATA_SIZE] = {static_cast<uint8_t>(uuid.uuid.uuid16), static_cast<uint8_t>(uuid.uuid.uuid16 >> 8)};
std::copy(service_data.data.begin(), service_data.data.end(), std::begin(data) + 2);
// dispatch data to parser
optional<ParseResult> result = this->device_parser_(data, data_size);
if (!result.has_value()) {
continue;
}
// publish sensor values
if (result->temperature.has_value() && this->temperature_ != nullptr)
this->temperature_->publish_state(*result->temperature);
if (result->external_temperature.has_value() && this->external_temperature_ != nullptr)
this->external_temperature_->publish_state(*result->external_temperature);
if (result->humidity.has_value() && this->humidity_ != nullptr)
this->humidity_->publish_state(*result->humidity);
if (result->battery_level.has_value() && this->battery_level_ != nullptr)
this->battery_level_->publish_state(*result->battery_level);
success = true;
}
return success;
}
void ThermoProBLE::update_device_type_(const std::string &device_name) {
// check for changed device name (should only happen on initial call)
if (this->device_name_ == device_name) {
return;
}
// remember device name
this->device_name_ = device_name;
// try to find device parser
for (const auto &mapping : DEVICE_PARSER_MAP) {
if (device_name.starts_with(mapping.prefix)) {
this->device_parser_ = mapping.parser;
return;
}
}
// device type unknown
this->device_parser_ = nullptr;
ESP_LOGVV(TAG, "update_device_type_(): unknown device type %s.", device_name.c_str());
}
static inline uint16_t read_uint16(const uint8_t *data, std::size_t offset) {
return static_cast<uint16_t>(data[offset + 0]) | (static_cast<uint16_t>(data[offset + 1]) << 8);
}
static inline int16_t read_int16(const uint8_t *data, std::size_t offset) {
return static_cast<int16_t>(read_uint16(data, offset));
}
static inline uint32_t read_uint32(const uint8_t *data, std::size_t offset) {
return static_cast<uint32_t>(data[offset + 0]) | (static_cast<uint32_t>(data[offset + 1]) << 8) |
(static_cast<uint32_t>(data[offset + 2]) << 16) | (static_cast<uint32_t>(data[offset + 3]) << 24);
}
// Battery calculation used with permission from:
// https://github.com/Bluetooth-Devices/thermopro-ble/blob/main/src/thermopro_ble/parser.py
//
// TP96x battery values appear to be a voltage reading, probably in millivolts.
// This means that calculating battery life from it is a non-linear function.
// Examining the curve, it looked fairly close to a curve from the tanh function.
// So, I created a script to use Tensorflow to optimize an equation in the format
// A*tanh(B*x+C)+D
// Where A,B,C,D are the variables to optimize for. This yielded the below function
static float tp96_battery(uint16_t voltage) {
float level = 52.317286f * tanh(static_cast<float>(voltage) / 273.624277936f - 8.76485439394f) + 51.06925f;
return std::max(0.0f, std::min(level, 100.0f));
}
static optional<ParseResult> parse_tp972(const uint8_t *data, std::size_t data_size) {
if (data_size != 23) {
ESP_LOGVV(TAG, "parse_tp972(): payload has wrong size of %d (!= 23)!", data_size);
return {};
}
ParseResult result;
// ambient temperature, 2 bytes, 16-bit unsigned integer, -54 °C offset
result.external_temperature = static_cast<float>(read_uint16(data, 1)) - 54.0f;
// battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage)
result.battery_level = tp96_battery(read_uint16(data, 3));
// internal temperature, 4 bytes, float, -54 °C offset
result.temperature = static_cast<float>(read_uint32(data, 9)) - 54.0f;
return result;
}
static optional<ParseResult> parse_tp96(const uint8_t *data, std::size_t data_size) {
if (data_size != 7) {
ESP_LOGVV(TAG, "parse_tp96(): payload has wrong size of %d (!= 7)!", data_size);
return {};
}
ParseResult result;
// internal temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset
result.temperature = static_cast<float>(read_uint16(data, 1)) - 30.0f;
// battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage)
result.battery_level = tp96_battery(read_uint16(data, 3));
// ambient temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset
result.external_temperature = static_cast<float>(read_uint16(data, 5)) - 30.0f;
return result;
}
static optional<ParseResult> parse_tp3(const uint8_t *data, std::size_t data_size) {
if (data_size < 6) {
ESP_LOGVV(TAG, "parse_tp3(): payload has wrong size of %d (< 6)!", data_size);
return {};
}
ParseResult result;
// temperature, 2 bytes, 16-bit signed integer, 0.1 °C
result.temperature = static_cast<float>(read_int16(data, 1)) * 0.1f;
// humidity, 1 byte, 8-bit unsigned integer, 1.0 %
result.humidity = static_cast<float>(data[3]);
// battery level, 2 bits (0-2)
result.battery_level = static_cast<float>(data[4] & 0x3) * 50.0;
return result;
}
} // namespace esphome::thermopro_ble
#endif

View File

@@ -0,0 +1,49 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#ifdef USE_ESP32
namespace esphome::thermopro_ble {
struct ParseResult {
optional<float> temperature;
optional<float> external_temperature;
optional<float> humidity;
optional<float> battery_level;
};
using DeviceParser = optional<ParseResult> (*)(const uint8_t *data, std::size_t data_size);
class ThermoProBLE : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { this->address_ = address; };
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
void dump_config() override;
void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; }
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
void set_external_temperature(sensor::Sensor *external_temperature) {
this->external_temperature_ = external_temperature;
}
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; }
protected:
uint64_t address_;
std::string device_name_;
DeviceParser device_parser_{nullptr};
sensor::Sensor *signal_strength_{nullptr};
sensor::Sensor *temperature_{nullptr};
sensor::Sensor *external_temperature_{nullptr};
sensor::Sensor *humidity_{nullptr};
sensor::Sensor *battery_level_{nullptr};
void update_device_type_(const std::string &device_name);
};
} // namespace esphome::thermopro_ble
#endif

View File

@@ -97,6 +97,7 @@ WIFI_MIN_AUTH_MODES = {
VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True)
WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition)
WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action)
WiFiConfigureAction = wifi_ns.class_(
@@ -590,6 +591,11 @@ async def wifi_enabled_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg)
@automation.register_condition("wifi.ap_active", WiFiAPActiveCondition, cv.Schema({}))
async def wifi_ap_active_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg)
@automation.register_action("wifi.enable", WiFiEnableAction, cv.Schema({}))
async def wifi_enable_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg)
@@ -601,6 +607,8 @@ async def wifi_disable_to_code(config, action_id, template_arg, args):
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save"
WIFI_CALLBACKS_KEY = "wifi_callbacks"
def request_wifi_scan_results():
@@ -613,13 +621,41 @@ def request_wifi_scan_results():
CORE.data[KEEP_SCAN_RESULTS_KEY] = True
def enable_runtime_power_save_control():
"""Enable runtime WiFi power save control.
Components that need to dynamically switch WiFi power saving on/off for latency
performance (e.g., audio streaming, large data transfers) should call this
function during their code generation. This enables the request_high_performance()
and release_high_performance() APIs.
Only supported on ESP32.
"""
CORE.data[RUNTIME_POWER_SAVE_KEY] = True
def request_wifi_callbacks() -> None:
"""Request that WiFi callbacks be compiled in.
Components that need to be notified about WiFi state changes (IP address changes,
scan results, connection state) should call this function during their code generation.
This enables the add_on_ip_state_callback(), add_on_wifi_scan_state_callback(),
and add_on_wifi_connect_state_callback() APIs.
"""
CORE.data[WIFI_CALLBACKS_KEY] = True
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure scan result retention."""
"""Final code generation step to configure optional WiFi features."""
if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False):
cg.add(
cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)")
)
if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False):
cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE")
if CORE.data.get(WIFI_CALLBACKS_KEY, False):
cg.add_define("USE_WIFI_CALLBACKS")
@automation.register_action(

View File

@@ -0,0 +1,116 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_WIFI
#include "wifi_component.h"
namespace esphome::wifi {
template<typename... Ts> class WiFiConnectedCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return global_wifi_component->is_connected(); }
};
template<typename... Ts> class WiFiEnabledCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); }
};
template<typename... Ts> class WiFiAPActiveCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return global_wifi_component->is_ap_active(); }
};
template<typename... Ts> class WiFiEnableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_wifi_component->enable(); }
};
template<typename... Ts> class WiFiDisableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_wifi_component->disable(); }
};
template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, public Component {
public:
TEMPLATABLE_VALUE(std::string, ssid)
TEMPLATABLE_VALUE(std::string, password)
TEMPLATABLE_VALUE(bool, save)
TEMPLATABLE_VALUE(uint32_t, connection_timeout)
void play(const Ts &...x) override {
auto ssid = this->ssid_.value(x...);
auto password = this->password_.value(x...);
// Avoid multiple calls
if (this->connecting_)
return;
// If already connected to the same AP, do nothing
if (global_wifi_component->wifi_ssid() == ssid) {
// Callback to notify the user that the connection was successful
this->connect_trigger_->trigger();
return;
}
// Create a new WiFiAP object with the new SSID and password
this->new_sta_.set_ssid(ssid);
this->new_sta_.set_password(password);
// Save the current STA
this->old_sta_ = global_wifi_component->get_sta();
// Disable WiFi
global_wifi_component->disable();
// Set the state to connecting
this->connecting_ = true;
// Store the new STA so once the WiFi is enabled, it will connect to it
// This is necessary because the WiFiComponent will raise an error and fallback to the saved STA
// if trying to connect to a new STA while already connected to another one
if (this->save_.value(x...)) {
global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password());
} else {
global_wifi_component->set_sta(new_sta_);
}
// Enable WiFi
global_wifi_component->enable();
// Set timeout for the connection
this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() {
// If the timeout is reached, stop connecting and revert to the old AP
global_wifi_component->disable();
global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password());
global_wifi_component->enable();
// Start a timeout for the fallback if the connection to the old AP fails
this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() {
this->connecting_ = false;
this->error_trigger_->trigger();
});
});
}
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }
Trigger<> *get_error_trigger() const { return this->error_trigger_; }
void loop() override {
if (!this->connecting_)
return;
if (global_wifi_component->is_connected()) {
// The WiFi is connected, stop the timeout and reset the connecting flag
this->cancel_timeout("wifi-connect-timeout");
this->cancel_timeout("wifi-fallback-timeout");
this->connecting_ = false;
if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) {
// Callback to notify the user that the connection was successful
this->connect_trigger_->trigger();
} else {
// Callback to notify the user that the connection failed
this->error_trigger_->trigger();
}
}
}
protected:
bool connecting_{false};
WiFiAP new_sta_;
WiFiAP old_sta_;
Trigger<> *connect_trigger_{new Trigger<>()};
Trigger<> *error_trigger_{new Trigger<>()};
};
} // namespace esphome::wifi
#endif

View File

@@ -37,8 +37,7 @@
#include "esphome/components/esp32_improv/esp32_improv_component.h"
#endif
namespace esphome {
namespace wifi {
namespace esphome::wifi {
static const char *const TAG = "wifi";
@@ -330,6 +329,19 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; }
void WiFiComponent::setup() {
this->wifi_pre_setup_();
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
// Create semaphore for high-performance mode requests
// Start at 0, increment on request, decrement on release
this->high_performance_semaphore_ = xSemaphoreCreateCounting(UINT32_MAX, 0);
if (this->high_performance_semaphore_ == nullptr) {
ESP_LOGE(TAG, "Failed semaphore");
}
// Store the configured power save mode as baseline
this->configured_power_save_ = this->power_save_;
#endif
if (this->enable_on_boot_) {
this->start();
} else {
@@ -371,6 +383,19 @@ void WiFiComponent::start() {
ESP_LOGV(TAG, "Setting Output Power Option failed");
}
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
// Synchronize power_save_ with semaphore state before applying
if (this->high_performance_semaphore_ != nullptr) {
UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
if (semaphore_count > 0) {
this->power_save_ = WIFI_POWER_SAVE_NONE;
this->is_high_performance_mode_ = true;
} else {
this->power_save_ = this->configured_power_save_;
this->is_high_performance_mode_ = false;
}
}
#endif
if (!this->wifi_apply_power_save_()) {
ESP_LOGV(TAG, "Setting Power Save Option failed");
}
@@ -525,11 +550,37 @@ void WiFiComponent::loop() {
}
}
}
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
// Check if power save mode needs to be updated based on high-performance requests
if (this->high_performance_semaphore_ != nullptr) {
// Semaphore count directly represents active requests (starts at 0, increments on request)
UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
if (semaphore_count > 0 && !this->is_high_performance_mode_) {
// Transition to high-performance mode (no power save)
ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count,
semaphore_count == 1 ? "request" : "requests");
this->power_save_ = WIFI_POWER_SAVE_NONE;
if (this->wifi_apply_power_save_()) {
this->is_high_performance_mode_ = true;
}
} else if (semaphore_count == 0 && this->is_high_performance_mode_) {
// Restore to configured power save mode
ESP_LOGV(TAG, "Restoring power save mode to configured setting");
this->power_save_ = this->configured_power_save_;
if (this->wifi_apply_power_save_()) {
this->is_high_performance_mode_ = false;
}
}
}
#endif
}
WiFiComponent::WiFiComponent() { global_wifi_component = this; }
bool WiFiComponent::has_ap() const { return this->has_ap_; }
bool WiFiComponent::is_ap_active() const { return this->state_ == WIFI_COMPONENT_STATE_AP; }
bool WiFiComponent::has_sta() const { return !this->sta_.empty(); }
#ifdef USE_WIFI_11KV_SUPPORT
void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
@@ -1567,7 +1618,12 @@ bool WiFiComponent::is_connected() {
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_;
}
void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; }
void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) {
this->power_save_ = power_save;
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
this->configured_power_save_ = power_save;
#endif
}
void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
@@ -1586,6 +1642,38 @@ bool WiFiComponent::is_esp32_improv_active_() {
#endif
}
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
bool WiFiComponent::request_high_performance() {
// Already configured for high performance - request satisfied
if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) {
return true;
}
// Semaphore initialization failed
if (this->high_performance_semaphore_ == nullptr) {
return false;
}
// Give the semaphore (non-blocking). This increments the count.
return xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE;
}
bool WiFiComponent::release_high_performance() {
// Already configured for high performance - nothing to release
if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) {
return true;
}
// Semaphore initialization failed
if (this->high_performance_semaphore_ == nullptr) {
return false;
}
// Take the semaphore (non-blocking). This decrements the count.
return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE;
}
#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE
#ifdef USE_WIFI_FAST_CONNECT
bool WiFiComponent::load_fast_connect_settings_(WiFiAP &params) {
SavedWifiFastConnectSettings fast_connect_save{};
@@ -1725,6 +1813,5 @@ bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->
WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace wifi
} // namespace esphome
} // namespace esphome::wifi
#endif

View File

@@ -49,8 +49,12 @@ extern "C" {
#include <WiFi.h>
#endif
namespace esphome {
namespace wifi {
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#endif
namespace esphome::wifi {
/// Sentinel value for RSSI when WiFi is not connected
static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127;
@@ -308,6 +312,7 @@ class WiFiComponent : public Component {
bool has_sta() const;
bool has_ap() const;
bool is_ap_active() const;
#ifdef USE_WIFI_11KV_SUPPORT
void set_btm(bool btm);
@@ -364,6 +369,58 @@ class WiFiComponent : public Component {
int32_t get_wifi_channel();
#ifdef USE_WIFI_CALLBACKS
/// Add a callback that will be called on configuration changes (IP change, SSID change, etc.)
/// @param callback The callback to be called; template arguments are:
/// - IP addresses
/// - DNS address 1
/// - DNS address 2
void add_on_ip_state_callback(
std::function<void(network::IPAddresses, network::IPAddress, network::IPAddress)> &&callback) {
this->ip_state_callback_.add(std::move(callback));
}
/// - Wi-Fi scan results
void add_on_wifi_scan_state_callback(std::function<void(wifi_scan_vector_t<WiFiScanResult> &)> &&callback) {
this->wifi_scan_state_callback_.add(std::move(callback));
}
/// - Wi-Fi SSID
/// - Wi-Fi BSSID
void add_on_wifi_connect_state_callback(std::function<void(std::string, wifi::bssid_t)> &&callback) {
this->wifi_connect_state_callback_.add(std::move(callback));
}
#endif // USE_WIFI_CALLBACKS
#ifdef USE_WIFI_RUNTIME_POWER_SAVE
/** Request high-performance mode (no power saving) for improved WiFi latency.
*
* Components that need maximum WiFi performance (e.g., audio streaming, large data transfers)
* can call this method to temporarily disable WiFi power saving. Multiple components can
* request high performance simultaneously using a counting semaphore.
*
* Power saving will be restored to the YAML-configured mode when all components have
* called release_high_performance().
*
* Note: Only supported on ESP32.
*
* @return true if request was satisfied (high-performance mode active or already configured),
* false if operation failed (semaphore error)
*/
bool request_high_performance();
/** Release a high-performance mode request.
*
* Should be called when a component no longer needs maximum WiFi latency.
* When all requests are released (semaphore count reaches zero), WiFi power saving
* is restored to the YAML-configured mode.
*
* Note: Only supported on ESP32.
*
* @return true if release was successful (or already in high-performance config),
* false if operation failed (semaphore error)
*/
bool release_high_performance();
#endif // USE_WIFI_RUNTIME_POWER_SAVE
protected:
#ifdef USE_WIFI_AP
void setup_ap_config_();
@@ -489,6 +546,11 @@ class WiFiComponent : public Component {
WiFiAP ap_;
#endif
optional<float> output_power_;
#ifdef USE_WIFI_CALLBACKS
CallbackManager<void(network::IPAddresses, network::IPAddress, network::IPAddress)> ip_state_callback_;
CallbackManager<void(wifi_scan_vector_t<WiFiScanResult> &)> wifi_scan_state_callback_;
CallbackManager<void(std::string, wifi::bssid_t)> wifi_connect_state_callback_;
#endif // USE_WIFI_CALLBACKS
ESPPreferenceObject pref_;
#ifdef USE_WIFI_FAST_CONNECT
ESPPreferenceObject fast_connect_pref_;
@@ -534,6 +596,12 @@ class WiFiComponent : public Component {
bool keep_scan_results_{false};
bool did_scan_this_cycle_{false};
bool skip_cooldown_next_cycle_{false};
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE};
bool is_high_performance_mode_{false};
SemaphoreHandle_t high_performance_semaphore_{nullptr};
#endif
// Pointers at the end (naturally aligned)
Trigger<> *connect_trigger_{new Trigger<>()};
@@ -547,107 +615,5 @@ class WiFiComponent : public Component {
extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
template<typename... Ts> class WiFiConnectedCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return global_wifi_component->is_connected(); }
};
template<typename... Ts> class WiFiEnabledCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); }
};
template<typename... Ts> class WiFiEnableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_wifi_component->enable(); }
};
template<typename... Ts> class WiFiDisableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_wifi_component->disable(); }
};
template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, public Component {
public:
TEMPLATABLE_VALUE(std::string, ssid)
TEMPLATABLE_VALUE(std::string, password)
TEMPLATABLE_VALUE(bool, save)
TEMPLATABLE_VALUE(uint32_t, connection_timeout)
void play(const Ts &...x) override {
auto ssid = this->ssid_.value(x...);
auto password = this->password_.value(x...);
// Avoid multiple calls
if (this->connecting_)
return;
// If already connected to the same AP, do nothing
if (global_wifi_component->wifi_ssid() == ssid) {
// Callback to notify the user that the connection was successful
this->connect_trigger_->trigger();
return;
}
// Create a new WiFiAP object with the new SSID and password
this->new_sta_.set_ssid(ssid);
this->new_sta_.set_password(password);
// Save the current STA
this->old_sta_ = global_wifi_component->get_sta();
// Disable WiFi
global_wifi_component->disable();
// Set the state to connecting
this->connecting_ = true;
// Store the new STA so once the WiFi is enabled, it will connect to it
// This is necessary because the WiFiComponent will raise an error and fallback to the saved STA
// if trying to connect to a new STA while already connected to another one
if (this->save_.value(x...)) {
global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password());
} else {
global_wifi_component->set_sta(new_sta_);
}
// Enable WiFi
global_wifi_component->enable();
// Set timeout for the connection
this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() {
// If the timeout is reached, stop connecting and revert to the old AP
global_wifi_component->disable();
global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password());
global_wifi_component->enable();
// Start a timeout for the fallback if the connection to the old AP fails
this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() {
this->connecting_ = false;
this->error_trigger_->trigger();
});
});
}
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }
Trigger<> *get_error_trigger() const { return this->error_trigger_; }
void loop() override {
if (!this->connecting_)
return;
if (global_wifi_component->is_connected()) {
// The WiFi is connected, stop the timeout and reset the connecting flag
this->cancel_timeout("wifi-connect-timeout");
this->cancel_timeout("wifi-fallback-timeout");
this->connecting_ = false;
if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) {
// Callback to notify the user that the connection was successful
this->connect_trigger_->trigger();
} else {
// Callback to notify the user that the connection failed
this->error_trigger_->trigger();
}
}
}
protected:
bool connecting_{false};
WiFiAP new_sta_;
WiFiAP old_sta_;
Trigger<> *connect_trigger_{new Trigger<>()};
Trigger<> *error_trigger_{new Trigger<>()};
};
} // namespace wifi
} // namespace esphome
} // namespace esphome::wifi
#endif

View File

@@ -38,8 +38,7 @@ extern "C" {
#include "esphome/core/log.h"
#include "esphome/core/util.h"
namespace esphome {
namespace wifi {
namespace esphome::wifi {
static const char *const TAG = "wifi_esp8266";
@@ -514,6 +513,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(),
it.channel);
s_sta_connected = true;
#ifdef USE_WIFI_CALLBACKS
global_wifi_component->wifi_connect_state_callback_.call(global_wifi_component->wifi_ssid(),
global_wifi_component->wifi_bssid());
#endif
break;
}
case EVENT_STAMODE_DISCONNECTED: {
@@ -533,6 +536,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
}
s_sta_connected = false;
s_sta_connecting = false;
#ifdef USE_WIFI_CALLBACKS
global_wifi_component->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0}));
#endif
break;
}
case EVENT_STAMODE_AUTHMODE_CHANGE: {
@@ -555,6 +561,11 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(),
format_ip_addr(it.mask).c_str());
s_sta_got_ip = true;
#ifdef USE_WIFI_CALLBACKS
global_wifi_component->ip_state_callback_.call(global_wifi_component->wifi_sta_ip_addresses(),
global_wifi_component->get_dns_address(0),
global_wifi_component->get_dns_address(1));
#endif
break;
}
case EVENT_STAMODE_DHCP_TIMEOUT: {
@@ -729,6 +740,9 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
it->is_hidden != 0);
}
this->scan_done_ = true;
#ifdef USE_WIFI_CALLBACKS
global_wifi_component->wifi_scan_state_callback_.call(global_wifi_component->scan_result_);
#endif
}
#ifdef USE_WIFI_AP
@@ -885,8 +899,6 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {(const ip_addr_t *) WiFi.dnsIP(num)}; }
void WiFiComponent::wifi_loop_() {}
} // namespace wifi
} // namespace esphome
} // namespace esphome::wifi
#endif
#endif

View File

@@ -41,8 +41,7 @@
#include "esphome/core/log.h"
#include "esphome/core/util.h"
namespace esphome {
namespace wifi {
namespace esphome::wifi {
static const char *const TAG = "wifi_esp32";
@@ -728,6 +727,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
s_sta_connected = true;
#ifdef USE_WIFI_CALLBACKS
this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid());
#endif
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) {
const auto &it = data->data.sta_disconnected;
@@ -751,6 +753,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
s_sta_connected = false;
s_sta_connecting = false;
error_from_callback_ = true;
#ifdef USE_WIFI_CALLBACKS
this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0}));
#endif
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) {
const auto &it = data->data.ip_got_ip;
@@ -759,12 +764,18 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
#endif /* USE_NETWORK_IPV6 */
ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw));
this->got_ipv4_address_ = true;
#ifdef USE_WIFI_CALLBACKS
this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
#endif
#if USE_NETWORK_IPV6
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) {
const auto &it = data->data.ip_got_ip6;
ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip));
this->num_ipv6_addresses_++;
#ifdef USE_WIFI_CALLBACKS
this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
#endif
#endif /* USE_NETWORK_IPV6 */
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) {
@@ -804,6 +815,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN,
ssid.empty());
}
#ifdef USE_WIFI_CALLBACKS
this->wifi_scan_state_callback_.call(this->scan_result_);
#endif
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) {
ESP_LOGV(TAG, "AP start");
@@ -1088,8 +1102,6 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
return network::IPAddress(dns_ip);
}
} // namespace wifi
} // namespace esphome
} // namespace esphome::wifi
#endif // USE_ESP32
#endif

View File

@@ -15,8 +15,7 @@
#include "esphome/core/log.h"
#include "esphome/core/util.h"
namespace esphome {
namespace wifi {
namespace esphome::wifi {
static const char *const TAG = "wifi_lt";
@@ -288,7 +287,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
buf[it.ssid_len] = '\0';
ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
#ifdef USE_WIFI_CALLBACKS
this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid());
#endif
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: {
@@ -314,6 +315,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
}
s_sta_connecting = false;
#ifdef USE_WIFI_CALLBACKS
this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0}));
#endif
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: {
@@ -335,11 +339,17 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(),
format_ip4_addr(WiFi.gatewayIP()).c_str());
s_sta_connecting = false;
#ifdef USE_WIFI_CALLBACKS
this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
#endif
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: {
// auto it = info.got_ip.ip_info;
ESP_LOGV(TAG, "Got IPv6");
#ifdef USE_WIFI_CALLBACKS
this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
#endif
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: {
@@ -433,6 +443,9 @@ void WiFiComponent::wifi_scan_done_callback_() {
}
WiFi.scanDelete();
this->scan_done_ = true;
#ifdef USE_WIFI_CALLBACKS
this->wifi_scan_state_callback_.call(this->scan_result_);
#endif
}
#ifdef USE_WIFI_AP
@@ -493,8 +506,6 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; }
void WiFiComponent::wifi_loop_() {}
} // namespace wifi
} // namespace esphome
} // namespace esphome::wifi
#endif // USE_LIBRETINY
#endif

View File

@@ -1,4 +1,3 @@
#include "wifi_component.h"
#ifdef USE_WIFI
@@ -15,11 +14,14 @@
#include "esphome/core/log.h"
#include "esphome/core/util.h"
namespace esphome {
namespace wifi {
namespace esphome::wifi {
static const char *const TAG = "wifi_pico_w";
// Track previous state for detecting changes
static bool s_sta_was_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_had_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
if (sta.has_value()) {
if (sta.value()) {
@@ -51,7 +53,7 @@ bool WiFiComponent::wifi_apply_power_save_() {
return ret == 0;
}
// TODO: The driver doesnt seem to have an API for this
// TODO: The driver doesn't seem to have an API for this
bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; }
bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
@@ -219,16 +221,61 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
}
void WiFiComponent::wifi_loop_() {
// Handle scan completion
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
this->scan_done_ = true;
ESP_LOGV(TAG, "Scan done");
#ifdef USE_WIFI_CALLBACKS
this->wifi_scan_state_callback_.call(this->scan_result_);
#endif
}
// Poll for connection state changes
// The arduino-pico WiFi library doesn't have event callbacks like ESP8266/ESP32,
// so we need to poll the link status to detect state changes
auto status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA);
bool is_connected = (status == CYW43_LINK_UP);
// Detect connection state change
if (is_connected && !s_sta_was_connected) {
// Just connected
s_sta_was_connected = true;
ESP_LOGV(TAG, "Connected");
#ifdef USE_WIFI_CALLBACKS
this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid());
#endif
} else if (!is_connected && s_sta_was_connected) {
// Just disconnected
s_sta_was_connected = false;
s_sta_had_ip = false;
ESP_LOGV(TAG, "Disconnected");
#ifdef USE_WIFI_CALLBACKS
this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0}));
#endif
}
// Detect IP address changes (only when connected)
if (is_connected) {
bool has_ip = false;
// Check for any IP address (IPv4 or IPv6)
for (auto addr : addrList) {
has_ip = true;
break;
}
if (has_ip && !s_sta_had_ip) {
// Just got IP address
s_sta_had_ip = true;
ESP_LOGV(TAG, "Got IP address");
#ifdef USE_WIFI_CALLBACKS
this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
#endif
}
}
}
void WiFiComponent::wifi_pre_setup_() {}
} // namespace wifi
} // namespace esphome
} // namespace esphome::wifi
#endif
#endif

View File

@@ -15,31 +15,27 @@ DEPENDENCIES = ["wifi"]
wifi_info_ns = cg.esphome_ns.namespace("wifi_info")
IPAddressWiFiInfo = wifi_info_ns.class_(
"IPAddressWiFiInfo", text_sensor.TextSensor, cg.PollingComponent
"IPAddressWiFiInfo", text_sensor.TextSensor, cg.Component
)
ScanResultsWiFiInfo = wifi_info_ns.class_(
"ScanResultsWiFiInfo", text_sensor.TextSensor, cg.PollingComponent
)
SSIDWiFiInfo = wifi_info_ns.class_(
"SSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent
"ScanResultsWiFiInfo", text_sensor.TextSensor, cg.Component
)
SSIDWiFiInfo = wifi_info_ns.class_("SSIDWiFiInfo", text_sensor.TextSensor, cg.Component)
BSSIDWiFiInfo = wifi_info_ns.class_(
"BSSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent
"BSSIDWiFiInfo", text_sensor.TextSensor, cg.Component
)
MacAddressWifiInfo = wifi_info_ns.class_(
"MacAddressWifiInfo", text_sensor.TextSensor, cg.Component
)
DNSAddressWifiInfo = wifi_info_ns.class_(
"DNSAddressWifiInfo", text_sensor.TextSensor, cg.PollingComponent
"DNSAddressWifiInfo", text_sensor.TextSensor, cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema(
IPAddressWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
)
.extend(cv.polling_component_schema("1s"))
.extend(
).extend(
{
cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
@@ -49,22 +45,31 @@ CONFIG_SCHEMA = cv.Schema(
),
cv.Optional(CONF_SCAN_RESULTS): text_sensor.text_sensor_schema(
ScanResultsWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
).extend(cv.polling_component_schema("60s")),
),
cv.Optional(CONF_SSID): text_sensor.text_sensor_schema(
SSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
).extend(cv.polling_component_schema("1s")),
),
cv.Optional(CONF_BSSID): text_sensor.text_sensor_schema(
BSSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
).extend(cv.polling_component_schema("1s")),
),
cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema(
MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_DNS_ADDRESS): text_sensor.text_sensor_schema(
DNSAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
).extend(cv.polling_component_schema("1s")),
),
}
)
# Keys that require WiFi callbacks
_NETWORK_INFO_KEYS = {
CONF_SSID,
CONF_BSSID,
CONF_IP_ADDRESS,
CONF_DNS_ADDRESS,
CONF_SCAN_RESULTS,
}
async def setup_conf(config, key):
if key in config:
@@ -74,6 +79,10 @@ async def setup_conf(config, key):
async def to_code(config):
# Request WiFi callbacks for any sensor that needs them
if _NETWORK_INFO_KEYS.intersection(config):
wifi.request_wifi_callbacks()
await setup_conf(config, CONF_SSID)
await setup_conf(config, CONF_BSSID)
await setup_conf(config, CONF_MAC_ADDRESS)

View File

@@ -2,18 +2,121 @@
#ifdef USE_WIFI
#include "esphome/core/log.h"
namespace esphome {
namespace wifi_info {
namespace esphome::wifi_info {
static const char *const TAG = "wifi_info";
static constexpr size_t MAX_STATE_LENGTH = 255;
/********************
* IPAddressWiFiInfo
*******************/
void IPAddressWiFiInfo::setup() {
wifi::global_wifi_component->add_on_ip_state_callback(
[this](network::IPAddresses ips, network::IPAddress dns1_ip, network::IPAddress dns2_ip) {
this->state_callback_(ips);
});
}
void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); }
void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); }
void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); }
void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); }
void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); }
void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) {
this->publish_state(ips[0].str());
uint8_t sensor = 0;
for (auto &ip : ips) {
if (ip.is_set()) {
if (this->ip_sensors_[sensor] != nullptr) {
this->ip_sensors_[sensor]->publish_state(ip.str());
}
sensor++;
}
}
}
/*********************
* DNSAddressWifiInfo
********************/
void DNSAddressWifiInfo::setup() {
wifi::global_wifi_component->add_on_ip_state_callback(
[this](network::IPAddresses ips, network::IPAddress dns1_ip, network::IPAddress dns2_ip) {
this->state_callback_(dns1_ip, dns2_ip);
});
}
void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); }
} // namespace wifi_info
} // namespace esphome
void DNSAddressWifiInfo::state_callback_(network::IPAddress dns1_ip, network::IPAddress dns2_ip) {
std::string dns_results = dns1_ip.str() + " " + dns2_ip.str();
this->publish_state(dns_results);
}
/**********************
* ScanResultsWiFiInfo
*********************/
void ScanResultsWiFiInfo::setup() {
wifi::global_wifi_component->add_on_wifi_scan_state_callback(
[this](const wifi::wifi_scan_vector_t<wifi::WiFiScanResult> &results) { this->state_callback_(results); });
}
void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); }
void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_t<wifi::WiFiScanResult> &results) {
std::string scan_results;
for (const auto &scan : results) {
if (scan.get_is_hidden())
continue;
scan_results += scan.get_ssid();
scan_results += ": ";
scan_results += esphome::to_string(scan.get_rssi());
scan_results += "dB\n";
}
// There's a limit of 255 characters per state; longer states just don't get sent so we truncate it
if (scan_results.length() > MAX_STATE_LENGTH) {
scan_results.resize(MAX_STATE_LENGTH);
}
this->publish_state(scan_results);
}
/***************
* SSIDWiFiInfo
**************/
void SSIDWiFiInfo::setup() {
wifi::global_wifi_component->add_on_wifi_connect_state_callback(
[this](const std::string &ssid, wifi::bssid_t bssid) { this->state_callback_(ssid); });
}
void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); }
void SSIDWiFiInfo::state_callback_(const std::string &ssid) { this->publish_state(ssid); }
/****************
* BSSIDWiFiInfo
***************/
void BSSIDWiFiInfo::setup() {
wifi::global_wifi_component->add_on_wifi_connect_state_callback(
[this](const std::string &ssid, wifi::bssid_t bssid) { this->state_callback_(bssid); });
}
void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); }
void BSSIDWiFiInfo::state_callback_(wifi::bssid_t bssid) {
char buf[18] = "unknown";
if (mac_address_is_valid(bssid.data())) {
format_mac_addr_upper(bssid.data(), buf);
}
this->publish_state(buf);
}
/*********************
* MacAddressWifiInfo
********************/
void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); }
} // namespace esphome::wifi_info
#endif

View File

@@ -7,121 +7,54 @@
#ifdef USE_WIFI
#include <array>
namespace esphome {
namespace wifi_info {
namespace esphome::wifi_info {
static constexpr size_t MAX_STATE_LENGTH = 255;
class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor {
class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor {
public:
void update() override {
auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses();
if (ips != this->last_ips_) {
this->last_ips_ = ips;
this->publish_state(ips[0].str());
uint8_t sensor = 0;
for (auto &ip : ips) {
if (ip.is_set()) {
if (this->ip_sensors_[sensor] != nullptr) {
this->ip_sensors_[sensor]->publish_state(ip.str());
}
sensor++;
}
}
}
}
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void setup() override;
void dump_config() override;
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
protected:
network::IPAddresses last_ips_;
void state_callback_(const network::IPAddresses &ips);
std::array<text_sensor::TextSensor *, 5> ip_sensors_;
};
class DNSAddressWifiInfo : public PollingComponent, public text_sensor::TextSensor {
class DNSAddressWifiInfo : public Component, public text_sensor::TextSensor {
public:
void update() override {
auto dns_one = wifi::global_wifi_component->get_dns_address(0);
auto dns_two = wifi::global_wifi_component->get_dns_address(1);
std::string dns_results = dns_one.str() + " " + dns_two.str();
if (dns_results != this->last_results_) {
this->last_results_ = dns_results;
this->publish_state(dns_results);
}
}
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void setup() override;
void dump_config() override;
protected:
std::string last_results_;
void state_callback_(network::IPAddress dns1_ip, network::IPAddress dns2_ip);
};
class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSensor {
class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor {
public:
void update() override {
std::string scan_results;
for (auto &scan : wifi::global_wifi_component->get_scan_result()) {
if (scan.get_is_hidden())
continue;
scan_results += scan.get_ssid();
scan_results += ": ";
scan_results += esphome::to_string(scan.get_rssi());
scan_results += "dB\n";
}
// There's a limit of 255 characters per state.
// Longer states just don't get sent so we truncate it.
if (scan_results.length() > MAX_STATE_LENGTH) {
scan_results.resize(MAX_STATE_LENGTH);
}
if (this->last_scan_results_ != scan_results) {
this->last_scan_results_ = scan_results;
this->publish_state(scan_results);
}
}
void setup() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void dump_config() override;
protected:
std::string last_scan_results_;
void state_callback_(const wifi::wifi_scan_vector_t<wifi::WiFiScanResult> &results);
};
class SSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor {
class SSIDWiFiInfo : public Component, public text_sensor::TextSensor {
public:
void update() override {
std::string ssid = wifi::global_wifi_component->wifi_ssid();
if (this->last_ssid_ != ssid) {
this->last_ssid_ = ssid;
this->publish_state(this->last_ssid_);
}
}
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void setup() override;
void dump_config() override;
protected:
std::string last_ssid_;
void state_callback_(const std::string &ssid);
};
class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor {
class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor {
public:
void update() override {
wifi::bssid_t bssid = wifi::global_wifi_component->wifi_bssid();
if (memcmp(bssid.data(), last_bssid_.data(), 6) != 0) {
std::copy(bssid.begin(), bssid.end(), last_bssid_.begin());
char buf[18];
format_mac_addr_upper(bssid.data(), buf);
this->publish_state(buf);
}
}
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void setup() override;
void dump_config() override;
protected:
wifi::bssid_t last_bssid_;
void state_callback_(wifi::bssid_t bssid);
};
class MacAddressWifiInfo : public Component, public text_sensor::TextSensor {
@@ -133,6 +66,5 @@ class MacAddressWifiInfo : public Component, public text_sensor::TextSensor {
void dump_config() override;
};
} // namespace wifi_info
} // namespace esphome
} // namespace esphome::wifi_info
#endif

View File

@@ -211,6 +211,8 @@
#define USE_WEBSERVER_SORTING
#define USE_WIFI_11KV_SUPPORT
#define USE_WIFI_FAST_CONNECT
#define USE_WIFI_CALLBACKS
#define USE_WIFI_RUNTIME_POWER_SAVE
#define USB_HOST_MAX_REQUESTS 16
#ifdef USE_ARDUINO

View File

@@ -22,7 +22,7 @@ pillow==11.3.0
cairosvg==2.8.2
freetype-py==2.5.1
jinja2==3.1.6
bleak==1.1.1
bleak==2.0.0
# esp-idf >= 5.0 requires this
pyparsing >= 3.0

View File

@@ -1,6 +1,10 @@
esphome:
on_boot:
then:
- wait_until:
condition:
api.connected:
state_subscription_only: true
- homeassistant.event:
event: esphome.button_pressed
data:

View File

@@ -39,6 +39,15 @@ sensor:
return 0.0;
update_interval: 60s
text:
- platform: template
name: "Template text"
optimistic: true
min_length: 0
max_length: 100
mode: text
initial_value: "Hello World"
text_sensor:
- platform: version
name: "ESPHome Version"
@@ -52,6 +61,25 @@ text_sensor:
return {"Goodbye (cruel) World"};
update_interval: 60s
event:
- platform: template
name: "Template Event"
id: template_event1
event_types:
- "custom_event_1"
- "custom_event_2"
button:
- platform: template
name: "Template Event Button"
on_press:
- logger.log: "Template Event Button pressed"
- lambda: |-
ESP_LOGD("template_event_button", "Template Event Button pressed");
- event.trigger:
id: template_event1
event_type: custom_event_1
binary_sensor:
- platform: template
id: template_binary_sensor1

View File

@@ -0,0 +1,13 @@
esp32_ble_tracker:
sensor:
- platform: thermopro_ble
mac_address: FE:74:B8:6A:97:B7
temperature:
name: "ThermoPro Temperature"
humidity:
name: "ThermoPro Humidity"
battery_level:
name: "ThermoPro Battery Level"
signal_strength:
name: "ThermoPro Signal Strength"

View File

@@ -0,0 +1,4 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -10,6 +10,10 @@ esphome:
- logger.log: "Connected to WiFi!"
on_error:
- logger.log: "Failed to connect to WiFi!"
- if:
condition: wifi.ap_active
then:
- logger.log: "WiFi AP is active!"
wifi:
networks:

View File

@@ -1,5 +1,16 @@
psram:
# Tests the high performance request and release; requires the USE_WIFI_RUNTIME_POWER_SAVE define
esphome:
platformio_options:
build_flags:
- "-DUSE_WIFI_RUNTIME_POWER_SAVE"
on_boot:
- then:
- lambda: |-
esphome::wifi::global_wifi_component->request_high_performance();
esphome::wifi::global_wifi_component->release_high_performance();
wifi:
use_psram: true
min_auth_mode: WPA