mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-04-13 20:14:19 +00:00
Compare commits
1 Commits
2026.04.0
...
feature-to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8a7fd4ef1 |
@@ -22,6 +22,7 @@ from ..const import (
|
||||
ATTR_DEBUG_BLOCK,
|
||||
ATTR_DETECT_BLOCKING_IO,
|
||||
ATTR_DIAGNOSTICS,
|
||||
ATTR_FEATURE_FLAGS,
|
||||
ATTR_HEALTHY,
|
||||
ATTR_ICON,
|
||||
ATTR_IP_ADDRESS,
|
||||
@@ -41,6 +42,7 @@ from ..const import (
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
ATTR_WAIT_BOOT,
|
||||
ExperimentalFeature,
|
||||
LogLevel,
|
||||
UpdateChannel,
|
||||
)
|
||||
@@ -70,6 +72,9 @@ SCHEMA_OPTIONS = vol.Schema(
|
||||
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
||||
vol.Optional(ATTR_DETECT_BLOCKING_IO): vol.Coerce(DetectBlockingIO),
|
||||
vol.Optional(ATTR_COUNTRY): str,
|
||||
vol.Optional(ATTR_FEATURE_FLAGS): vol.Schema(
|
||||
{vol.Coerce(ExperimentalFeature): vol.Boolean()}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -104,6 +109,10 @@ class APISupervisor(CoreSysAttributes):
|
||||
ATTR_AUTO_UPDATE: self.sys_updater.auto_update,
|
||||
ATTR_DETECT_BLOCKING_IO: BlockBusterManager.is_enabled(),
|
||||
ATTR_COUNTRY: self.sys_config.country,
|
||||
ATTR_FEATURE_FLAGS: {
|
||||
feature: self.sys_config.feature_flags.get(feature, False)
|
||||
for feature in ExperimentalFeature
|
||||
},
|
||||
# Depricated
|
||||
ATTR_WAIT_BOOT: self.sys_config.wait_boot,
|
||||
ATTR_ADDONS: [
|
||||
@@ -182,6 +191,10 @@ class APISupervisor(CoreSysAttributes):
|
||||
if ATTR_WAIT_BOOT in body:
|
||||
self.sys_config.wait_boot = body[ATTR_WAIT_BOOT]
|
||||
|
||||
if ATTR_FEATURE_FLAGS in body:
|
||||
for feature, enabled in body[ATTR_FEATURE_FLAGS].items():
|
||||
self.sys_config.set_feature_flag(feature, enabled)
|
||||
|
||||
# Save changes before processing addons in case of errors
|
||||
await self.sys_updater.save_data()
|
||||
await self.sys_config.save_data()
|
||||
|
||||
@@ -15,6 +15,7 @@ from .const import (
|
||||
ATTR_DEBUG_BLOCK,
|
||||
ATTR_DETECT_BLOCKING_IO,
|
||||
ATTR_DIAGNOSTICS,
|
||||
ATTR_FEATURE_FLAGS,
|
||||
ATTR_IMAGE,
|
||||
ATTR_LAST_BOOT,
|
||||
ATTR_LOGGING,
|
||||
@@ -24,6 +25,7 @@ from .const import (
|
||||
ENV_SUPERVISOR_SHARE,
|
||||
FILE_HASSIO_CONFIG,
|
||||
SUPERVISOR_DATA,
|
||||
ExperimentalFeature,
|
||||
LogLevel,
|
||||
)
|
||||
from .utils.common import FileConfiguration
|
||||
@@ -195,6 +197,17 @@ class CoreConfig(FileConfiguration):
|
||||
lvl = getattr(logging, self.logging.value.upper())
|
||||
logging.getLogger("supervisor").setLevel(lvl)
|
||||
|
||||
@property
|
||||
def feature_flags(self) -> dict[ExperimentalFeature, bool]:
|
||||
"""Return current state of all experimental feature flags."""
|
||||
return self._data.get(ATTR_FEATURE_FLAGS, {})
|
||||
|
||||
def set_feature_flag(self, feature: ExperimentalFeature, enabled: bool) -> None:
|
||||
"""Enable or disable an experimental feature flag."""
|
||||
if ATTR_FEATURE_FLAGS not in self._data:
|
||||
self._data[ATTR_FEATURE_FLAGS] = {}
|
||||
self._data[ATTR_FEATURE_FLAGS][feature] = enabled
|
||||
|
||||
@property
|
||||
def last_boot(self) -> datetime:
|
||||
"""Return last boot datetime."""
|
||||
|
||||
@@ -192,6 +192,7 @@ ATTR_ENVIRONMENT = "environment"
|
||||
ATTR_EVENT = "event"
|
||||
ATTR_EXCLUDE_DATABASE = "exclude_database"
|
||||
ATTR_EXTRA = "extra"
|
||||
ATTR_FEATURE_FLAGS = "feature_flags"
|
||||
ATTR_FEATURES = "features"
|
||||
ATTR_FIELDS = "fields"
|
||||
ATTR_FILENAME = "filename"
|
||||
@@ -548,6 +549,12 @@ class CpuArch(StrEnum):
|
||||
AMD64 = "amd64"
|
||||
|
||||
|
||||
class ExperimentalFeature(StrEnum):
|
||||
"""Experimental features that can be toggled."""
|
||||
|
||||
SUPERVISOR_V2_API = "supervisor_v2_api"
|
||||
|
||||
|
||||
@dataclass
|
||||
class HomeAssistantUser:
|
||||
"""A Home Assistant Core user.
|
||||
|
||||
@@ -20,6 +20,7 @@ from .const import (
|
||||
ATTR_DISPLAYNAME,
|
||||
ATTR_DNS,
|
||||
ATTR_ENABLE_IPV6,
|
||||
ATTR_FEATURE_FLAGS,
|
||||
ATTR_FORCE_SECURITY,
|
||||
ATTR_HASSOS,
|
||||
ATTR_HASSOS_UNRESTRICTED,
|
||||
@@ -47,6 +48,7 @@ from .const import (
|
||||
ATTR_VERSION,
|
||||
ATTR_WAIT_BOOT,
|
||||
SUPERVISOR_VERSION,
|
||||
ExperimentalFeature,
|
||||
LogLevel,
|
||||
UpdateChannel,
|
||||
)
|
||||
@@ -212,6 +214,9 @@ SCHEMA_SUPERVISOR_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_DIAGNOSTICS, default=None): vol.Maybe(vol.Boolean()),
|
||||
vol.Optional(ATTR_DETECT_BLOCKING_IO, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_COUNTRY): str,
|
||||
vol.Optional(ATTR_FEATURE_FLAGS, default=dict): vol.Schema(
|
||||
{vol.Coerce(ExperimentalFeature): vol.Boolean()}
|
||||
),
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ from awesomeversion import AwesomeVersion
|
||||
from blockbuster import BlockingError
|
||||
import pytest
|
||||
|
||||
from supervisor.const import CoreState
|
||||
from supervisor.const import CoreState, ExperimentalFeature
|
||||
from supervisor.core import Core
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.exceptions import HassioError, HostNotSupportedError, StoreGitError
|
||||
@@ -451,3 +451,82 @@ async def test_supervisor_api_stats_failure(
|
||||
"Could not inspect container 'hassio_supervisor': [500] {'message': 'fail'}"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_api_supervisor_info_feature_flags(
|
||||
api_client: TestClient, coresys: CoreSys
|
||||
):
|
||||
"""Test that supervisor info returns all feature flags with default False."""
|
||||
resp = await api_client.get("/supervisor/info")
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
|
||||
assert "feature_flags" in result["data"]
|
||||
feature_flags = result["data"]["feature_flags"]
|
||||
|
||||
# All known experimental features should be present and default to False
|
||||
for feature in ExperimentalFeature:
|
||||
assert feature in feature_flags
|
||||
assert feature_flags[feature] is False
|
||||
|
||||
|
||||
async def test_api_supervisor_options_feature_flags_enable(
|
||||
api_client: TestClient, coresys: CoreSys
|
||||
):
|
||||
"""Test enabling a feature flag via supervisor options."""
|
||||
assert not coresys.config.feature_flags.get(ExperimentalFeature.SUPERVISOR_V2_API)
|
||||
|
||||
response = await api_client.post(
|
||||
"/supervisor/options",
|
||||
json={"feature_flags": {"supervisor_v2_api": True}},
|
||||
)
|
||||
assert response.status == 200
|
||||
assert (
|
||||
coresys.config.feature_flags.get(ExperimentalFeature.SUPERVISOR_V2_API) is True
|
||||
)
|
||||
|
||||
|
||||
async def test_api_supervisor_options_feature_flags_disable(
|
||||
api_client: TestClient, coresys: CoreSys
|
||||
):
|
||||
"""Test disabling a feature flag via supervisor options."""
|
||||
coresys.config.set_feature_flag(ExperimentalFeature.SUPERVISOR_V2_API, True)
|
||||
assert (
|
||||
coresys.config.feature_flags.get(ExperimentalFeature.SUPERVISOR_V2_API) is True
|
||||
)
|
||||
|
||||
response = await api_client.post(
|
||||
"/supervisor/options",
|
||||
json={"feature_flags": {"supervisor_v2_api": False}},
|
||||
)
|
||||
assert response.status == 200
|
||||
assert (
|
||||
coresys.config.feature_flags.get(ExperimentalFeature.SUPERVISOR_V2_API) is False
|
||||
)
|
||||
|
||||
|
||||
async def test_api_supervisor_options_feature_flags_partial_update(
|
||||
api_client: TestClient, coresys: CoreSys
|
||||
):
|
||||
"""Test that omitting a feature flag in options leaves its state unchanged."""
|
||||
coresys.config.set_feature_flag(ExperimentalFeature.SUPERVISOR_V2_API, True)
|
||||
|
||||
# Post options without mentioning feature_flags at all
|
||||
response = await api_client.post("/supervisor/options", json={"debug": False})
|
||||
assert response.status == 200
|
||||
|
||||
# The feature flag should remain True
|
||||
assert (
|
||||
coresys.config.feature_flags.get(ExperimentalFeature.SUPERVISOR_V2_API) is True
|
||||
)
|
||||
|
||||
|
||||
async def test_api_supervisor_options_feature_flags_unknown_flag(
|
||||
api_client: TestClient,
|
||||
):
|
||||
"""Test that an unknown feature flag name is rejected."""
|
||||
response = await api_client.post(
|
||||
"/supervisor/options",
|
||||
json={"feature_flags": {"unknown_feature": True}},
|
||||
)
|
||||
assert response.status == 400
|
||||
|
||||
Reference in New Issue
Block a user