mirror of
https://github.com/esphome/esphome.git
synced 2026-10-08 20:06:03 +00:00
[sendspin] Add the dynamic pairing code (#20349)
This commit is contained in:
@@ -54,6 +54,8 @@ 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_DISPLAY_PAIRING_CODE = "on_display_pairing_code"
|
||||
CONF_ON_CLEAR_PAIRING_CODE = "on_clear_pairing_code"
|
||||
CONF_ON_PAIRING_SUCCEEDED = "on_pairing_succeeded"
|
||||
CONF_ON_PAIRING_FAILED = "on_pairing_failed"
|
||||
|
||||
@@ -144,6 +146,14 @@ _CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CLOSE_PAIRING_WINDOW, "add_on_close_pairing_window_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DISPLAY_PAIRING_CODE,
|
||||
"add_on_display_pairing_code_callback",
|
||||
[(cg.std_string, "code")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CLEAR_PAIRING_CODE, "add_on_clear_pairing_code_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PAIRING_SUCCEEDED,
|
||||
"add_on_pairing_succeeded_callback",
|
||||
@@ -164,6 +174,7 @@ class SendspinConfiguration:
|
||||
metadata_support: bool = False
|
||||
player_support: bool = False
|
||||
visualizer_support: bool = False
|
||||
pairing_code_display_support: bool = False
|
||||
|
||||
artwork_preferences: list[ConfigType] = field(default_factory=list)
|
||||
player_config: ConfigType | None = None
|
||||
@@ -200,6 +211,11 @@ def request_visualizer_support() -> None:
|
||||
_get_data().visualizer_support = True
|
||||
|
||||
|
||||
def request_pairing_code_display_support() -> None:
|
||||
"""Mark that the device can emit a dynamic pairing code (e.g. a pairing_code text sensor)."""
|
||||
_get_data().pairing_code_display_support = True
|
||||
|
||||
|
||||
def register_artwork_preference(config: ConfigType) -> int:
|
||||
"""Register an artwork slot preference and return the slot it was given.
|
||||
|
||||
@@ -266,6 +282,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ON_CLOSE_PAIRING_WINDOW): automation.validate_automation(
|
||||
{}
|
||||
),
|
||||
cv.Optional(CONF_ON_DISPLAY_PAIRING_CODE): automation.validate_automation(
|
||||
{}
|
||||
),
|
||||
cv.Optional(CONF_ON_CLEAR_PAIRING_CODE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_PAIRING_SUCCEEDED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_PAIRING_FAILED): automation.validate_automation({}),
|
||||
}
|
||||
@@ -277,16 +297,33 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _offers_dynamic_pairing_code(config: ConfigType) -> bool:
|
||||
"""Whether the device can show a dynamic pairing code, so dynamic_pairing_code is advertised."""
|
||||
return bool(
|
||||
config.get(CONF_ON_DISPLAY_PAIRING_CODE)
|
||||
or _get_data().pairing_code_display_support
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
return CONF_STATIC_PAIRING_CODE in config or _offers_dynamic_pairing_code(config)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
dynamic_code = _offers_dynamic_pairing_code(config)
|
||||
# The protocol allows only one pairing code method.
|
||||
if dynamic_code and CONF_STATIC_PAIRING_CODE in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_STATIC_PAIRING_CODE}' cannot be used with a dynamic pairing code "
|
||||
f"({CONF_ON_DISPLAY_PAIRING_CODE} or a pairing_code text sensor), since only "
|
||||
"one pairing code method can be offered",
|
||||
path=[CONF_STATIC_PAIRING_CODE],
|
||||
)
|
||||
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",
|
||||
"'%s' is off but nothing lets a server pair (%s or a dynamic pairing code), so no "
|
||||
"server can play on this device",
|
||||
CONF_UNPAIRED_ACCESS,
|
||||
CONF_STATIC_PAIRING_CODE,
|
||||
)
|
||||
@@ -366,6 +403,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
if (unpaired_access := config.get(CONF_UNPAIRED_ACCESS)) is not None:
|
||||
cg.add(var.set_default_unpaired_access(unpaired_access))
|
||||
|
||||
if _offers_dynamic_pairing_code(config):
|
||||
cg.add(var.set_pairing_code_display_supported(True))
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
# sendspin-cpp library
|
||||
|
||||
@@ -202,12 +202,12 @@ 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",
|
||||
" Unpaired access: %s\n"
|
||||
" Pairing code method: %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()));
|
||||
YESNO(this->client_ != nullptr && this->client_->is_unpaired_access_enabled()),
|
||||
this->pairing_code_method_());
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
// Slot indices come from the order the image platform entries were declared, so the log is the
|
||||
@@ -311,6 +311,17 @@ const char *SendspinHub::get_mac_address_into_buffer(std::span<char, MAC_ADDRESS
|
||||
return buf.data();
|
||||
}
|
||||
|
||||
// Validation rejects a static code together with the dynamic code, so at most one is set.
|
||||
const char *SendspinHub::pairing_code_method_() const {
|
||||
if (this->pairing_code_display_supported_) {
|
||||
return LOG_STR_LITERAL("dynamic");
|
||||
}
|
||||
if (this->static_pairing_code_ != nullptr) {
|
||||
return LOG_STR_LITERAL("static");
|
||||
}
|
||||
return LOG_STR_LITERAL("none");
|
||||
}
|
||||
|
||||
const char *SendspinHub::get_product_name_() const {
|
||||
return this->model_ != nullptr ? this->model_ : App.get_name().c_str();
|
||||
}
|
||||
@@ -328,6 +339,13 @@ sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
|
||||
config.protocol_task_psram_stack = this->task_stack_in_psram_;
|
||||
config.max_pairing_records = SENDSPIN_RECORD_SLOTS;
|
||||
|
||||
// The dynamic code needs a channel and a format. Only digits, since automations get the bare string and could not
|
||||
// tell a QR code token apart.
|
||||
if (this->pairing_code_display_supported_) {
|
||||
config.pairing_code_out_channels = {sendspin::SendspinPairingCodeChannel::DISPLAY};
|
||||
config.pairing_code_formats = {sendspin::SendspinPairingCodeFormat::DIGITS};
|
||||
}
|
||||
|
||||
if (this->static_pairing_code_ != nullptr) {
|
||||
config.static_pairing_code_locations = {"operator"};
|
||||
config.static_pairing_code = this->static_pairing_code_;
|
||||
@@ -365,6 +383,13 @@ void SendspinHub::on_open_pairing_window() { this->open_pairing_window_callbacks
|
||||
|
||||
void SendspinHub::on_close_pairing_window() { this->close_pairing_window_callbacks_.call(); }
|
||||
|
||||
// Only digits are offered, and the library refuses an activation in any other format.
|
||||
void SendspinHub::on_display_pairing_code(const std::string &code, sendspin::SendspinPairingCodeFormat /*format*/) {
|
||||
this->display_pairing_code_callbacks_.call(code);
|
||||
}
|
||||
|
||||
void SendspinHub::on_clear_pairing_code() { this->clear_pairing_code_callbacks_.call(); }
|
||||
|
||||
void SendspinHub::on_pairing_succeeded(const std::string &server_id) {
|
||||
this->pairing_succeeded_callbacks_.call(server_id);
|
||||
}
|
||||
|
||||
@@ -146,6 +146,14 @@ class SendspinHub final : public Component,
|
||||
this->close_pairing_window_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_display_pairing_code_callback(F &&callback) {
|
||||
this->display_pairing_code_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
template<typename F> void add_on_clear_pairing_code_callback(F &&callback) {
|
||||
this->clear_pairing_code_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));
|
||||
}
|
||||
@@ -186,6 +194,10 @@ class SendspinHub final : public Component,
|
||||
/// The static pairing code, used on every boot.
|
||||
void set_static_pairing_code(const char *code) { this->static_pairing_code_ = code; }
|
||||
|
||||
/// Set when on_display_pairing_code or a pairing_code text sensor is configured, so the device offers the dynamic
|
||||
/// pairing code.
|
||||
void set_pairing_code_display_supported(bool supported) { this->pairing_code_display_supported_ = supported; }
|
||||
|
||||
// --- Sendspin role specific methods ---
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
@@ -258,6 +270,8 @@ class SendspinHub final : public Component,
|
||||
/// @brief Returns the product name reported to the server: the configured model, or the device name.
|
||||
const char *get_product_name_() const;
|
||||
|
||||
const char *pairing_code_method_() const;
|
||||
|
||||
/// @brief Writes the active network interface's MAC, in lowercase, into @p buf and returns its data pointer.
|
||||
/// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi).
|
||||
static const char *get_mac_address_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
@@ -278,6 +292,10 @@ class SendspinHub final : public Component,
|
||||
|
||||
void on_close_pairing_window() override;
|
||||
|
||||
void on_display_pairing_code(const std::string &code, sendspin::SendspinPairingCodeFormat format) override;
|
||||
|
||||
void on_clear_pairing_code() override;
|
||||
|
||||
void on_pairing_succeeded(const std::string &server_id) override;
|
||||
|
||||
void on_pairing_failed(const std::string &server_id, sendspin::SendspinPairAbortReason reason) override;
|
||||
@@ -357,10 +375,13 @@ class SendspinHub final : public Component,
|
||||
// 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 &)> display_pairing_code_callbacks_{};
|
||||
LazyCallbackManager<void()> clear_pairing_code_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 pairing_code_display_supported_{false};
|
||||
bool default_unpaired_access_{true};
|
||||
bool task_stack_in_psram_{false};
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
|
||||
@@ -4,16 +4,29 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TYPE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_SENDSPIN_ID, SendspinHub, request_metadata_support, sendspin_ns
|
||||
from .. import (
|
||||
CONF_SENDSPIN_ID,
|
||||
SendspinHub,
|
||||
request_metadata_support,
|
||||
request_pairing_code_display_support,
|
||||
sendspin_ns,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
DEPENDENCIES = ["sendspin"]
|
||||
|
||||
CONF_PAIRING_CODE = "pairing_code"
|
||||
|
||||
SendspinTextSensor = sendspin_ns.class_(
|
||||
"SendspinTextSensor",
|
||||
text_sensor.TextSensor,
|
||||
cg.Component,
|
||||
)
|
||||
SendspinPairingCodeTextSensor = sendspin_ns.class_(
|
||||
"SendspinPairingCodeTextSensor",
|
||||
text_sensor.TextSensor,
|
||||
cg.Component,
|
||||
)
|
||||
|
||||
SendspinTextMetadataTypes = sendspin_ns.enum("SendspinTextMetadataTypes", is_class=True)
|
||||
SENDSPIN_TEXT_METADATA_TYPES = {
|
||||
@@ -26,18 +39,29 @@ SENDSPIN_TEXT_METADATA_TYPES = {
|
||||
|
||||
def _request_roles(config: ConfigType) -> ConfigType:
|
||||
"""Request the necessary Sendspin roles for the text sensor."""
|
||||
request_metadata_support()
|
||||
if config[CONF_TYPE] == CONF_PAIRING_CODE:
|
||||
# A display surface for the dynamic code, so advertise dynamic_pairing_code.
|
||||
request_pairing_code_display_support()
|
||||
else:
|
||||
request_metadata_support()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
_HUB_ID_SCHEMA = cv.Schema({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)})
|
||||
_METADATA_SCHEMA = text_sensor.text_sensor_schema(SendspinTextSensor).extend(
|
||||
_HUB_ID_SCHEMA
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
text_sensor.text_sensor_schema().extend(
|
||||
cv.typed_schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SendspinTextSensor),
|
||||
cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub),
|
||||
cv.Required(CONF_TYPE): cv.enum(SENDSPIN_TEXT_METADATA_TYPES),
|
||||
}
|
||||
**dict.fromkeys(SENDSPIN_TEXT_METADATA_TYPES, _METADATA_SCHEMA),
|
||||
CONF_PAIRING_CODE: text_sensor.text_sensor_schema(
|
||||
SendspinPairingCodeTextSensor
|
||||
).extend(_HUB_ID_SCHEMA),
|
||||
},
|
||||
key=CONF_TYPE,
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
_request_roles,
|
||||
@@ -50,4 +74,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_parented(var, config[CONF_SENDSPIN_ID])
|
||||
await text_sensor.register_text_sensor(var, config)
|
||||
|
||||
cg.add(var.set_metadata_type(config[CONF_TYPE]))
|
||||
if (
|
||||
metadata_type := SENDSPIN_TEXT_METADATA_TYPES.get(config[CONF_TYPE])
|
||||
) is not None:
|
||||
cg.add(var.set_metadata_type(metadata_type))
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "sendspin_text_sensor.h"
|
||||
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_METADATA) && defined(USE_TEXT_SENSOR)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_TEXT_SENSOR)
|
||||
|
||||
#ifdef USE_SENDSPIN_METADATA
|
||||
#include <sendspin/metadata_role.h>
|
||||
#endif
|
||||
|
||||
#include <string>
|
||||
|
||||
@@ -10,6 +12,7 @@ namespace esphome::sendspin_ {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "sendspin.text_sensor");
|
||||
|
||||
#ifdef USE_SENDSPIN_METADATA
|
||||
void SendspinTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Sendspin", this); }
|
||||
|
||||
// A field is nullopt when the server has not provided it or has explicitly cleared it. Both mean there is nothing to
|
||||
@@ -50,6 +53,16 @@ void SendspinTextSensor::publish_if_changed_(const char *value) {
|
||||
this->publish_state(value);
|
||||
}
|
||||
}
|
||||
#endif // USE_SENDSPIN_METADATA
|
||||
|
||||
void SendspinPairingCodeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Pairing Code", this); }
|
||||
|
||||
// THREAD CONTEXT: Main loop. The registered callbacks also fire on the main loop (SendspinHub dispatches them from
|
||||
// its loop()).
|
||||
void SendspinPairingCodeTextSensor::setup() {
|
||||
this->parent_->add_on_display_pairing_code_callback([this](const std::string &code) { this->publish_state(code); });
|
||||
this->parent_->add_on_clear_pairing_code_callback([this]() { this->publish_state(""); });
|
||||
}
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_METADATA) && defined(USE_TEXT_SENSOR)
|
||||
#if defined(USE_ESP_IDF) && defined(USE_TEXT_SENSOR)
|
||||
|
||||
#include "esphome/components/sendspin/sendspin_hub.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
#ifdef USE_SENDSPIN_METADATA
|
||||
#include <sendspin/metadata_role.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
#ifdef USE_SENDSPIN_METADATA
|
||||
|
||||
enum class SendspinTextMetadataTypes {
|
||||
TITLE,
|
||||
ARTIST,
|
||||
@@ -31,6 +35,13 @@ class SendspinTextSensor final : public SendspinChild, public text_sensor::TextS
|
||||
|
||||
SendspinTextMetadataTypes metadata_type_;
|
||||
};
|
||||
#endif // USE_SENDSPIN_METADATA
|
||||
|
||||
class SendspinPairingCodeTextSensor final : public SendspinChild, public text_sensor::TextSensor {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
#endif
|
||||
|
||||
@@ -9,7 +9,12 @@ 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.components.sendspin import (
|
||||
CONF_STATIC_PAIRING_CODE,
|
||||
CONFIG_SCHEMA,
|
||||
FINAL_VALIDATE_SCHEMA,
|
||||
request_pairing_code_display_support,
|
||||
)
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
@@ -75,3 +80,16 @@ def test_unquoted_static_pairing_code_rejected(
|
||||
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA(_hub_config(static_pairing_code=1234567))
|
||||
|
||||
|
||||
def test_static_code_with_dynamic_code_rejected(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""Only one pairing code method can be offered, so a static code next to the dynamic
|
||||
code would be silently ignored."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
request_pairing_code_display_support()
|
||||
config = CONFIG_SCHEMA(_hub_config(static_pairing_code="01234567"))
|
||||
|
||||
with pytest.raises(cv.Invalid, match="cannot be used with a dynamic pairing code"):
|
||||
FINAL_VALIDATE_SCHEMA(config)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Validation tests for the sendspin text sensor platform.
|
||||
|
||||
These cover behavior a compile test cannot observe: which roles a sensor type requests.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.sendspin import _get_data
|
||||
from esphome.components.sendspin.text_sensor import (
|
||||
CONF_PAIRING_CODE,
|
||||
CONFIG_SCHEMA,
|
||||
SENDSPIN_TEXT_METADATA_TYPES,
|
||||
)
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def _sensor_config(**overrides: Any) -> ConfigType:
|
||||
"""Build a minimal valid text sensor config, allowing field overrides."""
|
||||
config: ConfigType = {"name": "Sendspin Text", "type": "title"}
|
||||
config.update(overrides)
|
||||
return config
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sensor_type", sorted(SENDSPIN_TEXT_METADATA_TYPES))
|
||||
def test_metadata_types_request_metadata_role(
|
||||
set_core_config: SetCoreConfigCallable, sensor_type: str
|
||||
) -> None:
|
||||
"""Metadata sensors need the metadata role, and say nothing about pairing codes."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
CONFIG_SCHEMA(_sensor_config(type=sensor_type))
|
||||
|
||||
assert _get_data().metadata_support is True
|
||||
assert _get_data().pairing_code_display_support is False
|
||||
|
||||
|
||||
def test_pairing_code_requests_code_display(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A pairing_code sensor is a way to show the dynamic code, so it alone makes the
|
||||
hub offer it. It needs no role, so a pairing-only config builds without metadata."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
CONFIG_SCHEMA(_sensor_config(type=CONF_PAIRING_CODE))
|
||||
|
||||
assert _get_data().pairing_code_display_support is True
|
||||
assert _get_data().metadata_support is False
|
||||
|
||||
|
||||
def test_unknown_type_rejected(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""A misspelled type must fail rather than fall through to a default."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA(_sensor_config(type="pairing_secret"))
|
||||
@@ -8,6 +8,7 @@ from esphome.components.sendspin import (
|
||||
CONF_UNPAIRED_ACCESS,
|
||||
CONFIG_SCHEMA as HUB_CONFIG_SCHEMA,
|
||||
FINAL_VALIDATE_SCHEMA as HUB_FINAL_VALIDATE_SCHEMA,
|
||||
request_pairing_code_display_support,
|
||||
)
|
||||
from esphome.const import PlatformFramework
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
@@ -45,7 +46,7 @@ def test_off_without_pairing_method_warns(
|
||||
assert NO_PAIRING_WARNING in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["static_pairing_code"])
|
||||
@pytest.mark.parametrize("method", ["static_pairing_code", "pairing_code"])
|
||||
def test_off_with_pairing_method_does_not_warn(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -55,6 +56,8 @@ def test_off_with_pairing_method_does_not_warn(
|
||||
hub_config = {"id": HUB_ID, CONF_UNPAIRED_ACCESS: False}
|
||||
if method == "static_pairing_code":
|
||||
hub_config["static_pairing_code"] = "01234567"
|
||||
else:
|
||||
request_pairing_code_display_support()
|
||||
config = HUB_CONFIG_SCHEMA(hub_config)
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
|
||||
@@ -7,6 +7,9 @@ sendspin:
|
||||
manufacturer: Test Manufacturer
|
||||
model: Test Model
|
||||
firmware_version: 1.2.3
|
||||
# No static_pairing_code here: the on_display_pairing_code automation below makes
|
||||
# this a dynamic-pairing-code device, and validation rejects both together.
|
||||
# common-pairing.yaml covers the static-code build.
|
||||
on_open_pairing_window:
|
||||
then:
|
||||
- logger.log: "Pairing window requested"
|
||||
@@ -16,6 +19,14 @@ sendspin:
|
||||
then:
|
||||
- logger.log: "Pairing window prompt dismissed"
|
||||
- sendspin.cancel_pairing_window:
|
||||
on_display_pairing_code:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Pairing code: %s"
|
||||
args: ["code.c_str()"]
|
||||
on_clear_pairing_code:
|
||||
then:
|
||||
- logger.log: "Pairing code cleared"
|
||||
on_pairing_succeeded:
|
||||
then:
|
||||
- logger.log:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# The pairing code text sensor on its own, with no metadata text sensor to pull in the
|
||||
# metadata role. This is the build where USE_SENDSPIN_METADATA is undefined while the text
|
||||
# sensor platform is still compiled, which is guarded separately in sendspin_text_sensor.{h,cpp}.
|
||||
#
|
||||
# Deliberately does not include common.yaml: that file pulls in the shared hub package, and
|
||||
# the point here is the narrower configuration. sendspin depends on network, so the interface
|
||||
# has to be declared locally instead.
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
sendspin:
|
||||
id: sendspin_hub_id
|
||||
|
||||
text_sensor:
|
||||
- platform: sendspin
|
||||
name: "Pairing Code"
|
||||
type: pairing_code
|
||||
@@ -14,3 +14,7 @@ text_sensor:
|
||||
- platform: sendspin
|
||||
name: "Album Artist"
|
||||
type: album_artist
|
||||
- platform: sendspin
|
||||
name: "Pairing Code"
|
||||
type: pairing_code
|
||||
entity_category: diagnostic
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
sendspin: !include common-text_sensor-pairing.yaml
|
||||
Reference in New Issue
Block a user