mirror of
https://github.com/esphome/esphome.git
synced 2026-10-08 20:06:03 +00:00
[sendspin] Add the static pairing code, pairing window and unpaired access option (#20348)
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
@@ -25,6 +27,8 @@ from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# mdns for autodiscovery
|
||||
AUTO_LOAD = ["mdns"]
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
@@ -46,6 +50,31 @@ CONF_FIXED_DELAY = "fixed_delay"
|
||||
CONF_DECODE_MEMORY = "decode_memory"
|
||||
CONF_CODECS = "codecs"
|
||||
|
||||
CONF_STATIC_PAIRING_CODE = "static_pairing_code"
|
||||
CONF_UNPAIRED_ACCESS = "unpaired_access"
|
||||
CONF_ON_OPEN_PAIRING_WINDOW = "on_open_pairing_window"
|
||||
CONF_ON_CLOSE_PAIRING_WINDOW = "on_close_pairing_window"
|
||||
CONF_ON_PAIRING_SUCCEEDED = "on_pairing_succeeded"
|
||||
CONF_ON_PAIRING_FAILED = "on_pairing_failed"
|
||||
|
||||
# A static pairing code is exactly 8 decimal digits.
|
||||
STATIC_PAIRING_CODE_DIGITS = 8
|
||||
|
||||
|
||||
def _validate_static_pairing_code(value: Any) -> str:
|
||||
# string_strict so leading zeros survive and `!secret` works.
|
||||
value = cv.string_strict(value)
|
||||
if len(value) != STATIC_PAIRING_CODE_DIGITS or not (
|
||||
value.isascii() and value.isdigit()
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"{CONF_STATIC_PAIRING_CODE} must be exactly "
|
||||
f"{STATIC_PAIRING_CODE_DIGITS} decimal digits "
|
||||
'(quote the value so leading zeros are preserved, e.g. "01234567")'
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
# Matches ARTWORK_MAX_SLOTS in sendspin-cpp.
|
||||
MAX_ARTWORK_SLOTS = 4
|
||||
|
||||
@@ -108,6 +137,26 @@ SendspinHub = sendspin_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_OPEN_PAIRING_WINDOW, "add_on_open_pairing_window_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CLOSE_PAIRING_WINDOW, "add_on_close_pairing_window_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PAIRING_SUCCEEDED,
|
||||
"add_on_pairing_succeeded_callback",
|
||||
[(cg.std_string, "server_id")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PAIRING_FAILED,
|
||||
"add_on_pairing_failed_callback",
|
||||
[(cg.std_string, "server_id"), (cg.StringRef, "reason")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SendspinConfiguration:
|
||||
artwork_support: bool = False
|
||||
@@ -207,6 +256,18 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_MANUFACTURER): DEVICE_INFO_STRING,
|
||||
cv.Optional(CONF_MODEL): DEVICE_INFO_STRING,
|
||||
cv.Optional(CONF_FIRMWARE_VERSION): DEVICE_INFO_STRING,
|
||||
cv.Optional(CONF_STATIC_PAIRING_CODE): cv.sensitive(
|
||||
_validate_static_pairing_code
|
||||
),
|
||||
cv.Optional(CONF_UNPAIRED_ACCESS): cv.boolean,
|
||||
cv.Optional(CONF_ON_OPEN_PAIRING_WINDOW): automation.validate_automation(
|
||||
{}
|
||||
),
|
||||
cv.Optional(CONF_ON_CLOSE_PAIRING_WINDOW): automation.validate_automation(
|
||||
{}
|
||||
),
|
||||
cv.Optional(CONF_ON_PAIRING_SUCCEEDED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_PAIRING_FAILED): automation.validate_automation({}),
|
||||
}
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
@@ -216,21 +277,42 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _has_pairing_method(config: ConfigType) -> bool:
|
||||
"""Whether the config gives a server any way to pair with the device."""
|
||||
return CONF_STATIC_PAIRING_CODE in config
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
if not config.get(CONF_UNPAIRED_ACCESS, True) and not _has_pairing_method(config):
|
||||
_LOGGER.warning(
|
||||
"'%s' is off but nothing lets a server pair (%s), so no server can play on this "
|
||||
"device",
|
||||
CONF_UNPAIRED_ACCESS,
|
||||
CONF_STATIC_PAIRING_CODE,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def _request_controller_role(config: ConfigType) -> ConfigType:
|
||||
"""Request the controller role for the sendspin.switch action."""
|
||||
request_controller_support()
|
||||
return config
|
||||
|
||||
|
||||
# Selects the hub. sendspin.switch adds the controller role it needs; the pairing window actions need no role.
|
||||
SENDSPIN_HUB_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(SendspinHub),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
SENDSPIN_SIMPLE_ACTION_SCHEMA = cv.All(
|
||||
automation.maybe_simple_id(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(SendspinHub),
|
||||
}
|
||||
)
|
||||
),
|
||||
_request_controller_role,
|
||||
SENDSPIN_HUB_ACTION_SCHEMA, _request_controller_role
|
||||
)
|
||||
|
||||
|
||||
@@ -241,6 +323,19 @@ automation.register_apply_action(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"sendspin.confirm_pairing_window",
|
||||
SENDSPIN_HUB_ACTION_SCHEMA,
|
||||
automation.ApplyCall("confirm_pairing_window()"),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"sendspin.cancel_pairing_window",
|
||||
SENDSPIN_HUB_ACTION_SCHEMA,
|
||||
automation.ApplyCall("cancel_pairing_window()"),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -265,6 +360,14 @@ async def to_code(config: ConfigType) -> None:
|
||||
if value:
|
||||
cg.add(setter(value))
|
||||
|
||||
if (code := config.get(CONF_STATIC_PAIRING_CODE)) is not None:
|
||||
cg.add(var.set_static_pairing_code(code))
|
||||
|
||||
if (unpaired_access := config.get(CONF_UNPAIRED_ACCESS)) is not None:
|
||||
cg.add(var.set_default_unpaired_access(unpaired_access))
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
# sendspin-cpp library
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.9.3")
|
||||
# esp_websocket_client links esp_tls even for ws:// connections.
|
||||
|
||||
@@ -26,6 +26,26 @@ static const char *const TAG = "sendspin.hub";
|
||||
|
||||
namespace {
|
||||
|
||||
// The reason passed to on_pairing_failed automations, in the protocol's own spelling.
|
||||
StringRef pair_abort_reason_to_string(sendspin::SendspinPairAbortReason reason) {
|
||||
using sendspin::SendspinPairAbortReason;
|
||||
switch (reason) {
|
||||
case SendspinPairAbortReason::ATTEMPT_TIMEOUT:
|
||||
return StringRef::from_lit("attempt_timeout");
|
||||
case SendspinPairAbortReason::CONCURRENT_ATTEMPT:
|
||||
return StringRef::from_lit("concurrent_attempt");
|
||||
case SendspinPairAbortReason::METHOD_NOT_SUPPORTED:
|
||||
return StringRef::from_lit("method_not_supported");
|
||||
case SendspinPairAbortReason::PAIRING_CODE_MISMATCH:
|
||||
return StringRef::from_lit("pairing_code_mismatch");
|
||||
case SendspinPairAbortReason::USER_CANCELLED:
|
||||
return StringRef::from_lit("user_cancelled");
|
||||
case SendspinPairAbortReason::UNKNOWN:
|
||||
break;
|
||||
}
|
||||
return StringRef::from_lit("unknown");
|
||||
}
|
||||
|
||||
// Zeroes through a volatile pointer so dead-store elimination cannot drop it.
|
||||
void secure_wipe(void *data, size_t len) {
|
||||
volatile auto *p = static_cast<volatile uint8_t *>(data);
|
||||
@@ -134,8 +154,7 @@ void SendspinHub::setup() {
|
||||
this->client_->add_player(this->player_config_).set_listener(this->player_listener_);
|
||||
#endif
|
||||
|
||||
// Any server may play without pairing, as before rc1.
|
||||
this->client_->set_unpaired_access_enabled(true);
|
||||
this->client_->set_unpaired_access_enabled(this->default_unpaired_access_);
|
||||
|
||||
#ifndef USE_SENDSPIN_SWITCH
|
||||
this->enabled_ = true;
|
||||
@@ -183,9 +202,11 @@ void SendspinHub::dump_config() {
|
||||
" Model: %s\n"
|
||||
" Firmware version: %s\n"
|
||||
" Task stack in PSRAM: %s\n"
|
||||
" Static pairing code: %s\n"
|
||||
" Unpaired access: %s",
|
||||
client_id, get_mac_address_into_buffer(mac_buf), this->manufacturer_, this->get_product_name_(),
|
||||
this->firmware_version_, YESNO(this->task_stack_in_psram_),
|
||||
YESNO(this->static_pairing_code_ != nullptr),
|
||||
YESNO(this->client_ != nullptr && this->client_->is_unpaired_access_enabled()));
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
@@ -254,6 +275,20 @@ void SendspinHub::leave_group() {
|
||||
}
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from the sendspin.confirm_pairing_window action)
|
||||
void SendspinHub::confirm_pairing_window() {
|
||||
if (this->is_client_running()) {
|
||||
this->client_->confirm_pairing_window();
|
||||
}
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from the sendspin.cancel_pairing_window action)
|
||||
void SendspinHub::cancel_pairing_window() {
|
||||
if (this->is_client_running()) {
|
||||
this->client_->cancel_pairing_window();
|
||||
}
|
||||
}
|
||||
|
||||
const char *SendspinHub::get_mac_address_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
||||
// The server matches this MAC against the L2 source MAC of the device's multicast traffic.
|
||||
// ESP-IDF derives the ethernet MAC as base+3 by default on ESP32-S3, so we cannot use the
|
||||
@@ -293,6 +328,11 @@ sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
|
||||
config.protocol_task_psram_stack = this->task_stack_in_psram_;
|
||||
config.max_pairing_records = SENDSPIN_RECORD_SLOTS;
|
||||
|
||||
if (this->static_pairing_code_ != nullptr) {
|
||||
config.static_pairing_code_locations = {"operator"};
|
||||
config.static_pairing_code = this->static_pairing_code_;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -321,6 +361,18 @@ void SendspinHub::on_release_high_performance() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void SendspinHub::on_open_pairing_window() { this->open_pairing_window_callbacks_.call(); }
|
||||
|
||||
void SendspinHub::on_close_pairing_window() { this->close_pairing_window_callbacks_.call(); }
|
||||
|
||||
void SendspinHub::on_pairing_succeeded(const std::string &server_id) {
|
||||
this->pairing_succeeded_callbacks_.call(server_id);
|
||||
}
|
||||
|
||||
void SendspinHub::on_pairing_failed(const std::string &server_id, sendspin::SendspinPairAbortReason reason) {
|
||||
this->pairing_failed_callbacks_.call(server_id, pair_abort_reason_to_string(reason));
|
||||
}
|
||||
|
||||
// --- SendspinNetworkProvider override ---
|
||||
|
||||
// THREAD CONTEXT: Main loop (polled by start() and client_->loop())
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/core/version.h"
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
@@ -120,12 +121,39 @@ class SendspinHub final : public Component,
|
||||
/// No-op if the hub's client is not running. Must be called from the main loop thread.
|
||||
void leave_group();
|
||||
|
||||
/// @brief Confirms a pairing attempt on the device. With no attempt waiting, it opens the pairing window for the
|
||||
/// next one.
|
||||
///
|
||||
/// No-op if the hub's client is not running. Must be called from the main loop thread.
|
||||
void confirm_pairing_window();
|
||||
|
||||
/// @brief Closes an open pairing window, so a waiting pairing attempt is not confirmed.
|
||||
///
|
||||
/// No-op if the hub's client is not running. Must be called from the main loop thread.
|
||||
void cancel_pairing_window();
|
||||
|
||||
// --- Configuration setters (called from codegen) ---
|
||||
|
||||
template<typename F> void add_group_update_callback(F &&callback) {
|
||||
this->group_update_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_open_pairing_window_callback(F &&callback) {
|
||||
this->open_pairing_window_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_close_pairing_window_callback(F &&callback) {
|
||||
this->close_pairing_window_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_pairing_succeeded_callback(F &&callback) {
|
||||
this->pairing_succeeded_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_pairing_failed_callback(F &&callback) {
|
||||
this->pairing_failed_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
|
||||
|
||||
/// @brief Requests the Sendspin client, including the server, the roles and the mDNS advertisement, to start or
|
||||
@@ -136,6 +164,9 @@ class SendspinHub final : public Component,
|
||||
/// called this once. Must be called from the main loop thread.
|
||||
void set_enabled(bool enabled);
|
||||
|
||||
/// Turns unpaired (Sentinel) access on or off from setup(); see SendspinClient::set_unpaired_access_enabled().
|
||||
void set_default_unpaired_access(bool enabled) { this->default_unpaired_access_ = enabled; }
|
||||
|
||||
/// @brief Returns whether the Sendspin client is running.
|
||||
bool is_client_running() const { return this->client_ != nullptr && this->client_->is_started(); }
|
||||
|
||||
@@ -152,6 +183,9 @@ class SendspinHub final : public Component,
|
||||
void set_mdns(mdns::MDNSComponent *mdns) { this->mdns_ = mdns; }
|
||||
#endif
|
||||
|
||||
/// The static pairing code, used on every boot.
|
||||
void set_static_pairing_code(const char *code) { this->static_pairing_code_ = code; }
|
||||
|
||||
// --- Sendspin role specific methods ---
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
@@ -240,6 +274,14 @@ class SendspinHub final : public Component,
|
||||
|
||||
void on_release_high_performance() override;
|
||||
|
||||
void on_open_pairing_window() override;
|
||||
|
||||
void on_close_pairing_window() override;
|
||||
|
||||
void on_pairing_succeeded(const std::string &server_id) override;
|
||||
|
||||
void on_pairing_failed(const std::string &server_id, sendspin::SendspinPairAbortReason reason) override;
|
||||
|
||||
// --- SendspinNetworkProvider override ---
|
||||
bool is_network_ready() override;
|
||||
|
||||
@@ -312,6 +354,14 @@ class SendspinHub final : public Component,
|
||||
// Callback fan-out to child components
|
||||
CallbackManager<void(const sendspin::GroupUpdateObject &)> group_update_callbacks_{};
|
||||
|
||||
// Lazy: each pairing callback is fed by an optional YAML surface.
|
||||
LazyCallbackManager<void()> open_pairing_window_callbacks_{};
|
||||
LazyCallbackManager<void()> close_pairing_window_callbacks_{};
|
||||
LazyCallbackManager<void(const std::string &)> pairing_succeeded_callbacks_{};
|
||||
LazyCallbackManager<void(const std::string &, StringRef)> pairing_failed_callbacks_{};
|
||||
|
||||
const char *static_pairing_code_{nullptr}; // Codegen string literal, or nullptr when not configured
|
||||
bool default_unpaired_access_{true};
|
||||
bool task_stack_in_psram_{false};
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
bool mdns_advertised_{false}; // Last state requested from mdns
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Validation tests for the sendspin hub's pairing options.
|
||||
|
||||
These cover the rejection branches, which a compile test cannot reach: a
|
||||
`test*.yaml` can only assert that a configuration is accepted.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.sendspin import CONF_STATIC_PAIRING_CODE, CONFIG_SCHEMA
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def _hub_config(**overrides: Any) -> ConfigType:
|
||||
"""Build a minimal valid hub config, allowing field overrides."""
|
||||
config: ConfigType = {"id": "sendspin_hub_id"}
|
||||
config.update(overrides)
|
||||
return config
|
||||
|
||||
|
||||
def test_minimal_config_is_accepted(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""The baseline the rejection tests vary is itself valid."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
config = CONFIG_SCHEMA(_hub_config())
|
||||
|
||||
assert CONF_STATIC_PAIRING_CODE not in config
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", ["01234567", "00000000", "99999999"])
|
||||
def test_static_pairing_code_accepted(
|
||||
set_core_config: SetCoreConfigCallable, code: str
|
||||
) -> None:
|
||||
"""Eight decimal digits, leading zeros included, pass through unchanged."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
assert (
|
||||
CONFIG_SCHEMA(_hub_config(static_pairing_code=code))[CONF_STATIC_PAIRING_CODE]
|
||||
== code
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
"0123456", # too short
|
||||
"012345678", # too long
|
||||
"0123456a", # not all decimal digits
|
||||
"0123 567", # whitespace is not a digit
|
||||
"01234567", # full-width digits are not ASCII
|
||||
"",
|
||||
],
|
||||
)
|
||||
def test_static_pairing_code_rejected(
|
||||
set_core_config: SetCoreConfigCallable, code: str
|
||||
) -> None:
|
||||
"""Anything that is not exactly eight decimal digits is refused here, since the
|
||||
library would otherwise refuse to start the client."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="exactly 8 decimal digits"):
|
||||
CONFIG_SCHEMA(_hub_config(static_pairing_code=code))
|
||||
|
||||
|
||||
def test_unquoted_static_pairing_code_rejected(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""An unquoted YAML code arrives as an int, having already lost its leading
|
||||
zeros, so it is refused rather than silently pairing with the wrong value."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA(_hub_config(static_pairing_code=1234567))
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Validation tests for the hub's unpaired access policy."""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.sendspin import (
|
||||
CONF_UNPAIRED_ACCESS,
|
||||
CONFIG_SCHEMA as HUB_CONFIG_SCHEMA,
|
||||
FINAL_VALIDATE_SCHEMA as HUB_FINAL_VALIDATE_SCHEMA,
|
||||
)
|
||||
from esphome.const import PlatformFramework
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
HUB_ID = "sendspin_hub_id"
|
||||
NO_PAIRING_WARNING = "nothing lets a server pair"
|
||||
|
||||
|
||||
def test_hub_key_absent_by_default(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""Codegen leaves the hub's built-in default of on in place when the key is not set."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
assert CONF_UNPAIRED_ACCESS not in HUB_CONFIG_SCHEMA({"id": HUB_ID})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [True, False])
|
||||
def test_hub_key_accepted(set_core_config: SetCoreConfigCallable, value: bool) -> None:
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
config = HUB_CONFIG_SCHEMA({"id": HUB_ID, CONF_UNPAIRED_ACCESS: value})
|
||||
|
||||
assert config[CONF_UNPAIRED_ACCESS] is value
|
||||
|
||||
|
||||
def test_off_without_pairing_method_warns(
|
||||
set_core_config: SetCoreConfigCallable, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""With unpaired access off and no way to pair, no server could ever play."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
config = HUB_CONFIG_SCHEMA({"id": HUB_ID, CONF_UNPAIRED_ACCESS: False})
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
HUB_FINAL_VALIDATE_SCHEMA(config)
|
||||
|
||||
assert NO_PAIRING_WARNING in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["static_pairing_code"])
|
||||
def test_off_with_pairing_method_does_not_warn(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
method: str,
|
||||
) -> None:
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
hub_config = {"id": HUB_ID, CONF_UNPAIRED_ACCESS: False}
|
||||
if method == "static_pairing_code":
|
||||
hub_config["static_pairing_code"] = "01234567"
|
||||
config = HUB_CONFIG_SCHEMA(hub_config)
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
HUB_FINAL_VALIDATE_SCHEMA(config)
|
||||
|
||||
assert NO_PAIRING_WARNING not in caplog.text
|
||||
|
||||
|
||||
def test_on_does_not_warn(
|
||||
set_core_config: SetCoreConfigCallable, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
config = HUB_CONFIG_SCHEMA({"id": HUB_ID})
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
HUB_FINAL_VALIDATE_SCHEMA(config)
|
||||
|
||||
assert NO_PAIRING_WARNING not in caplog.text
|
||||
@@ -7,3 +7,27 @@ sendspin:
|
||||
manufacturer: Test Manufacturer
|
||||
model: Test Model
|
||||
firmware_version: 1.2.3
|
||||
on_open_pairing_window:
|
||||
then:
|
||||
- logger.log: "Pairing window requested"
|
||||
- sendspin.confirm_pairing_window:
|
||||
# Calling cancel_pairing_window here only compiles the action; a real config cancels from a user input.
|
||||
on_close_pairing_window:
|
||||
then:
|
||||
- logger.log: "Pairing window prompt dismissed"
|
||||
- sendspin.cancel_pairing_window:
|
||||
on_pairing_succeeded:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Paired with server %s"
|
||||
args: ["server_id.c_str()"]
|
||||
on_pairing_failed:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Pairing with server %s failed: %s"
|
||||
args: ["server_id.c_str()", "reason.c_str()"]
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return reason == "user_cancelled";'
|
||||
then:
|
||||
- logger.log: "Pairing was cancelled"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# The hub on its own, with no other sendspin platforms, to build the pairing options.
|
||||
#
|
||||
# Deliberately does not include common.yaml: that file pulls in the shared hub package, and
|
||||
# the point here is a separate hub configuration. sendspin depends on network, so the interface
|
||||
# has to be declared locally instead.
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
# Also the static-pairing-code build.
|
||||
sendspin:
|
||||
id: sendspin_hub_id
|
||||
static_pairing_code: "01234567"
|
||||
# Covers the hub's unpaired access option.
|
||||
unpaired_access: false
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
sendspin: !include common-pairing.yaml
|
||||
Reference in New Issue
Block a user