[sendspin] Add an unpaired access switch (#20350)

This commit is contained in:
Kevin Ahrendt
2026-10-08 08:13:31 -10:00
committed by GitHub
parent 9c79fa7430
commit 920295527f
9 changed files with 274 additions and 43 deletions
@@ -1,24 +1,42 @@
"""Validation tests for the hub's unpaired access policy."""
"""Validation tests for the unpaired access policy.
The hub's `unpaired_access` key and an `unpaired_access` switch both set the
policy, so the switch platform rejects a config that has both.
"""
import logging
import pytest
from esphome import config_validation as cv
from esphome.components.sendspin import (
CONF_UNPAIRED_ACCESS,
CONFIG_SCHEMA as HUB_CONFIG_SCHEMA,
DOMAIN,
FINAL_VALIDATE_SCHEMA as HUB_FINAL_VALIDATE_SCHEMA,
_get_data,
request_pairing_code_display_support,
)
from esphome.components.sendspin.switch import (
CONFIG_SCHEMA as SWITCH_CONFIG_SCHEMA,
FINAL_VALIDATE_SCHEMA as SWITCH_FINAL_VALIDATE_SCHEMA,
)
from esphome.const import PlatformFramework
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
HUB_ID = "sendspin_hub_id"
NO_PAIRING_WARNING = "nothing lets a server pair"
NO_PAIRING_WARNING = "no new server can pair with this device"
def _switch_config(switch_type: str) -> ConfigType:
return SWITCH_CONFIG_SCHEMA(
{"name": "Sendspin Switch", "type": switch_type, "sendspin_id": HUB_ID}
)
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."""
"""With no default in the schema, the switch can tell whether the key was set."""
set_core_config(PlatformFramework.ESP32_IDF)
assert CONF_UNPAIRED_ACCESS not in HUB_CONFIG_SCHEMA({"id": HUB_ID})
@@ -36,7 +54,7 @@ def test_hub_key_accepted(set_core_config: SetCoreConfigCallable, value: bool) -
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."""
"""With unpaired access off and no way to pair, no new server could pair."""
set_core_config(PlatformFramework.ESP32_IDF)
config = HUB_CONFIG_SCHEMA({"id": HUB_ID, CONF_UNPAIRED_ACCESS: False})
@@ -76,3 +94,92 @@ def test_on_does_not_warn(
HUB_FINAL_VALIDATE_SCHEMA(config)
assert NO_PAIRING_WARNING not in caplog.text
def test_switch_with_hub_key_rejected(set_core_config: SetCoreConfigCallable) -> None:
"""Both would set the policy, and the hub's value would be silently ignored."""
set_core_config(
PlatformFramework.ESP32_IDF,
full_config={DOMAIN: {"id": HUB_ID, CONF_UNPAIRED_ACCESS: False}},
)
config = _switch_config(CONF_UNPAIRED_ACCESS)
with pytest.raises(cv.Invalid, match="set the switch's restore_mode instead"):
SWITCH_FINAL_VALIDATE_SCHEMA(config)
def test_switch_without_hub_key_accepted(
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(PlatformFramework.ESP32_IDF, full_config={DOMAIN: {"id": HUB_ID}})
config = _switch_config(CONF_UNPAIRED_ACCESS)
assert SWITCH_FINAL_VALIDATE_SCHEMA(config) is config
def test_switch_without_pairing_method_warns(
set_core_config: SetCoreConfigCallable, caplog: pytest.LogCaptureFixture
) -> None:
"""Turning the switch off would leave no way for a new server to pair."""
set_core_config(PlatformFramework.ESP32_IDF, full_config={DOMAIN: {"id": HUB_ID}})
config = _switch_config(CONF_UNPAIRED_ACCESS)
with caplog.at_level(logging.WARNING):
SWITCH_FINAL_VALIDATE_SCHEMA(config)
assert NO_PAIRING_WARNING in caplog.text
def test_switch_with_pairing_method_does_not_warn(
set_core_config: SetCoreConfigCallable, caplog: pytest.LogCaptureFixture
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
full_config={DOMAIN: {"id": HUB_ID, "static_pairing_code": "01234567"}},
)
config = _switch_config(CONF_UNPAIRED_ACCESS)
with caplog.at_level(logging.WARNING):
SWITCH_FINAL_VALIDATE_SCHEMA(config)
assert NO_PAIRING_WARNING not in caplog.text
def test_enabled_switch_ignores_hub_key(set_core_config: SetCoreConfigCallable) -> None:
"""Only the unpaired access switch competes with the hub key."""
set_core_config(
PlatformFramework.ESP32_IDF,
full_config={DOMAIN: {"id": HUB_ID, CONF_UNPAIRED_ACCESS: False}},
)
config = _switch_config("enabled")
assert SWITCH_FINAL_VALIDATE_SCHEMA(config) is config
@pytest.mark.parametrize("switch_type", ["enabled", CONF_UNPAIRED_ACCESS])
def test_duplicate_switch_type_rejected(
set_core_config: SetCoreConfigCallable, switch_type: str
) -> None:
"""Two switches of one type would fight over the same hub setting."""
entry = {"platform": DOMAIN, "type": switch_type}
set_core_config(
PlatformFramework.ESP32_IDF,
full_config={DOMAIN: {"id": HUB_ID}, "switch": [entry, dict(entry)]},
)
config = _switch_config(switch_type)
with pytest.raises(cv.Invalid, match="Only one sendspin"):
SWITCH_FINAL_VALIDATE_SCHEMA(config)
@pytest.mark.parametrize("switch_type", ["enabled", CONF_UNPAIRED_ACCESS])
def test_switch_requests_its_type(
set_core_config: SetCoreConfigCallable, switch_type: str
) -> None:
"""The hub skips the codegen value for a setting a switch drives, so the client
waits for that switch to restore before its first start."""
set_core_config(PlatformFramework.ESP32_IDF)
_switch_config(switch_type)
assert _get_data().switch_types == {switch_type}
@@ -10,5 +10,5 @@ wifi:
sendspin:
id: sendspin_hub_id
static_pairing_code: "01234567"
# Covers the hub's unpaired access option.
# Covers the hub's unpaired access option, which is only valid without an unpaired access switch.
unpaired_access: false
@@ -5,3 +5,6 @@ switch:
- platform: sendspin
name: "Sendspin Enabled"
type: enabled
- platform: sendspin
name: "Sendspin Unpaired Access"
type: unpaired_access