mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-08-25 15:01:14 +00:00
REST: Allow setting single config value
* /v1/config/{path} supports setting single config value (post body). Lists are supported as well by using the index: - general/latitude (value: 55.55) - optimize/ev_available_charge_rates_percent/0 (value: 42) Whole tree can be overriden as well (no merge): - optimize/ev_available_charge_rates_percent (value: [42, 43, 44] * ConfigEOS: Add set_config_value, get_config_value
This commit is contained in:
committed by
Bobby Noelte
parent
1bb74ed836
commit
94618f5f66
@@ -30,7 +30,7 @@ from akkudoktoreos.core.coreabc import SingletonMixin
|
||||
from akkudoktoreos.core.decorators import classproperty
|
||||
from akkudoktoreos.core.logging import get_logger
|
||||
from akkudoktoreos.core.logsettings import LoggingCommonSettings
|
||||
from akkudoktoreos.core.pydantic import merge_models
|
||||
from akkudoktoreos.core.pydantic import access_nested_value, merge_models
|
||||
from akkudoktoreos.devices.settings import DevicesCommonSettings
|
||||
from akkudoktoreos.measurement.measurement import MeasurementCommonSettings
|
||||
from akkudoktoreos.optimization.optimization import OptimizationCommonSettings
|
||||
@@ -379,6 +379,32 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
|
||||
"""
|
||||
self._setup()
|
||||
|
||||
def set_config_value(self, path: str, value: Any) -> None:
|
||||
"""Set a configuration value based on the provided path.
|
||||
|
||||
Supports string paths (with '/' separators) or sequence paths (list/tuple).
|
||||
Trims leading and trailing '/' from string paths.
|
||||
|
||||
Args:
|
||||
path (str): The path to the configuration key (e.g., "key1/key2/key3" or key1/key2/0).
|
||||
value (Any]): The value to set.
|
||||
"""
|
||||
access_nested_value(self, path, True, value)
|
||||
|
||||
def get_config_value(self, path: str) -> Any:
|
||||
"""Get a configuration value based on the provided path.
|
||||
|
||||
Supports string paths (with '/' separators) or sequence paths (list/tuple).
|
||||
Trims leading and trailing '/' from string paths.
|
||||
|
||||
Args:
|
||||
path (str): The path to the configuration key (e.g., "key1/key2/key3" or key1/key2/0).
|
||||
|
||||
Returns:
|
||||
Any: The retrieved value.
|
||||
"""
|
||||
return access_nested_value(self, path, False)
|
||||
|
||||
def _create_initial_config_file(self) -> None:
|
||||
if self.general.config_file_path and not self.general.config_file_path.exists():
|
||||
self.general.config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
@@ -51,6 +51,70 @@ def merge_models(source: BaseModel, update_dict: dict[str, Any]) -> dict[str, An
|
||||
return merged_dict
|
||||
|
||||
|
||||
def access_nested_value(
|
||||
model: BaseModel, path: str, setter: bool, value: Optional[Any] = None
|
||||
) -> Any:
|
||||
"""Get or set a nested model value based on the provided path.
|
||||
|
||||
Supports string paths (with '/' separators) or sequence paths (list/tuple).
|
||||
Trims leading and trailing '/' from string paths.
|
||||
|
||||
Args:
|
||||
model (BaseModel): The model object for partial assignment.
|
||||
path (str): The path to the model key (e.g., "key1/key2/key3" or key1/key2/0).
|
||||
setter (bool): True to set value at path, False to return value at path.
|
||||
value (Optional[Any]): The value to set.
|
||||
|
||||
Returns:
|
||||
Any: The retrieved value if acting as a getter, or None if setting a value.
|
||||
"""
|
||||
path_elements = path.strip("/").split("/")
|
||||
|
||||
cfg: Any = model
|
||||
parent: BaseModel = model
|
||||
model_key: str = ""
|
||||
|
||||
for i, key in enumerate(path_elements):
|
||||
is_final_key = i == len(path_elements) - 1
|
||||
|
||||
if isinstance(cfg, list):
|
||||
try:
|
||||
idx = int(key)
|
||||
if is_final_key:
|
||||
if not setter: # Getter
|
||||
return cfg[idx]
|
||||
else: # Setter
|
||||
new_list = list(cfg)
|
||||
new_list[idx] = value
|
||||
# Trigger validation
|
||||
setattr(parent, model_key, new_list)
|
||||
else:
|
||||
cfg = cfg[idx]
|
||||
except ValidationError as e:
|
||||
raise ValueError(f"Error updating model: {e}") from e
|
||||
except (ValueError, IndexError) as e:
|
||||
raise IndexError(f"Invalid list index at {path}: {key}") from e
|
||||
|
||||
elif isinstance(cfg, BaseModel):
|
||||
parent = cfg
|
||||
model_key = key
|
||||
if is_final_key:
|
||||
if not setter: # Getter
|
||||
return getattr(cfg, key)
|
||||
else: # Setter
|
||||
try:
|
||||
# Verification also if nested value is provided opposed to just setattr
|
||||
# Will merge partial assignment
|
||||
cfg = cfg.__pydantic_validator__.validate_assignment(cfg, key, value)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error updating model: {e}") from e
|
||||
else:
|
||||
cfg = getattr(cfg, key)
|
||||
|
||||
else:
|
||||
raise KeyError(f"Key '{key}' not found in model.")
|
||||
|
||||
|
||||
class PydanticTypeAdapterDateTime(TypeAdapter[pendulum.DateTime]):
|
||||
"""Custom type adapter for Pendulum DateTime fields."""
|
||||
|
||||
|
@@ -10,7 +10,9 @@ from typing import Annotated, Any, AsyncGenerator, Dict, List, Optional, Union
|
||||
|
||||
import httpx
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Query, Request
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi import Path as FastapiPath
|
||||
from fastapi import Query, Request
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, Response
|
||||
|
||||
@@ -302,6 +304,58 @@ def fastapi_config_put(settings: SettingsEOS) -> ConfigEOS:
|
||||
return config_eos
|
||||
|
||||
|
||||
@app.put("/v1/config/{path:path}", tags=["config"])
|
||||
def fastapi_config_put_key(
|
||||
path: str = FastapiPath(
|
||||
..., description="The nested path to the configuration key (e.g., general/latitude)."
|
||||
),
|
||||
value: Any = Body(..., description="The value to assign to the specified configuration path."),
|
||||
) -> ConfigEOS:
|
||||
"""Update a nested key or index in the config model.
|
||||
|
||||
Args:
|
||||
path (str): The nested path to the key (e.g., "general/latitude" or "optimize/nested_list/0").
|
||||
value (Any): The new value to assign to the key or index at path.
|
||||
|
||||
Returns:
|
||||
configuration (ConfigEOS): The current configuration after the update.
|
||||
"""
|
||||
try:
|
||||
config_eos.set_config_value(path, value)
|
||||
except IndexError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except KeyError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
return config_eos
|
||||
|
||||
|
||||
@app.get("/v1/config/{path:path}", tags=["config"])
|
||||
def fastapi_config_get_key(
|
||||
path: str = FastapiPath(
|
||||
..., description="The nested path to the configuration key (e.g., general/latitude)."
|
||||
),
|
||||
) -> Response:
|
||||
"""Get the value of a nested key or index in the config model.
|
||||
|
||||
Args:
|
||||
path (str): The nested path to the key (e.g., "general/latitude" or "optimize/nested_list/0").
|
||||
|
||||
Returns:
|
||||
value (Any): The value of the selected nested key.
|
||||
"""
|
||||
try:
|
||||
return config_eos.get_config_value(path)
|
||||
except IndexError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except KeyError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/v1/measurement/keys", tags=["measurement"])
|
||||
def fastapi_measurement_keys_get() -> list[str]:
|
||||
"""Get a list of available measurement keys."""
|
||||
|
Reference in New Issue
Block a user