[sendspin] Add the static pairing code, pairing window and unpaired access option (#20348)

This commit is contained in:
Kevin Ahrendt
2026-10-08 07:19:14 -10:00
committed by GitHub
parent b1c145dd5f
commit 78bf74eafb
8 changed files with 407 additions and 10 deletions
@@ -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
+24
View File
@@ -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