391 lines
7.5 KiB
Python
391 lines
7.5 KiB
Python
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
from app.my_dataclasses import (
|
|
MIN_FAN_SPEED,
|
|
MAX_FAN_SPEED,
|
|
)
|
|
|
|
|
|
@dataclass(
|
|
frozen=True,
|
|
slots=True,
|
|
)
|
|
class Co2Config:
|
|
base_speed: int
|
|
hysteresis: int
|
|
|
|
thresholds: tuple[
|
|
tuple[int, int],
|
|
...
|
|
]
|
|
|
|
|
|
@dataclass(
|
|
frozen=True,
|
|
slots=True,
|
|
)
|
|
class AutoConfig:
|
|
version: int
|
|
check_interval: float
|
|
co2: Co2Config
|
|
temperature: TemperatureConfig
|
|
|
|
@dataclass(
|
|
frozen=True,
|
|
slots=True,
|
|
)
|
|
class TemperatureConfig:
|
|
hysteresis: float
|
|
|
|
|
|
def _require_dict(name: str, value: Any) -> dict:
|
|
|
|
if not isinstance(value, dict):
|
|
raise ValueError(
|
|
f"{name} must be an object"
|
|
)
|
|
|
|
return value
|
|
|
|
|
|
def _parse_speed(name: str, value: Any) -> int:
|
|
|
|
if type(value) is not int:
|
|
raise ValueError(
|
|
f"{name} must be an integer"
|
|
)
|
|
|
|
if not (
|
|
MIN_FAN_SPEED
|
|
<= value
|
|
<= MAX_FAN_SPEED
|
|
):
|
|
raise ValueError(
|
|
f"{name} must be between "
|
|
f"{MIN_FAN_SPEED} and "
|
|
f"{MAX_FAN_SPEED}"
|
|
)
|
|
|
|
return value
|
|
|
|
|
|
def _parse_thresholds(value: Any) -> tuple[
|
|
tuple[int, int],
|
|
...
|
|
]:
|
|
|
|
if not isinstance(value, list):
|
|
raise ValueError(
|
|
"co2.thresholds must be a list"
|
|
)
|
|
|
|
if not value:
|
|
raise ValueError(
|
|
"co2.thresholds cannot be empty"
|
|
)
|
|
|
|
result = []
|
|
|
|
previous_ppm = None
|
|
previous_speed = None
|
|
|
|
for index, item in enumerate(value):
|
|
|
|
item = _require_dict(
|
|
f"co2.thresholds[{index}]",
|
|
item,
|
|
)
|
|
|
|
unknown = set(item) - {
|
|
"ppm",
|
|
"speed",
|
|
}
|
|
|
|
if unknown:
|
|
raise ValueError(
|
|
f"Unknown fields in "
|
|
f"co2.thresholds[{index}]: "
|
|
f"{sorted(unknown)}"
|
|
)
|
|
|
|
if "ppm" not in item:
|
|
raise ValueError(
|
|
f"co2.thresholds[{index}].ppm "
|
|
f"is required"
|
|
)
|
|
|
|
if "speed" not in item:
|
|
raise ValueError(
|
|
f"co2.thresholds[{index}].speed "
|
|
f"is required"
|
|
)
|
|
|
|
ppm = item["ppm"]
|
|
|
|
if type(ppm) is not int:
|
|
raise ValueError(
|
|
f"co2.thresholds[{index}].ppm "
|
|
f"must be an integer"
|
|
)
|
|
|
|
if ppm <= 0:
|
|
raise ValueError(
|
|
f"co2.thresholds[{index}].ppm "
|
|
f"must be > 0"
|
|
)
|
|
|
|
speed = _parse_speed(
|
|
(
|
|
f"co2.thresholds"
|
|
f"[{index}].speed"
|
|
),
|
|
item["speed"],
|
|
)
|
|
|
|
if (
|
|
previous_ppm is not None
|
|
and ppm <= previous_ppm
|
|
):
|
|
raise ValueError(
|
|
"CO2 thresholds must be "
|
|
"strictly increasing"
|
|
)
|
|
|
|
if (
|
|
previous_speed is not None
|
|
and speed <= previous_speed
|
|
):
|
|
raise ValueError(
|
|
"AUTO speeds must be "
|
|
"strictly increasing"
|
|
)
|
|
|
|
result.append(
|
|
(
|
|
ppm,
|
|
speed,
|
|
)
|
|
)
|
|
|
|
previous_ppm = ppm
|
|
previous_speed = speed
|
|
|
|
return tuple(result)
|
|
|
|
|
|
def _parse_co2(value: Any) -> Co2Config:
|
|
|
|
data = _require_dict(
|
|
"co2",
|
|
value,
|
|
)
|
|
|
|
unknown = set(data) - {
|
|
"base_speed",
|
|
"hysteresis",
|
|
"thresholds",
|
|
}
|
|
|
|
if unknown:
|
|
raise ValueError(
|
|
f"Unknown CO2 config fields: "
|
|
f"{sorted(unknown)}"
|
|
)
|
|
|
|
if "base_speed" not in data:
|
|
raise ValueError(
|
|
"co2.base_speed is required"
|
|
)
|
|
|
|
if "hysteresis" not in data:
|
|
raise ValueError(
|
|
"co2.hysteresis is required"
|
|
)
|
|
|
|
if "thresholds" not in data:
|
|
raise ValueError(
|
|
"co2.thresholds is required"
|
|
)
|
|
|
|
base_speed = _parse_speed(
|
|
"co2.base_speed",
|
|
data["base_speed"],
|
|
)
|
|
|
|
hysteresis = data["hysteresis"]
|
|
|
|
if type(hysteresis) is not int:
|
|
raise ValueError(
|
|
"co2.hysteresis must be "
|
|
"an integer"
|
|
)
|
|
|
|
if hysteresis < 0:
|
|
raise ValueError(
|
|
"co2.hysteresis must be >= 0"
|
|
)
|
|
|
|
thresholds = _parse_thresholds(
|
|
data["thresholds"]
|
|
)
|
|
|
|
first_speed = thresholds[0][1]
|
|
|
|
if first_speed <= base_speed:
|
|
raise ValueError(
|
|
"First threshold speed must be "
|
|
"greater than base_speed"
|
|
)
|
|
|
|
return Co2Config(
|
|
base_speed=base_speed,
|
|
hysteresis=hysteresis,
|
|
thresholds=thresholds,
|
|
)
|
|
|
|
|
|
def load_auto_config(path: str | Path) -> AutoConfig:
|
|
|
|
path = Path(path)
|
|
|
|
with path.open(
|
|
"r",
|
|
encoding="utf-8",
|
|
) as file:
|
|
raw = yaml.safe_load(file)
|
|
|
|
data = _require_dict(
|
|
"AUTO config",
|
|
raw,
|
|
)
|
|
|
|
unknown = set(data) - {
|
|
"version",
|
|
"check_interval",
|
|
"co2",
|
|
"temperature",
|
|
}
|
|
|
|
if unknown:
|
|
raise ValueError(
|
|
f"Unknown AUTO config fields: "
|
|
f"{sorted(unknown)}"
|
|
)
|
|
|
|
version = data.get("version")
|
|
|
|
if version != 1:
|
|
raise ValueError(
|
|
f"Unsupported AUTO config "
|
|
f"version: {version!r}"
|
|
)
|
|
|
|
check_interval = data.get(
|
|
"check_interval"
|
|
)
|
|
|
|
if (
|
|
type(check_interval) not in {
|
|
int,
|
|
float,
|
|
}
|
|
):
|
|
raise ValueError(
|
|
"check_interval must be a number"
|
|
)
|
|
|
|
if check_interval <= 0:
|
|
raise ValueError(
|
|
"check_interval must be > 0"
|
|
)
|
|
|
|
# --------------------------------------------------
|
|
# CO2
|
|
# --------------------------------------------------
|
|
|
|
if "co2" not in data:
|
|
raise ValueError(
|
|
"co2 config is required"
|
|
)
|
|
|
|
co2 = _parse_co2(
|
|
data["co2"]
|
|
)
|
|
|
|
# --------------------------------------------------
|
|
# Temperature
|
|
# --------------------------------------------------
|
|
|
|
if "temperature" not in data:
|
|
raise ValueError(
|
|
"temperature config is required"
|
|
)
|
|
|
|
temperature_data = _require_dict(
|
|
"temperature",
|
|
data["temperature"],
|
|
)
|
|
|
|
unknown_temperature = (
|
|
set(temperature_data)
|
|
- {
|
|
"hysteresis",
|
|
}
|
|
)
|
|
|
|
if unknown_temperature:
|
|
raise ValueError(
|
|
f"Unknown temperature config fields: "
|
|
f"{sorted(unknown_temperature)}"
|
|
)
|
|
|
|
temperature_hysteresis = (
|
|
temperature_data.get(
|
|
"hysteresis"
|
|
)
|
|
)
|
|
|
|
if (
|
|
type(temperature_hysteresis)
|
|
not in {
|
|
int,
|
|
float,
|
|
}
|
|
):
|
|
raise ValueError(
|
|
"temperature.hysteresis "
|
|
"must be a number"
|
|
)
|
|
|
|
temperature_hysteresis = float(
|
|
temperature_hysteresis
|
|
)
|
|
|
|
if temperature_hysteresis < 0:
|
|
raise ValueError(
|
|
"temperature.hysteresis "
|
|
"must be >= 0"
|
|
)
|
|
|
|
temperature = TemperatureConfig(
|
|
hysteresis=temperature_hysteresis,
|
|
)
|
|
|
|
# --------------------------------------------------
|
|
# Result
|
|
# --------------------------------------------------
|
|
|
|
return AutoConfig(
|
|
version=version,
|
|
check_interval=float(
|
|
check_interval
|
|
),
|
|
co2=co2,
|
|
temperature=temperature,
|
|
)
|
|
|