Files
ClimatController/app/auto/config.py
T

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,
)