mirror of
https://github.com/esphome/esphome.git
synced 2026-09-14 16:48:40 +00:00
Merge remote-tracking branch 'upstream/web_server_idf_session_ownership' into integration
This commit is contained in:
@@ -441,6 +441,7 @@ esphome/components/sen21231/* @shreyaskarnik
|
||||
esphome/components/sen5x/* @martgras
|
||||
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
|
||||
esphome/components/sendspin/* @kahrendt
|
||||
esphome/components/sendspin/media_player/* @kahrendt
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/serial_proxy/* @kbx81
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
)
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -33,6 +34,7 @@ from esphome.const import (
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_NRF52,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
@@ -304,7 +306,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_NRF52]),
|
||||
validate_config,
|
||||
)
|
||||
|
||||
@@ -369,6 +371,8 @@ async def to_code(config):
|
||||
|
||||
if CONF_TOUCH_WAKEUP in config:
|
||||
cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP]))
|
||||
if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations:
|
||||
zephyr_add_prj_conf("POWEROFF", True)
|
||||
|
||||
cg.add_define("USE_DEEP_SLEEP")
|
||||
|
||||
@@ -413,7 +417,7 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
if CONF_SLEEP_DURATION in config:
|
||||
template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32)
|
||||
template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.uint32)
|
||||
cg.add(var.set_sleep_duration(template_))
|
||||
|
||||
if CONF_UNTIL in config:
|
||||
|
||||
@@ -59,6 +59,8 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
lt_deep_sleep_enter();
|
||||
}
|
||||
|
||||
bool DeepSleepComponent::should_teardown_() { return true; }
|
||||
|
||||
} // namespace esphome::deep_sleep
|
||||
|
||||
#endif // USE_BK72XX
|
||||
|
||||
@@ -9,11 +9,22 @@ static const char *const TAG = "deep_sleep";
|
||||
// 5 seconds for deep sleep to ensure clean disconnect from Home Assistant
|
||||
static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000;
|
||||
|
||||
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
std::atomic<DeepSleepComponent *> global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void DeepSleepComponent::setup() {
|
||||
#ifdef USE_ZEPHYR
|
||||
k_sem_init(&this->wakeup_sem_, 0, 1);
|
||||
#endif
|
||||
global_has_deep_sleep = true;
|
||||
this->schedule_sleep_();
|
||||
// It can be used from another thread for waking up the device.
|
||||
// It should be called as last item in setup.
|
||||
global_deep_sleep.store(this);
|
||||
}
|
||||
|
||||
void DeepSleepComponent::schedule_sleep_() {
|
||||
this->next_enter_deep_sleep_ = false;
|
||||
const optional<uint32_t> run_duration = get_run_duration_();
|
||||
if (run_duration.has_value()) {
|
||||
ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration);
|
||||
@@ -58,13 +69,17 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
if (this->sleep_duration_.has_value()) {
|
||||
ESP_LOGI(TAG, "Sleeping for %" PRId64 "us", *this->sleep_duration_);
|
||||
}
|
||||
App.run_safe_shutdown_hooks();
|
||||
// It's critical to teardown components cleanly for deep sleep to ensure
|
||||
// Home Assistant sees a clean disconnect instead of marking the device unavailable
|
||||
App.teardown_components(TEARDOWN_TIMEOUT_DEEP_SLEEP_MS);
|
||||
App.run_powerdown_hooks();
|
||||
|
||||
if (this->should_teardown_()) {
|
||||
App.run_safe_shutdown_hooks();
|
||||
// It's critical to teardown components cleanly for deep sleep to ensure
|
||||
// Home Assistant sees a clean disconnect instead of marking the device unavailable
|
||||
App.teardown_components(TEARDOWN_TIMEOUT_DEEP_SLEEP_MS);
|
||||
App.run_powerdown_hooks();
|
||||
}
|
||||
|
||||
this->deep_sleep_();
|
||||
this->schedule_sleep_();
|
||||
}
|
||||
|
||||
float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <atomic>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_sleep.h>
|
||||
@@ -14,6 +15,10 @@
|
||||
#include "esphome/core/time.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <zephyr/kernel.h>
|
||||
#endif
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
@@ -120,6 +125,9 @@ class DeepSleepComponent : public Component {
|
||||
|
||||
void prevent_deep_sleep();
|
||||
void allow_deep_sleep();
|
||||
#ifdef USE_ZEPHYR
|
||||
void wakeup();
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Returns nullopt if no run duration is set. Otherwise, returns the run
|
||||
@@ -129,6 +137,8 @@ class DeepSleepComponent : public Component {
|
||||
void dump_config_platform_();
|
||||
bool prepare_to_sleep_();
|
||||
void deep_sleep_();
|
||||
void schedule_sleep_();
|
||||
bool should_teardown_();
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
bool pin_prevents_sleep_(WakeUpPinItem &pinItem) const;
|
||||
@@ -157,6 +167,9 @@ class DeepSleepComponent : public Component {
|
||||
optional<uint32_t> run_duration_;
|
||||
bool next_enter_deep_sleep_{false};
|
||||
bool prevent_{false};
|
||||
#ifdef USE_ZEPHYR
|
||||
k_sem wakeup_sem_;
|
||||
#endif
|
||||
};
|
||||
|
||||
extern bool global_has_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -243,5 +256,8 @@ template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, publ
|
||||
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); }
|
||||
};
|
||||
|
||||
extern std::atomic<DeepSleepComponent *>
|
||||
global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace deep_sleep
|
||||
} // namespace esphome
|
||||
|
||||
@@ -165,6 +165,8 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
bool DeepSleepComponent::should_teardown_() { return true; }
|
||||
|
||||
} // namespace deep_sleep
|
||||
} // namespace esphome
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -18,6 +18,8 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
ESP.deepSleep(this->sleep_duration_.value_or(0)); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
bool DeepSleepComponent::should_teardown_() { return true; }
|
||||
|
||||
} // namespace deep_sleep
|
||||
} // namespace esphome
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "deep_sleep_component.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/log.h"
|
||||
#include <zephyr/sys/poweroff.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/stats/stats.h>
|
||||
#include <zephyr/pm/pm.h>
|
||||
|
||||
namespace esphome::deep_sleep {
|
||||
|
||||
static const char *const TAG = "deep_sleep";
|
||||
|
||||
void DeepSleepComponent::wakeup() { k_sem_give(&this->wakeup_sem_); }
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
void DeepSleepComponent::dump_config_platform_() {}
|
||||
|
||||
bool DeepSleepComponent::prepare_to_sleep_() { return true; }
|
||||
|
||||
void DeepSleepComponent::deep_sleep_() {
|
||||
k_timeout_t sleep_duration = K_FOREVER;
|
||||
if (this->sleep_duration_.has_value()) {
|
||||
sleep_duration = K_USEC(*this->sleep_duration_);
|
||||
} else {
|
||||
#ifndef USE_ZIGBEE
|
||||
// the device can be woken up through one of the following signals:
|
||||
// - The DETECT signal, optionally generated by the GPIO peripheral.
|
||||
// - The ANADETECT signal, optionally generated by the LPCOMP module.
|
||||
// - The SENSE signal, optionally generated by the NFC module to wake-on-field.
|
||||
// - Detecting a valid USB voltage on the VBUS pin (VBUS,DETECT).
|
||||
// - A reset.
|
||||
//
|
||||
// The system is reset when it wakes up from System OFF mode.
|
||||
sys_poweroff();
|
||||
#endif
|
||||
}
|
||||
// It might wake up immediately if k_sem_give was called again after wake up
|
||||
int ret = k_sem_take(&this->wakeup_sem_, sleep_duration);
|
||||
if (ret == 0) {
|
||||
ESP_LOGD(TAG, "Woken up by another thread");
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Timeout expired (normal sleep)");
|
||||
}
|
||||
}
|
||||
|
||||
bool DeepSleepComponent::should_teardown_() {
|
||||
if (this->sleep_duration_.has_value()) {
|
||||
return false;
|
||||
}
|
||||
#ifdef USE_ZIGBEE
|
||||
return false;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::deep_sleep
|
||||
|
||||
#endif
|
||||
@@ -472,14 +472,15 @@ async def _late_logger_init(config: ConfigType) -> None:
|
||||
# esphome implement own fatal error handler which save PC/LR before reset
|
||||
zephyr_add_prj_conf("RESET_ON_FATAL_ERROR", False)
|
||||
zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True)
|
||||
if config[CONF_HARDWARE_UART] == UART0:
|
||||
zephyr_add_overlay("""&uart0 { status = "okay";};""")
|
||||
if config[CONF_HARDWARE_UART] == UART1:
|
||||
zephyr_add_overlay("""&uart1 { status = "okay";};""")
|
||||
if config[CONF_HARDWARE_UART] == USB_CDC:
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
|
||||
zephyr_add_prj_conf("UART_LINE_CTRL", True)
|
||||
zephyr_add_cdc_acm(config, 0)
|
||||
if has_serial_logging:
|
||||
if config[CONF_HARDWARE_UART] == UART0:
|
||||
zephyr_add_overlay("""&uart0 { status = "okay";};""")
|
||||
if config[CONF_HARDWARE_UART] == UART1:
|
||||
zephyr_add_overlay("""&uart1 { status = "okay";};""")
|
||||
if config[CONF_HARDWARE_UART] == USB_CDC:
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
|
||||
zephyr_add_prj_conf("UART_LINE_CTRL", True)
|
||||
zephyr_add_cdc_acm(config, 0)
|
||||
|
||||
# Register at end for safe mode
|
||||
await cg.register_component(log, config)
|
||||
|
||||
@@ -65,10 +65,12 @@ void Logger::pre_setup() {
|
||||
break;
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
#ifdef CONFIG_USB_DEVICE_STACK
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
|
||||
if (device_is_ready(uart_dev)) {
|
||||
usb_enable(nullptr);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, network, psram, socket, wifi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
# mdns for autodiscovery
|
||||
@@ -13,6 +15,8 @@ CODEOWNERS = ["@kahrendt"]
|
||||
DEPENDENCIES = ["network"]
|
||||
DOMAIN = "sendspin"
|
||||
|
||||
CONF_SENDSPIN_ID = "sendspin_id"
|
||||
|
||||
# Trailing underscore avoids clashing with sendspin-cpp's global `sendspin` namespace.
|
||||
# Analysis tools strip the trailing underscore (same pattern as `template_`).
|
||||
sendspin_ns = cg.esphome_ns.namespace("sendspin_")
|
||||
@@ -22,6 +26,13 @@ SendspinHub = sendspin_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
SendspinSwitchCommandAction = sendspin_ns.class_(
|
||||
"SendspinSwitchCommandAction",
|
||||
automation.Action,
|
||||
cg.Parented.template(SendspinHub),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SendspinConfiguration:
|
||||
artwork_support: bool = False
|
||||
@@ -101,6 +112,41 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _request_controller_role(config: ConfigType) -> ConfigType:
|
||||
"""Request the controller role for the sendspin.switch action."""
|
||||
request_controller_support()
|
||||
return config
|
||||
|
||||
|
||||
SENDSPIN_SIMPLE_ACTION_SCHEMA = cv.All(
|
||||
automation.maybe_simple_id(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(SendspinHub),
|
||||
}
|
||||
)
|
||||
),
|
||||
_request_controller_role,
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"sendspin.switch",
|
||||
SendspinSwitchCommandAction,
|
||||
SENDSPIN_SIMPLE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def sendspin_switch_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "sendspin_hub.h"
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
#ifdef USE_SENDSPIN_CONTROLLER
|
||||
template<typename... Ts> class SendspinSwitchCommandAction : public Action<Ts...>, public Parented<SendspinHub> {
|
||||
public:
|
||||
void play(const Ts &...x) override {
|
||||
// Clear any EXTERNAL_SOURCE state so the switch command is followed
|
||||
this->parent_->update_state(sendspin::SendspinClientState::SYNCHRONIZED);
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::SWITCH);
|
||||
}
|
||||
};
|
||||
#endif // USE_SENDSPIN_CONTROLLER
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -0,0 +1,45 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import media_player
|
||||
from esphome.components.const import CONF_VOLUME_INCREMENT
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_SENDSPIN_ID, SendspinHub, request_controller_support, sendspin_ns
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
DEPENDENCIES = ["sendspin"]
|
||||
|
||||
SendspinMediaPlayer = sendspin_ns.class_(
|
||||
"SendspinMediaPlayer",
|
||||
media_player.MediaPlayer,
|
||||
cg.Component,
|
||||
)
|
||||
|
||||
|
||||
def _request_roles(config: ConfigType) -> ConfigType:
|
||||
"""Request the necessary Sendspin roles for the media player."""
|
||||
request_controller_support()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
media_player.media_player_schema(SendspinMediaPlayer).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub),
|
||||
cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage,
|
||||
}
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
_request_roles,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_SENDSPIN_ID])
|
||||
await media_player.register_media_player(var, config)
|
||||
|
||||
cg.add(var.set_volume_increment(config[CONF_VOLUME_INCREMENT]))
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "sendspin_media_player.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_MEDIA_PLAYER) && defined(USE_SENDSPIN_CONTROLLER)
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <sendspin/types.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include <esp_timer.h>
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
static const char *const TAG = "sendspin.media_player";
|
||||
|
||||
// THREAD CONTEXT: Main loop. The callbacks registered here also fire on the main loop,
|
||||
// since SendspinHub dispatches group updates and controller state from client_->loop().
|
||||
void SendspinMediaPlayer::setup() {
|
||||
// Register for group updates to sync playback state
|
||||
this->parent_->add_group_update_callback([this](const sendspin::GroupUpdateObject &group_obj) {
|
||||
if (group_obj.playback_state.has_value()) {
|
||||
media_player::MediaPlayerState new_state;
|
||||
switch (group_obj.playback_state.value()) {
|
||||
case sendspin::SendspinPlaybackState::PLAYING:
|
||||
new_state = media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
break;
|
||||
case sendspin::SendspinPlaybackState::STOPPED:
|
||||
default:
|
||||
new_state = media_player::MEDIA_PLAYER_STATE_IDLE;
|
||||
break;
|
||||
}
|
||||
if (this->state != new_state) {
|
||||
this->state = new_state;
|
||||
this->publish_state();
|
||||
ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this->parent_->add_controller_state_callback([this](const sendspin::ServerStateControllerObject &state) {
|
||||
float new_volume = static_cast<float>(state.volume) / 100.0f;
|
||||
bool new_muted = state.muted;
|
||||
if ((new_volume != this->volume) || (new_muted != this->muted_)) {
|
||||
this->volume = new_volume;
|
||||
this->muted_ = new_muted;
|
||||
this->publish_state();
|
||||
}
|
||||
});
|
||||
|
||||
// Publish an initial state
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked by the media_player framework)
|
||||
media_player::MediaPlayerTraits SendspinMediaPlayer::get_traits() {
|
||||
auto traits = media_player::MediaPlayerTraits();
|
||||
|
||||
// By default, the base media player always enables these traits, but they are not actually supported by this media
|
||||
// player
|
||||
traits.clear_feature_flags(media_player::MediaPlayerEntityFeature::PLAY_MEDIA |
|
||||
media_player::MediaPlayerEntityFeature::BROWSE_MEDIA |
|
||||
media_player::MediaPlayerEntityFeature::MEDIA_ANNOUNCE);
|
||||
|
||||
traits.add_feature_flags(
|
||||
media_player::MediaPlayerEntityFeature::PLAY | media_player::MediaPlayerEntityFeature::PAUSE |
|
||||
media_player::MediaPlayerEntityFeature::STOP | media_player::MediaPlayerEntityFeature::VOLUME_STEP |
|
||||
media_player::MediaPlayerEntityFeature::VOLUME_SET | media_player::MediaPlayerEntityFeature::VOLUME_MUTE);
|
||||
|
||||
// NEXT_TRACK, PREVIOUS_TRACK, SHUFFLE_SET, and REPEAT_SET are intentionally not advertised: the ESPHome native API
|
||||
// does not implement the corresponding media player commands, so Home Assistant cannot actually send them even if
|
||||
// we expose the capability. They remain accessible via ESPHome YAML automations.
|
||||
|
||||
return traits;
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked by the media_player framework)
|
||||
void SendspinMediaPlayer::control(const media_player::MediaPlayerCall &call) {
|
||||
if (!this->is_ready()) {
|
||||
// Ignore any commands sent before the media player is setup
|
||||
return;
|
||||
}
|
||||
|
||||
auto volume = call.get_volume();
|
||||
if (volume.has_value()) {
|
||||
uint8_t new_volume = static_cast<uint8_t>(std::roundf(volume.value() * 100.0f));
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::VOLUME, new_volume, std::nullopt);
|
||||
}
|
||||
|
||||
auto command = call.get_command();
|
||||
if (!command.has_value()) {
|
||||
return;
|
||||
}
|
||||
switch (command.value()) {
|
||||
case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
|
||||
if (this->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING) {
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::PAUSE);
|
||||
} else {
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::PLAY);
|
||||
}
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::PLAY);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::PAUSE);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_STOP:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::STOP);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_REPEAT_OFF:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_OFF);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_REPEAT_ONE:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_ONE);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_REPEAT_ALL:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_ALL);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_SHUFFLE:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::SHUFFLE);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_UNSHUFFLE:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::UNSHUFFLE);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_NEXT:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::NEXT);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PREVIOUS:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::PREVIOUS);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP:
|
||||
this->parent_->send_client_command(
|
||||
sendspin::SendspinControllerCommand::VOLUME,
|
||||
static_cast<uint8_t>(std::roundf(std::min(1.0f, this->volume + this->volume_increment_) * 100.0f)),
|
||||
std::nullopt);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN:
|
||||
this->parent_->send_client_command(
|
||||
sendspin::SendspinControllerCommand::VOLUME,
|
||||
static_cast<uint8_t>(std::roundf(std::max(0.0f, this->volume - this->volume_increment_) * 100.0f)),
|
||||
std::nullopt);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_MUTE:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::MUTE, std::nullopt, true);
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_UNMUTE:
|
||||
this->parent_->send_client_command(sendspin::SendspinControllerCommand::MUTE, std::nullopt, false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SendspinMediaPlayer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Sendspin Media Player: volume_increment=%.2f", this->volume_increment_);
|
||||
}
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
#endif
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_MEDIA_PLAYER) && defined(USE_SENDSPIN_CONTROLLER)
|
||||
|
||||
#include "esphome/components/media_player/media_player.h"
|
||||
#include "esphome/components/sendspin/sendspin_hub.h"
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
class SendspinMediaPlayer : public SendspinChild, public media_player::MediaPlayer {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
// MediaPlayer implementations
|
||||
media_player::MediaPlayerTraits get_traits() override;
|
||||
|
||||
void set_volume_increment(float volume_increment) { this->volume_increment_ = volume_increment; }
|
||||
|
||||
bool is_muted() const override { return this->muted_; }
|
||||
|
||||
protected:
|
||||
// Receives commands from HA
|
||||
void control(const media_player::MediaPlayerCall &call) override;
|
||||
|
||||
float volume_increment_{0.05f};
|
||||
bool muted_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
#endif
|
||||
@@ -31,6 +31,11 @@ void SendspinHub::setup() {
|
||||
this->client_->set_network_provider(this);
|
||||
this->client_->set_persistence_provider(this);
|
||||
|
||||
#ifdef USE_SENDSPIN_CONTROLLER
|
||||
this->controller_role_ = &this->client_->add_controller();
|
||||
this->controller_role_->set_listener(this);
|
||||
#endif
|
||||
|
||||
if (!this->client_->start_server()) {
|
||||
ESP_LOGE(TAG, "Failed to start Sendspin server");
|
||||
this->mark_failed();
|
||||
@@ -138,6 +143,23 @@ std::optional<uint32_t> SendspinHub::load_last_server_hash() {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// --- Sendspin role specific methods/overrides ---
|
||||
|
||||
#ifdef USE_SENDSPIN_CONTROLLER
|
||||
// THREAD CONTEXT: Main loop (invoked from ESPHome actions / other components)
|
||||
void SendspinHub::send_client_command(sendspin::SendspinControllerCommand command, std::optional<uint8_t> volume,
|
||||
std::optional<bool> mute) {
|
||||
if (this->is_ready()) {
|
||||
this->controller_role_->send_command(command, volume, mute);
|
||||
}
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop())
|
||||
void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObject &state) {
|
||||
this->controller_state_callbacks_.call(state);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
#include <sendspin/config.h>
|
||||
#include <sendspin/types.h>
|
||||
|
||||
#ifdef USE_SENDSPIN_CONTROLLER
|
||||
#include <sendspin/controller_role.h>
|
||||
#endif
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -50,6 +54,9 @@ struct LastPlayedServerPref {
|
||||
/// (for services the library pulls; e.g., persistence, network readiness).
|
||||
/// - User -> library communication uses exposed functions on the client and role objects that the user calls.
|
||||
class SendspinHub final : public Component,
|
||||
#ifdef USE_SENDSPIN_CONTROLLER
|
||||
public sendspin::ControllerRoleListener,
|
||||
#endif
|
||||
public sendspin::SendspinClientListener,
|
||||
public sendspin::SendspinNetworkProvider,
|
||||
public sendspin::SendspinPersistenceProvider {
|
||||
@@ -94,6 +101,17 @@ class SendspinHub final : public Component,
|
||||
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
|
||||
|
||||
// --- Sendspin role specific methods ---
|
||||
|
||||
#ifdef USE_SENDSPIN_CONTROLLER
|
||||
void send_client_command(sendspin::SendspinControllerCommand command, std::optional<uint8_t> volume = std::nullopt,
|
||||
std::optional<bool> mute = std::nullopt);
|
||||
|
||||
template<typename F> void add_controller_state_callback(F &&callback) {
|
||||
this->controller_state_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
/// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info.
|
||||
sendspin::SendspinClientConfig build_client_config_();
|
||||
@@ -112,6 +130,19 @@ class SendspinHub final : public Component,
|
||||
bool save_last_server_hash(uint32_t hash) override;
|
||||
std::optional<uint32_t> load_last_server_hash() override;
|
||||
|
||||
// --- Sendspin role specific methods/overrides/member variables ---
|
||||
|
||||
#ifdef USE_SENDSPIN_CONTROLLER
|
||||
sendspin::ControllerRole *controller_role_{nullptr};
|
||||
|
||||
void on_controller_state(const sendspin::ServerStateControllerObject &state) override;
|
||||
|
||||
// Callback fan-out to child components; they filter as needed
|
||||
CallbackManager<void(const sendspin::ServerStateControllerObject &)> controller_state_callbacks_{};
|
||||
#endif
|
||||
|
||||
// --- Core member variables ---
|
||||
|
||||
ESPPreferenceObject last_played_server_pref_;
|
||||
|
||||
std::unique_ptr<sendspin::SendspinClient> client_;
|
||||
|
||||
@@ -472,24 +472,49 @@ void AsyncResponseStream::printf(const char *fmt, ...) {
|
||||
|
||||
#ifdef USE_WEBSERVER
|
||||
AsyncEventSource::~AsyncEventSource() {
|
||||
for (auto *ses : this->sessions_) {
|
||||
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
LockGuard guard{this->pending_mutex_};
|
||||
for (auto *vec : {&this->sessions_, &this->pending_sessions_}) {
|
||||
for (auto *ses : *vec) {
|
||||
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
|
||||
// Httpd task: set up the live httpd_req_t and park the session; main loop does the rest.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory,clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
auto *rsp = new AsyncEventSourceResponse(request, this, this->web_server_);
|
||||
if (this->on_connect_) {
|
||||
this->on_connect_(rsp);
|
||||
{
|
||||
LockGuard guard{this->pending_mutex_};
|
||||
this->pending_sessions_.push_back(rsp);
|
||||
this->has_pending_sessions_.store(true, std::memory_order_release);
|
||||
}
|
||||
this->sessions_.push_back(rsp);
|
||||
// Wake up WebServer::loop() to drain deferred event queues for this client.
|
||||
// Safe from httpd task context via the pending_enable_loop_ flag.
|
||||
this->web_server_->enable_loop_soon_any_context();
|
||||
}
|
||||
|
||||
bool AsyncEventSource::loop() {
|
||||
// Fast path: one atomic load per tick. Lock only on a real connect.
|
||||
if (this->has_pending_sessions_.load(std::memory_order_acquire)) {
|
||||
std::vector<AsyncEventSourceResponse *> incoming;
|
||||
{
|
||||
LockGuard guard{this->pending_mutex_};
|
||||
incoming.swap(this->pending_sessions_);
|
||||
this->has_pending_sessions_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
for (auto *rsp : incoming) {
|
||||
// Already disconnected? Drop it; skip on_connect_/prime on a dead session.
|
||||
if (rsp->fd_.load() == 0) {
|
||||
delete rsp; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
continue;
|
||||
}
|
||||
this->sessions_.push_back(rsp);
|
||||
if (this->on_connect_) {
|
||||
this->on_connect_(rsp);
|
||||
}
|
||||
rsp->prime_();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up dead sessions safely
|
||||
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
|
||||
// and the main loop handles the actual cleanup to avoid race conditions
|
||||
@@ -534,6 +559,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
||||
esphome::web_server_idf::AsyncEventSource *server,
|
||||
esphome::web_server::WebServer *ws)
|
||||
: server_(server), web_server_(ws), entities_iterator_(ws, server) {
|
||||
// Httpd task only. prime_() on the main loop handles event_buffer_ / iterator setup.
|
||||
httpd_req_t *req = *request;
|
||||
|
||||
httpd_resp_set_status(req, HTTPD_200);
|
||||
@@ -555,9 +581,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
||||
|
||||
// Use non-blocking send to prevent watchdog timeouts when TCP buffers are full
|
||||
httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send);
|
||||
}
|
||||
|
||||
// Configure reconnect timeout and send config
|
||||
// this should always go through since the tcp send buffer is empty on connect
|
||||
void AsyncEventSourceResponse::prime_() {
|
||||
auto *ws = this->web_server_;
|
||||
|
||||
// tcp send buffer is empty on connect, so these should always go through
|
||||
auto message = ws->get_config_json();
|
||||
this->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
|
||||
|
||||
@@ -578,12 +607,6 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
||||
#endif
|
||||
|
||||
this->entities_iterator_.begin(ws->include_internal_);
|
||||
|
||||
// just dump them all up-front and take advantage of the deferred queue
|
||||
// on second thought that takes too long, but leaving the commented code here for debug purposes
|
||||
// while(!this->entities_iterator_.completed()) {
|
||||
// this->entities_iterator_.advance();
|
||||
//}
|
||||
}
|
||||
|
||||
void AsyncEventSourceResponse::destroy(void *ptr) {
|
||||
|
||||
@@ -299,6 +299,9 @@ class AsyncEventSourceResponse {
|
||||
AsyncEventSourceResponse(const AsyncWebServerRequest *request, esphome::web_server_idf::AsyncEventSource *server,
|
||||
esphome::web_server::WebServer *ws);
|
||||
|
||||
// Main-loop only: sends initial ping/config/sorting_groups, starts entity iterator.
|
||||
void prime_();
|
||||
|
||||
void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator);
|
||||
void process_deferred_queue_();
|
||||
void process_buffer_();
|
||||
@@ -347,13 +350,16 @@ class AsyncEventSource : public AsyncWebHandler {
|
||||
size_t count() const { return this->sessions_.size(); }
|
||||
|
||||
protected:
|
||||
// Ordered to minimize padding on 32-bit: atomic<bool> last consumes trailing pad.
|
||||
std::string url_;
|
||||
// Use vector instead of set: SSE sessions are typically 1-5 connections (browsers, dashboards).
|
||||
// Linear search is faster than red-black tree overhead for this small dataset.
|
||||
// Only operations needed: add session, remove session, iterate sessions - no need for sorted order.
|
||||
// Main-loop only. Vector: SSE sessions are 1-5 connections, linear search beats set.
|
||||
std::vector<AsyncEventSourceResponse *> sessions_;
|
||||
// Httpd-task intake; guarded by pending_mutex_, gated by has_pending_sessions_.
|
||||
std::vector<AsyncEventSourceResponse *> pending_sessions_;
|
||||
Mutex pending_mutex_;
|
||||
connect_handler_t on_connect_{};
|
||||
esphome::web_server::WebServer *web_server_;
|
||||
std::atomic<bool> has_pending_sessions_{false};
|
||||
};
|
||||
#endif // USE_WEBSERVER
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from .const import (
|
||||
KEY_BOARD,
|
||||
KEY_BOOTLOADER,
|
||||
KEY_EXTRA_BUILD_FILES,
|
||||
KEY_KCONFIG,
|
||||
KEY_OVERLAY,
|
||||
KEY_PM_STATIC,
|
||||
KEY_PRJ_CONF,
|
||||
@@ -54,6 +55,7 @@ class ZephyrData(TypedDict):
|
||||
extra_build_files: dict[str, Path]
|
||||
pm_static: list[Section]
|
||||
user: dict[str, list[str]]
|
||||
kconfig: str
|
||||
|
||||
|
||||
def zephyr_set_core_data(config: ConfigType) -> None:
|
||||
@@ -65,6 +67,7 @@ def zephyr_set_core_data(config: ConfigType) -> None:
|
||||
extra_build_files={},
|
||||
pm_static=[],
|
||||
user={},
|
||||
kconfig="",
|
||||
)
|
||||
|
||||
|
||||
@@ -185,8 +188,12 @@ def zephyr_add_cdc_acm(config: ConfigType, id: int) -> None:
|
||||
)
|
||||
|
||||
|
||||
def zephyr_add_pm_static(section: Section):
|
||||
CORE.data[KEY_ZEPHYR][KEY_PM_STATIC].extend(section)
|
||||
def zephyr_add_kconfig(kconfig: str) -> None:
|
||||
zephyr_data()[KEY_KCONFIG] += textwrap.dedent(kconfig) + "\n"
|
||||
|
||||
|
||||
def zephyr_add_pm_static(sections: list[Section]) -> None:
|
||||
zephyr_data()[KEY_PM_STATIC].extend(sections)
|
||||
|
||||
|
||||
def zephyr_add_user(key, value):
|
||||
@@ -273,3 +280,18 @@ def copy_files():
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("zephyr/pm_static.yml"), pm_static
|
||||
)
|
||||
|
||||
kconfig = zephyr_data()[KEY_KCONFIG]
|
||||
if kconfig:
|
||||
kconfig = (
|
||||
textwrap.dedent(
|
||||
"""
|
||||
menu "Zephyr"
|
||||
source "Kconfig.zephyr"
|
||||
endmenu
|
||||
"""
|
||||
)
|
||||
+ "\n"
|
||||
+ kconfig
|
||||
)
|
||||
write_file_if_changed(CORE.relative_build_path("zephyr/Kconfig"), kconfig)
|
||||
|
||||
@@ -8,6 +8,7 @@ KEY_BOOTLOADER: Final = "bootloader"
|
||||
KEY_EXTRA_BUILD_FILES: Final = "extra_build_files"
|
||||
KEY_OVERLAY: Final = "overlay"
|
||||
KEY_PM_STATIC: Final = "pm_static"
|
||||
KEY_KCONFIG: Final = "kconfig"
|
||||
KEY_PRJ_CONF: Final = "prj_conf"
|
||||
KEY_ZEPHYR = "zephyr"
|
||||
KEY_BOARD: Final = "board"
|
||||
|
||||
@@ -32,6 +32,7 @@ from .const import (
|
||||
from .const_zephyr import (
|
||||
CONF_IEEE802154_VENDOR_OUI,
|
||||
CONF_MAX_EP_NUMBER,
|
||||
CONF_SLEEPY,
|
||||
CONF_ZIGBEE_ID,
|
||||
KEY_EP_NUMBER,
|
||||
)
|
||||
@@ -107,6 +108,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.requires_component("nrf52"),
|
||||
),
|
||||
cv.OnlyWith(CONF_SLEEPY, "nrf52", default=False): cv.All(
|
||||
cv.boolean,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
zigbee_require_vfs_select,
|
||||
|
||||
@@ -4,6 +4,7 @@ CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor"
|
||||
CONF_ZIGBEE_SENSOR = "zigbee_sensor"
|
||||
CONF_ZIGBEE_SWITCH = "zigbee_switch"
|
||||
CONF_ZIGBEE_NUMBER = "zigbee_number"
|
||||
CONF_SLEEPY = "sleepy"
|
||||
CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui"
|
||||
|
||||
# Keys for CORE.data storage
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include <zephyr/settings/settings.h>
|
||||
#include <zephyr/storage/flash_map.h>
|
||||
#include "esphome/core/hal.h"
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
#include "esphome/components/deep_sleep/deep_sleep_component.h"
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
#include <zboss_api.h>
|
||||
@@ -116,6 +119,12 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
|
||||
/* Set default response value. */
|
||||
p_device_cb_param->status = RET_OK;
|
||||
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
if (auto *ds = deep_sleep::global_deep_sleep.load()) {
|
||||
ds->wakeup();
|
||||
}
|
||||
#endif
|
||||
|
||||
// endpoints are enumerated from 1
|
||||
if (global_zigbee->callbacks_.size() >= endpoint) {
|
||||
const auto &cb = global_zigbee->callbacks_[endpoint - 1];
|
||||
@@ -181,9 +190,11 @@ void ZigbeeComponent::setup() {
|
||||
ESP_LOGE(TAG, "Cannot load settings, err: %d", err);
|
||||
return;
|
||||
}
|
||||
zigbee_configure_sleepy_behavior(this->sleepy_);
|
||||
zigbee_enable();
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_CONFIG
|
||||
static const char *role() {
|
||||
switch (zb_get_network_role()) {
|
||||
case ZB_NWK_DEVICE_TYPE_COORDINATOR:
|
||||
@@ -207,6 +218,7 @@ static const char *get_wipe_on_boot() {
|
||||
return "NO";
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
void ZigbeeComponent::dump_config() {
|
||||
char ieee_addr_buf[IEEE_ADDR_BUF_SIZE] = {0};
|
||||
@@ -222,6 +234,7 @@ void ZigbeeComponent::dump_config() {
|
||||
" Wipe on boot: %s\n"
|
||||
" Device is joined to the network: %s\n"
|
||||
" Sleep time: %us\n"
|
||||
" RX ON when idle: %s\n"
|
||||
" Current channel: %d\n"
|
||||
" Current page: %d\n"
|
||||
" Sleep threshold: %ums\n"
|
||||
@@ -230,9 +243,9 @@ void ZigbeeComponent::dump_config() {
|
||||
" Short addr: 0x%04X\n"
|
||||
" Long pan id: 0x%s\n"
|
||||
" Short pan id: 0x%04X",
|
||||
get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, zb_get_current_channel(),
|
||||
zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(),
|
||||
extended_pan_id_buf, zb_get_pan_id());
|
||||
get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, YESNO(zb_get_rx_on_when_idle()),
|
||||
zb_get_current_channel(), zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf,
|
||||
zb_get_short_address(), extended_pan_id_buf, zb_get_pan_id());
|
||||
dump_reporting_();
|
||||
}
|
||||
|
||||
@@ -302,6 +315,12 @@ void ZigbeeComponent::after_reporting_info(zb_zcl_configure_reporting_req_t *con
|
||||
|
||||
extern "C" {
|
||||
void zboss_signal_handler(zb_uint8_t param) { esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param); }
|
||||
void zb_osif_serial_put_bytes(const zb_uint8_t *buf, zb_short_t len) {
|
||||
(void) buf;
|
||||
(void) len;
|
||||
}
|
||||
void zb_osif_serial_flush() {}
|
||||
void zb_osif_serial_init() {}
|
||||
|
||||
// NOLINTBEGIN(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
|
||||
extern zb_ret_t __real_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_req_t *config_rep_req,
|
||||
|
||||
@@ -81,6 +81,7 @@ class ZigbeeComponent : public Component {
|
||||
Trigger<> *get_join_trigger() { return &this->join_trigger_; };
|
||||
void force_report();
|
||||
void loop() override;
|
||||
void set_sleepy(bool sleepy) { this->sleepy_ = sleepy; }
|
||||
|
||||
protected:
|
||||
static void zcl_device_cb(zb_bufid_t bufid);
|
||||
@@ -95,6 +96,7 @@ class ZigbeeComponent : public Component {
|
||||
bool force_report_{false};
|
||||
uint32_t sleep_time_{};
|
||||
uint32_t sleep_remainder_{};
|
||||
bool sleepy_{};
|
||||
};
|
||||
|
||||
class ZigbeeEntity {
|
||||
@@ -107,5 +109,7 @@ class ZigbeeEntity {
|
||||
ZigbeeComponent *parent_{nullptr};
|
||||
};
|
||||
|
||||
extern ZigbeeComponent *global_zigbee; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome::zigbee
|
||||
#endif
|
||||
|
||||
@@ -63,6 +63,7 @@ from .const import (
|
||||
)
|
||||
from .const_zephyr import (
|
||||
CONF_IEEE802154_VENDOR_OUI,
|
||||
CONF_SLEEPY,
|
||||
CONF_ZIGBEE_BINARY_SENSOR,
|
||||
CONF_ZIGBEE_ID,
|
||||
CONF_ZIGBEE_NUMBER,
|
||||
@@ -169,6 +170,11 @@ async def zephyr_to_code(config: ConfigType) -> None:
|
||||
zephyr_add_prj_conf("NET_IP_ADDR_CHECK", False)
|
||||
zephyr_add_prj_conf("NET_UDP", False)
|
||||
|
||||
# disable all extra to reduce power and save flash
|
||||
zephyr_add_prj_conf("ZIGBEE_HAVE_SERIAL", False)
|
||||
zephyr_add_prj_conf("ZBOSS_ERROR_PRINT_TO_LOG", False)
|
||||
zephyr_add_prj_conf("DK_LIBRARY", False)
|
||||
|
||||
cg.add_build_flag("-Wl,--wrap=zb_zcl_put_reporting_info_from_req")
|
||||
|
||||
if CONF_IEEE802154_VENDOR_OUI in config:
|
||||
@@ -200,6 +206,8 @@ async def zephyr_to_code(config: ConfigType) -> None:
|
||||
|
||||
CORE.add_job(_ctx_to_code, config)
|
||||
|
||||
cg.add(var.set_sleepy(config[CONF_SLEEPY]))
|
||||
|
||||
|
||||
async def _attr_to_code(config: ConfigType) -> None:
|
||||
# Create the basic attributes structure and attribute list
|
||||
|
||||
@@ -4,3 +4,9 @@ esphome:
|
||||
- deep_sleep.prevent
|
||||
- delay: 1s
|
||||
- deep_sleep.allow
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return false;'
|
||||
then:
|
||||
- deep_sleep.enter:
|
||||
sleep_duration: 60min
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
deep_sleep:
|
||||
run_duration: 10s
|
||||
sleep_duration: 50s
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
zigbee:
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "Temperature"
|
||||
id: temperature_sensor
|
||||
@@ -0,0 +1,8 @@
|
||||
# `sendspin.switch` action enables the controller role, so we use a standalone test
|
||||
packages:
|
||||
base: !include common.yaml
|
||||
|
||||
wifi:
|
||||
on_connect:
|
||||
then:
|
||||
- sendspin.switch:
|
||||
@@ -0,0 +1,5 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
media_player:
|
||||
- platform: sendspin
|
||||
id: media_player_id
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common-action.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common-media_player.yaml
|
||||
@@ -4,3 +4,4 @@ zigbee:
|
||||
wipe_on_boot: once
|
||||
power_source: battery
|
||||
ieee802154_vendor_oui: 0x231
|
||||
sleepy: true
|
||||
|
||||
Reference in New Issue
Block a user