mirror of
https://github.com/esphome/esphome.git
synced 2026-10-09 12:26:02 +00:00
[sendspin] Add an unpaired access switch (#20350)
This commit is contained in:
@@ -5,7 +5,7 @@ from typing import Any
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, mdns, network, psram, socket, wifi
|
||||
from esphome.components.const import CONF_MANUFACTURER
|
||||
from esphome.components.const import CONF_ENABLED, CONF_MANUFACTURER
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
@@ -175,6 +175,7 @@ class SendspinConfiguration:
|
||||
player_support: bool = False
|
||||
visualizer_support: bool = False
|
||||
pairing_code_display_support: bool = False
|
||||
switch_types: set[str] = field(default_factory=set)
|
||||
|
||||
artwork_preferences: list[ConfigType] = field(default_factory=list)
|
||||
player_config: ConfigType | None = None
|
||||
@@ -216,6 +217,11 @@ def request_pairing_code_display_support() -> None:
|
||||
_get_data().pairing_code_display_support = True
|
||||
|
||||
|
||||
def request_switch(switch_type: str) -> None:
|
||||
"""Mark that a sendspin switch of this type drives the matching hub setting."""
|
||||
_get_data().switch_types.add(switch_type)
|
||||
|
||||
|
||||
def register_artwork_preference(config: ConfigType) -> int:
|
||||
"""Register an artwork slot preference and return the slot it was given.
|
||||
|
||||
@@ -275,6 +281,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_STATIC_PAIRING_CODE): cv.sensitive(
|
||||
_validate_static_pairing_code
|
||||
),
|
||||
# No default: the unpaired access switch rejects this key when it is set.
|
||||
cv.Optional(CONF_UNPAIRED_ACCESS): cv.boolean,
|
||||
cv.Optional(CONF_ON_OPEN_PAIRING_WINDOW): automation.validate_automation(
|
||||
{}
|
||||
@@ -322,8 +329,8 @@ 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 or a dynamic pairing code), so no "
|
||||
"server can play on this device",
|
||||
"'%s' is off but there is no pairing method (%s or a dynamic pairing code), so no "
|
||||
"new server can pair with this device",
|
||||
CONF_UNPAIRED_ACCESS,
|
||||
CONF_STATIC_PAIRING_CODE,
|
||||
)
|
||||
@@ -400,8 +407,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
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))
|
||||
# The client starts once both are set: here, or by the switch that drives the setting.
|
||||
switch_types = _get_data().switch_types
|
||||
if CONF_ENABLED not in switch_types:
|
||||
cg.add(var.set_enabled(True))
|
||||
if CONF_UNPAIRED_ACCESS not in switch_types:
|
||||
cg.add(var.set_unpaired_access_enabled(config.get(CONF_UNPAIRED_ACCESS, True)))
|
||||
|
||||
if _offers_dynamic_pairing_code(config):
|
||||
cg.add(var.set_pairing_code_display_supported(True))
|
||||
|
||||
@@ -154,16 +154,15 @@ void SendspinHub::setup() {
|
||||
this->client_->add_player(this->player_config_).set_listener(this->player_listener_);
|
||||
#endif
|
||||
|
||||
this->client_->set_unpaired_access_enabled(this->default_unpaired_access_);
|
||||
|
||||
#ifndef USE_SENDSPIN_SWITCH
|
||||
this->enabled_ = true;
|
||||
#endif
|
||||
// Set before setup() by codegen; an unpaired access switch sets it later, from its own setup().
|
||||
if (this->unpaired_access_.has_value()) {
|
||||
this->client_->set_unpaired_access_enabled(*this->unpaired_access_);
|
||||
}
|
||||
}
|
||||
|
||||
void SendspinHub::loop() {
|
||||
if (this->enabled_.has_value() && this->enabled_.value() != this->client_->is_started() &&
|
||||
!this->status_has_error()) {
|
||||
if (this->enabled_.has_value() && this->unpaired_access_.has_value() &&
|
||||
this->enabled_.value() != this->client_->is_started() && !this->status_has_error()) {
|
||||
if (!this->enabled_.value()) {
|
||||
this->client_->stop();
|
||||
} else if (!this->client_->start()) {
|
||||
@@ -222,7 +221,7 @@ void SendspinHub::dump_config() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
||||
// THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components)
|
||||
void SendspinHub::set_enabled(bool enabled) {
|
||||
if (this->status_has_error()) {
|
||||
ESP_LOGE(TAG, "Cannot %s: Sendspin failed to start, reboot to retry",
|
||||
@@ -232,6 +231,15 @@ void SendspinHub::set_enabled(bool enabled) {
|
||||
this->enabled_ = enabled;
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components)
|
||||
void SendspinHub::set_unpaired_access_enabled(bool enabled) {
|
||||
this->unpaired_access_ = enabled;
|
||||
// Before setup() there is no client yet; setup() applies the stored value.
|
||||
if (this->client_ != nullptr) {
|
||||
this->client_->set_unpaired_access_enabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
// THREAD CONTEXT: Main loop
|
||||
void SendspinHub::update_mdns_service_() {
|
||||
|
||||
@@ -168,12 +168,13 @@ class SendspinHub final : public Component,
|
||||
/// stop.
|
||||
///
|
||||
/// Applied from the hub's loop(). Stopping blocks until the client is fully stopped; the roles' clear callbacks
|
||||
/// fire from inside that call. With a sendspin switch configured the client stays stopped until the switch has
|
||||
/// called this once. Must be called from the main loop thread.
|
||||
/// fire from inside that call. The client stays stopped until this has been called once, by codegen or by the
|
||||
/// enabled switch. 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; }
|
||||
/// Turns unpaired (Sentinel) access on or off. The client waits for this call, by codegen or by the unpaired
|
||||
/// access switch, before its first start. May be called before setup(). Main loop only.
|
||||
void set_unpaired_access_enabled(bool enabled);
|
||||
|
||||
/// @brief Returns whether the Sendspin client is running.
|
||||
bool is_client_running() const { return this->client_ != nullptr && this->client_->is_started(); }
|
||||
@@ -382,14 +383,14 @@ class SendspinHub final : public Component,
|
||||
|
||||
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
|
||||
bool mdns_advertised_{false}; // Last state requested from mdns
|
||||
#endif
|
||||
|
||||
// Requested client state, applied from loop(). Empty until the switch restores its state.
|
||||
// Requested client state, applied from loop(). The client does not start until both are set.
|
||||
std::optional<bool> enabled_;
|
||||
std::optional<bool> unpaired_access_;
|
||||
|
||||
// Device information sent in the `client/hello` message. Defaults apply when neither the
|
||||
// sendspin configuration nor the project information supplies a value.
|
||||
|
||||
@@ -1,35 +1,101 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
from esphome.components.const import CONF_ENABLED
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG
|
||||
from esphome.const import CONF_PLATFORM, CONF_SWITCH, CONF_TYPE, ENTITY_CATEGORY_CONFIG
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_SENDSPIN_ID, SendspinHub, sendspin_ns
|
||||
from .. import (
|
||||
CONF_SENDSPIN_ID,
|
||||
CONF_STATIC_PAIRING_CODE,
|
||||
CONF_UNPAIRED_ACCESS,
|
||||
DOMAIN,
|
||||
SendspinHub,
|
||||
_has_pairing_method,
|
||||
request_switch,
|
||||
sendspin_ns,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
DEPENDENCIES = ["sendspin"]
|
||||
|
||||
SendspinSwitch = sendspin_ns.class_("SendspinSwitch", switch.Switch, cg.Component)
|
||||
SendspinEnabledSwitch = sendspin_ns.class_(
|
||||
"SendspinEnabledSwitch", switch.Switch, cg.Component
|
||||
)
|
||||
SendspinUnpairedAccessSwitch = sendspin_ns.class_(
|
||||
"SendspinUnpairedAccessSwitch", switch.Switch, cg.Component
|
||||
)
|
||||
|
||||
|
||||
def _switch_schema(class_: cg.MockObjClass) -> cv.Schema:
|
||||
return (
|
||||
switch.switch_schema(
|
||||
class_,
|
||||
block_inverted=True,
|
||||
default_restore_mode="RESTORE_DEFAULT_ON",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
.extend({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)})
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def _request_switch(config: ConfigType) -> ConfigType:
|
||||
"""Tell the hub to wait for this switch before the client's first start."""
|
||||
request_switch(config[CONF_TYPE])
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.typed_schema(
|
||||
{
|
||||
CONF_ENABLED: switch.switch_schema(
|
||||
SendspinSwitch,
|
||||
block_inverted=True,
|
||||
default_restore_mode="RESTORE_DEFAULT_ON",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
.extend({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)})
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
CONF_ENABLED: _switch_schema(SendspinEnabledSwitch),
|
||||
CONF_UNPAIRED_ACCESS: _switch_schema(SendspinUnpairedAccessSwitch),
|
||||
},
|
||||
key=CONF_TYPE,
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
_request_switch,
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
full_config = fv.full_config.get()
|
||||
switch_type = config[CONF_TYPE]
|
||||
same_type = [
|
||||
conf
|
||||
for conf in full_config.get(CONF_SWITCH, [])
|
||||
if conf.get(CONF_PLATFORM) == DOMAIN and conf.get(CONF_TYPE) == switch_type
|
||||
]
|
||||
# Two switches of one type would each drive the same hub setting.
|
||||
if len(same_type) > 1:
|
||||
raise cv.Invalid(f"Only one sendspin '{switch_type}' switch is allowed")
|
||||
if switch_type != CONF_UNPAIRED_ACCESS:
|
||||
return config
|
||||
hub_config = full_config.get(DOMAIN, {})
|
||||
if CONF_UNPAIRED_ACCESS in hub_config:
|
||||
raise cv.Invalid(
|
||||
f"'{DOMAIN}: {CONF_UNPAIRED_ACCESS}' cannot be set together with an "
|
||||
f"{CONF_UNPAIRED_ACCESS} switch; set the switch's restore_mode instead"
|
||||
)
|
||||
if not _has_pairing_method(hub_config):
|
||||
_LOGGER.warning(
|
||||
"The %s switch has no pairing method (%s or a dynamic pairing code), so "
|
||||
"while it is off no new server can pair with this device",
|
||||
CONF_UNPAIRED_ACCESS,
|
||||
CONF_STATIC_PAIRING_CODE,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -1,26 +1,49 @@
|
||||
#include "sendspin_switch.h"
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_SWITCH)
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
static const char *const TAG = "sendspin.switch";
|
||||
|
||||
void SendspinSwitch::setup() {
|
||||
// --- SendspinEnabledSwitch ---
|
||||
|
||||
void SendspinEnabledSwitch::setup() {
|
||||
// The hub waits for this request, so a restore mode without a state still has to answer.
|
||||
this->control(this->get_initial_state_with_restore_mode().value_or(true));
|
||||
}
|
||||
|
||||
void SendspinSwitch::dump_config() { LOG_SWITCH("", "Sendspin Switch", this); }
|
||||
void SendspinEnabledSwitch::dump_config() { LOG_SWITCH("", "Sendspin Enabled Switch", this); }
|
||||
|
||||
// THREAD CONTEXT: Main loop
|
||||
void SendspinSwitch::write_state(bool state) {
|
||||
void SendspinEnabledSwitch::write_state(bool state) {
|
||||
this->parent_->set_enabled(state);
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
// --- SendspinUnpairedAccessSwitch ---
|
||||
|
||||
void SendspinUnpairedAccessSwitch::setup() {
|
||||
// The hub holds the client's first start for this, so a restore mode without a state still has to answer.
|
||||
this->control(this->get_initial_state_with_restore_mode().value_or(true));
|
||||
}
|
||||
|
||||
void SendspinUnpairedAccessSwitch::dump_config() { LOG_SWITCH("", "Sendspin Unpaired Access Switch", this); }
|
||||
|
||||
// THREAD CONTEXT: Main loop
|
||||
void SendspinUnpairedAccessSwitch::write_state(bool state) {
|
||||
const bool turning_off = !state && this->has_state() && this->state;
|
||||
this->parent_->set_unpaired_access_enabled(state);
|
||||
this->publish_state(state);
|
||||
// Write the change to flash now: losing it in a power cut would reopen unpaired access.
|
||||
if (turning_off) {
|
||||
global_preferences->sync();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
#endif
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#if defined(USE_ESP_IDF) && defined(USE_SENDSPIN_SWITCH)
|
||||
|
||||
#include "esphome/components/sendspin/sendspin_hub.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
@@ -10,7 +10,19 @@
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
/// @brief Switch that starts and stops the Sendspin client through the hub (see SendspinHub::set_enabled()).
|
||||
class SendspinSwitch final : public switch_::Switch, public SendspinChild {
|
||||
class SendspinEnabledSwitch final : public switch_::Switch, public SendspinChild {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
/// @brief Switch that turns unpaired (Sentinel) access on and off through the hub (see
|
||||
/// SendspinHub::set_unpaired_access_enabled()). The library does not persist the setting, so the switch's restore
|
||||
/// mode does.
|
||||
class SendspinUnpairedAccessSwitch final : public switch_::Switch, public SendspinChild {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -21,4 +33,4 @@ class SendspinSwitch final : public switch_::Switch, public SendspinChild {
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user