From 9c79fa7430ac2139ff0a560bf9434a3e25558fc7 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 8 Oct 2026 13:36:12 -0400 Subject: [PATCH] [sendspin] Add the dynamic pairing code (#20349) --- esphome/components/sendspin/__init__.py | 46 +++++++++++++- esphome/components/sendspin/sendspin_hub.cpp | 33 ++++++++-- esphome/components/sendspin/sendspin_hub.h | 21 +++++++ .../sendspin/text_sensor/__init__.py | 43 ++++++++++--- .../text_sensor/sendspin_text_sensor.cpp | 15 ++++- .../text_sensor/sendspin_text_sensor.h | 13 +++- .../component_tests/sendspin/test_pairing.py | 20 ++++++- .../sendspin/test_text_sensor.py | 60 +++++++++++++++++++ .../sendspin/test_unpaired_access.py | 5 +- tests/components/sendspin/common-hub.yaml | 11 ++++ .../sendspin/common-text_sensor-pairing.yaml | 17 ++++++ .../sendspin/common-text_sensor.yaml | 4 ++ .../test-text_sensor-pairing.esp32-idf.yaml | 2 + 13 files changed, 271 insertions(+), 19 deletions(-) create mode 100644 tests/component_tests/sendspin/test_text_sensor.py create mode 100644 tests/components/sendspin/common-text_sensor-pairing.yaml create mode 100644 tests/components/sendspin/test-text_sensor-pairing.esp32-idf.yaml diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index fcc53dde4b..5e104fe58a 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -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 diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 12617e0081..33fdb78f10 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -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::spanpairing_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); } diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index 7c3708c374..d33269048e 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -146,6 +146,14 @@ class SendspinHub final : public Component, this->close_pairing_window_callbacks_.add(std::forward(callback)); } + template void add_on_display_pairing_code_callback(F &&callback) { + this->display_pairing_code_callbacks_.add(std::forward(callback)); + } + + template void add_on_clear_pairing_code_callback(F &&callback) { + this->clear_pairing_code_callbacks_.add(std::forward(callback)); + } + template void add_on_pairing_succeeded_callback(F &&callback) { this->pairing_succeeded_callbacks_.add(std::forward(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 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 open_pairing_window_callbacks_{}; LazyCallbackManager close_pairing_window_callbacks_{}; + LazyCallbackManager display_pairing_code_callbacks_{}; + LazyCallbackManager clear_pairing_code_callbacks_{}; LazyCallbackManager pairing_succeeded_callbacks_{}; LazyCallbackManager 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 diff --git a/esphome/components/sendspin/text_sensor/__init__.py b/esphome/components/sendspin/text_sensor/__init__.py index 87f6c9b936..24aa2d5734 100644 --- a/esphome/components/sendspin/text_sensor/__init__.py +++ b/esphome/components/sendspin/text_sensor/__init__.py @@ -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)) diff --git a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp index cda987f2e3..629396a681 100644 --- a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp +++ b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp @@ -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 +#endif #include @@ -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_ diff --git a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h index 9fe1c9381e..cc25ab93c7 100644 --- a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h +++ b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h @@ -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 +#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 diff --git a/tests/component_tests/sendspin/test_pairing.py b/tests/component_tests/sendspin/test_pairing.py index dcfce8cece..79612ccff5 100644 --- a/tests/component_tests/sendspin/test_pairing.py +++ b/tests/component_tests/sendspin/test_pairing.py @@ -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) diff --git a/tests/component_tests/sendspin/test_text_sensor.py b/tests/component_tests/sendspin/test_text_sensor.py new file mode 100644 index 0000000000..6dc073a59c --- /dev/null +++ b/tests/component_tests/sendspin/test_text_sensor.py @@ -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")) diff --git a/tests/component_tests/sendspin/test_unpaired_access.py b/tests/component_tests/sendspin/test_unpaired_access.py index a66de471ab..80bf5a2417 100644 --- a/tests/component_tests/sendspin/test_unpaired_access.py +++ b/tests/component_tests/sendspin/test_unpaired_access.py @@ -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): diff --git a/tests/components/sendspin/common-hub.yaml b/tests/components/sendspin/common-hub.yaml index 147227b2f1..d89d16856d 100644 --- a/tests/components/sendspin/common-hub.yaml +++ b/tests/components/sendspin/common-hub.yaml @@ -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: diff --git a/tests/components/sendspin/common-text_sensor-pairing.yaml b/tests/components/sendspin/common-text_sensor-pairing.yaml new file mode 100644 index 0000000000..c2d92eb453 --- /dev/null +++ b/tests/components/sendspin/common-text_sensor-pairing.yaml @@ -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 diff --git a/tests/components/sendspin/common-text_sensor.yaml b/tests/components/sendspin/common-text_sensor.yaml index 23111e8d37..087bbdb6b0 100644 --- a/tests/components/sendspin/common-text_sensor.yaml +++ b/tests/components/sendspin/common-text_sensor.yaml @@ -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 diff --git a/tests/components/sendspin/test-text_sensor-pairing.esp32-idf.yaml b/tests/components/sendspin/test-text_sensor-pairing.esp32-idf.yaml new file mode 100644 index 0000000000..2336f87740 --- /dev/null +++ b/tests/components/sendspin/test-text_sensor-pairing.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + sendspin: !include common-text_sensor-pairing.yaml