Система расписаний

Automatica_1_v2
Sergey Revyakin 2 weeks ago
parent b8b7e0c378
commit 5c215efbf6

@ -1,9 +1,13 @@
from flask import Flask, request, jsonify
from dotenv import dotenv_values
from dotenv import dotenv_values, load_dotenv
from common.nn_profile_schedule import (
get_profile_model_entries,
normalize_profile_name,
resolve_active_profile,
)
from common.runtime import load_root_env, validate_env, as_int, as_str
import os
import sys
import re
import matplotlib.pyplot as plt
from Model import Model
import numpy as np
@ -16,6 +20,7 @@ import shutil
import json
import gc
import logging
from pathlib import Path
TORCHSIG_PATH = "/app/torchsig"
if TORCHSIG_PATH not in sys.path:
@ -39,6 +44,9 @@ plt.ioff()
alg_list = []
model_list = []
ROOT_ENV = load_root_env(__file__)
RUNTIME_ENV = Path(ROOT_ENV).parent / "runtime" / "nn_active_profile.env"
if RUNTIME_ENV.exists():
load_dotenv(RUNTIME_ENV, override=True)
validate_env("NN_server/server.py", {
"GENERAL_SERVER_IP": as_str,
"GENERAL_SERVER_PORT": as_int,
@ -49,10 +57,8 @@ validate_env("NN_server/server.py", {
"SRC_EXAMPLE": as_str,
})
config = dict(dotenv_values(ROOT_ENV))
def is_model_config_key(key, value):
return bool(re.fullmatch(r"NN_\d+", key or "")) and isinstance(value, str) and " && " in value
if RUNTIME_ENV.exists():
config.update(dotenv_values(RUNTIME_ENV))
def get_required_drone_streak(freq):
@ -85,11 +91,20 @@ def update_drone_streak(freq, prediction):
if not config:
raise RuntimeError("[NN_server/server.py] .env was loaded but no keys were parsed")
if not any(is_model_config_key(key, value) for key, value in config.items()):
raise RuntimeError("[NN_server/server.py] no NN_* model entries configured")
logging.info("NN config loaded from %s", ROOT_ENV)
if RUNTIME_ENV.exists():
logging.info("NN runtime overrides loaded from %s", RUNTIME_ENV)
gen_server_ip = config['GENERAL_SERVER_IP']
gen_server_port = config['GENERAL_SERVER_PORT']
requested_profile = normalize_profile_name(config.get("NN_ACTIVE_PROFILE"))
active_profile = resolve_active_profile({k: v for k, v in config.items() if k != "NN_SCHEDULE"})
if requested_profile != active_profile:
logging.warning(
"Requested NN profile %s is not configured, falling back to %s",
requested_profile,
active_profile,
)
logging.info("NN active profile: %s", active_profile)
drone_streaks = {}
def init_data_for_inference():
@ -106,29 +121,32 @@ def init_data_for_inference():
try:
global model_list
for key, value in config.items():
if is_model_config_key(key, value):
params = value.split(' && ')
module = importlib.import_module('Models.' + params[4])
classes = {}
for value in params[9][1:-1].split(','):
classes[len(classes)] = value
model = Model(file_model=params[0], file_config=params[1], src_example=params[2], src_result=params[3],
type_model=params[4], build_model_func=getattr(module, params[5]),
pre_func=getattr(module, params[6]), inference_func=getattr(module, params[7]),
post_func=getattr(module, params[8]), classes=classes, number_synthetic_examples=int(params[10]),
number_src_data_for_one_synthetic_example=int(params[11]), path_to_src_dataset=params[12])
model_list.append(model)
# if key.startswith('ALG_'):
# params = config[key].split(' && ')
# module = importlib.import_module('Algorithms.' + params[2])
# classes = {}
# for value in params[6][1:-1].split(','):
# classes[len(classes)] = value
# alg = Algorithm(src_example=params[0], src_result=params[1], type_alg=params[2], pre_func=getattr(module, params[3]),
# inference_func=getattr(module, params[4]), post_func=getattr(module, params[5]), classes=classes,
# number_synthetic_examples=int(params[7]), number_src_data_for_one_synthetic_example=int(params[8]), path_to_src_dataset=params[9])
# alg_list.append(alg)
model_list = []
loaded_model_keys = []
model_entries = get_profile_model_entries(config, active_profile)
if not model_entries:
raise RuntimeError(f"[NN_server/server.py] no models configured for profile {active_profile!r}")
for key, value in model_entries:
params = value.split(' && ')
module = importlib.import_module('Models.' + params[4])
classes = {}
for value in params[9][1:-1].split(','):
classes[len(classes)] = value
model = Model(file_model=params[0], file_config=params[1], src_example=params[2], src_result=params[3],
type_model=params[4], build_model_func=getattr(module, params[5]),
pre_func=getattr(module, params[6]), inference_func=getattr(module, params[7]),
post_func=getattr(module, params[8]), classes=classes, number_synthetic_examples=int(params[10]),
number_src_data_for_one_synthetic_example=int(params[11]), path_to_src_dataset=params[12])
model_list.append(model)
loaded_model_keys.append(key)
logging.info(
"Loaded %s NN models for profile %s: %s",
len(model_list),
active_profile,
", ".join(loaded_model_keys),
)
except Exception as exc:
print(str(exc))
print()

@ -45,6 +45,7 @@ chmod +x install_all.sh
### Docker Compose
- `dronedetector-server-to-master` -> `src/server_to_master.py`
- `dronedetector-nn-server` -> `NN_server/server.py`
- `dronedetector-telemetry-server` -> `telemetry/telemetry_server.py`
Compose unit:
- `dronedetector-compose.service`
@ -55,6 +56,47 @@ Compose unit:
Все entrypoint'ы загружают root `.env` через `common/runtime.py` и валидируют обязательные переменные. При ошибке сервис падает сразу с понятным сообщением.
### Переключение NN-профилей по расписанию
`NN_server` поднимает один активный профиль моделей на весь процесс.
Базовый профиль:
```bash
NN_1=...
NN_21=...
NN_22=...
```
Именованные профили:
```bash
NN_PROFILE_DAY_1=...
NN_PROFILE_DAY_21=...
NN_PROFILE_DAY_22=...
NN_PROFILE_NIGHT_1=...
NN_PROFILE_NIGHT_21=...
NN_PROFILE_NIGHT_22=...
```
Управляющие ключи:
```bash
NN_ACTIVE_PROFILE=DEFAULT
NN_SCHEDULE=08:00-20:00=DAY;20:00-08:00=NIGHT
```
Правила:
- если `NN_SCHEDULE` пустой, используется `NN_ACTIVE_PROFILE`
- если `NN_ACTIVE_PROFILE` не задан, используется `DEFAULT`
- если выбранный профиль отсутствует, `NN_server` откатывается на `DEFAULT`
- формат расписания: `HH:MM-HH:MM=PROFILE`
- поддерживаются интервалы через полночь
Host-side переключатель:
- `scripts/nn_profile_switcher.py`
- хранит applied profile в `runtime/nn_active_profile.env`
- при смене профиля делает `docker compose restart dronedetector-nn-server`
- запускается через `dronedetector-nn-profile-switch.timer`
## 4. API (без изменения контрактов)
- NN_server: `POST /receive_data`
@ -70,6 +112,8 @@ systemctl status dronedetector-sdr-*.service
journalctl -u dronedetector-sdr-868.service -n 200 --no-pager
systemctl status dronedetector-compose.service
journalctl -u dronedetector-compose.service -n 200 --no-pager
systemctl status dronedetector-nn-profile-switch.timer
journalctl -u dronedetector-nn-profile-switch.service -n 100 --no-pager
```
### docker compose
@ -105,6 +149,7 @@ SDR precheck перед каждым unit запуском:
- Docker Engine (если отсутствует)
- NVIDIA Container Toolkit
- `docker compose up -d --build`
- применение активного NN-профиля через switcher
- установка unit'ов в `/etc/systemd/system`
- verify + авто-логи при ошибке
@ -123,7 +168,7 @@ SDR precheck перед каждым unit запуском:
## 9. Ручная приемка
1. `./install_all.sh` выполняется до конца.
2. `docker compose -f deploy/docker/docker-compose.yml up -d` поднимает оба контейнера.
2. `docker compose -f deploy/docker/docker-compose.yml up -d` поднимает все Docker-сервисы.
3. Все `dronedetector-sdr-*` имеют `active (running)`.
4. Тестовый POST в `NN_server /receive_data` доходит до `server_to_master /process_data`.
5. Контур работает минимум 1 минуту без падений.
@ -210,4 +255,4 @@ docker compose -f deploy/docker/docker-compose.yml logs --timestamps dronedetect
sudo hackrf_spiflash -w hackrf_one_usb.bin
```
./.venv-sdr/bin/python scripts_nn/data_saver_headless.py --serial 000 --freq 4500000000 --save-dir /home/sibscience-4/Dataset/3300 --file-tag DJI_3_ --samp-rate 20000000 --split-size 400000 --delay 0.1 --rf-gain 12 --if-gain 30 --bb-gain 36
./.venv-sdr/bin/python scripts_nn/data_saver_headless.py --serial 000 --freq 4500000000 --save-dir /home/sibscience-4/Dataset/3300 --file-tag DJI_3_ --samp-rate 20000000 --split-size 400000 --delay 0.1 --rf-gain 12 --if-gain 30 --bb-gain 36

@ -0,0 +1,191 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
import re
from typing import Mapping
DEFAULT_PROFILE = "DEFAULT"
MODEL_VALUE_SEPARATOR = " && "
_DEFAULT_MODEL_KEY_RE = re.compile(r"^NN_(?P<id>\d+)$")
_PROFILE_MODEL_KEY_RE = re.compile(r"^NN_PROFILE_(?P<profile>[A-Za-z0-9_]+)_(?P<id>\d+)$")
_SCHEDULE_RULE_RE = re.compile(
r"^\s*(?P<start>\d{2}:\d{2})\s*-\s*(?P<end>\d{2}:\d{2})\s*=\s*(?P<profile>[A-Za-z0-9_]+)\s*$"
)
@dataclass(frozen=True)
class ScheduleRule:
start_minutes: int
end_minutes: int
profile: str
raw_rule: str
def normalize_profile_name(value: str | None) -> str:
normalized = str(value or "").strip().upper()
return normalized or DEFAULT_PROFILE
def load_simple_env_file(path: str | Path) -> dict[str, str]:
env_path = Path(path)
if not env_path.exists():
return {}
values: dict[str, str] = {}
for line in env_path.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if stripped.startswith("export "):
stripped = stripped[len("export ") :].strip()
if "=" not in stripped:
continue
key, raw_value = stripped.split("=", 1)
key = key.strip()
raw_value = raw_value.strip()
if (
len(raw_value) >= 2
and raw_value[0] == raw_value[-1]
and raw_value[0] in {"'", '"'}
):
raw_value = raw_value[1:-1]
values[key] = raw_value
return values
def parse_schedule(schedule_raw: str | None) -> list[ScheduleRule]:
schedule_text = str(schedule_raw or "").strip()
if not schedule_text:
return []
rules: list[ScheduleRule] = []
for raw_rule in schedule_text.split(";"):
candidate = raw_rule.strip()
if not candidate:
continue
match = _SCHEDULE_RULE_RE.fullmatch(candidate)
if match is None:
raise ValueError(f"invalid NN_SCHEDULE rule: {candidate!r}")
start_minutes = _parse_hhmm(match.group("start"))
end_minutes = _parse_hhmm(match.group("end"))
if start_minutes == end_minutes:
raise ValueError(f"invalid NN_SCHEDULE rule with zero-length interval: {candidate!r}")
rules.append(
ScheduleRule(
start_minutes=start_minutes,
end_minutes=end_minutes,
profile=normalize_profile_name(match.group("profile")),
raw_rule=candidate,
)
)
if not rules:
raise ValueError("NN_SCHEDULE does not contain any usable rules")
return rules
def get_requested_active_profile(
config: Mapping[str, str | None], now: datetime | None = None
) -> str:
default_profile = normalize_profile_name(config.get("NN_ACTIVE_PROFILE"))
rules = parse_schedule(config.get("NN_SCHEDULE"))
if not rules:
return default_profile
current = now or datetime.now()
current_minutes = current.hour * 60 + current.minute
for rule in rules:
if _rule_matches(rule, current_minutes):
return rule.profile
return default_profile
def resolve_active_profile(config: Mapping[str, str | None], now: datetime | None = None) -> str:
available_profiles = get_available_profiles(config)
if not available_profiles:
raise ValueError("no NN_* model entries configured")
requested_profile = get_requested_active_profile(config, now=now)
if requested_profile in available_profiles:
return requested_profile
if DEFAULT_PROFILE in available_profiles:
return DEFAULT_PROFILE
raise ValueError(
f"requested NN profile {requested_profile!r} is not configured and DEFAULT profile is unavailable"
)
def get_available_profiles(config: Mapping[str, str | None]) -> set[str]:
profiles: set[str] = set()
for key, value in config.items():
if not _is_model_value(value):
continue
if _DEFAULT_MODEL_KEY_RE.fullmatch(key):
profiles.add(DEFAULT_PROFILE)
continue
match = _PROFILE_MODEL_KEY_RE.fullmatch(key)
if match is not None:
profiles.add(normalize_profile_name(match.group("profile")))
return profiles
def get_profile_model_entries(
config: Mapping[str, str | None], profile: str
) -> list[tuple[str, str]]:
selected_profile = normalize_profile_name(profile)
entries: list[tuple[int, str, str]] = []
for key, value in config.items():
if not _is_model_value(value):
continue
if selected_profile == DEFAULT_PROFILE:
match = _DEFAULT_MODEL_KEY_RE.fullmatch(key)
if match is None:
continue
entries.append((int(match.group("id")), key, value))
continue
match = _PROFILE_MODEL_KEY_RE.fullmatch(key)
if match is None:
continue
if normalize_profile_name(match.group("profile")) != selected_profile:
continue
logical_key = f"NN_{match.group('id')}"
entries.append((int(match.group("id")), logical_key, value))
entries.sort(key=lambda item: item[0])
return [(logical_key, value) for _, logical_key, value in entries]
def format_active_profile_env(profile: str) -> str:
return f"NN_ACTIVE_PROFILE={normalize_profile_name(profile)}\n"
def _parse_hhmm(token: str) -> int:
hours_str, minutes_str = token.split(":", 1)
hours = int(hours_str)
minutes = int(minutes_str)
if not (0 <= hours <= 23 and 0 <= minutes <= 59):
raise ValueError(f"invalid time value {token!r}")
return hours * 60 + minutes
def _rule_matches(rule: ScheduleRule, current_minutes: int) -> bool:
if rule.start_minutes < rule.end_minutes:
return rule.start_minutes <= current_minutes < rule.end_minutes
return current_minutes >= rule.start_minutes or current_minutes < rule.end_minutes
def _is_model_value(value: str | None) -> bool:
return isinstance(value, str) and MODEL_VALUE_SEPARATOR in value

@ -48,6 +48,7 @@ services:
- "8080:8080"
volumes:
- ../../.env:/app/.env:ro
- ../../runtime:/app/runtime
- ../../NN_server:/app/NN_server
- ../../common:/app/common
- ../../train_scripts:/app/train_scripts:ro

@ -0,0 +1,12 @@
[Unit]
Description=DroneDetector NN Profile Switcher
After=network-online.target docker.service dronedetector-compose.service
Wants=network-online.target
Requires=docker.service
ConditionPathExists=__PROJECT_ROOT__/.env
[Service]
Type=oneshot
WorkingDirectory=__PROJECT_ROOT__
ExecCondition=/usr/bin/systemctl --quiet is-active dronedetector-compose.service
ExecStart=/usr/bin/python3 __PROJECT_ROOT__/scripts/nn_profile_switcher.py --project-root __PROJECT_ROOT__

@ -0,0 +1,11 @@
[Unit]
Description=Run DroneDetector NN Profile Switcher Every Minute
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
Persistent=true
Unit=dronedetector-nn-profile-switch.service
[Install]
WantedBy=timers.target

@ -22,6 +22,9 @@ SDR_UNITS=(
dronedetector-sdr-2400.service
)
NN_SWITCHER_SERVICE="dronedetector-nn-profile-switch.service"
NN_SWITCHER_TIMER="dronedetector-nn-profile-switch.timer"
log() {
printf '[install_all] %s\n' "$*"
}
@ -34,6 +37,8 @@ die() {
print_failure_logs() {
log "Collecting diagnostics..."
systemctl --no-pager --full status dronedetector-compose.service || true
systemctl --no-pager --full status "${NN_SWITCHER_SERVICE}" || true
systemctl --no-pager --full status "${NN_SWITCHER_TIMER}" || true
for unit in "${SDR_UNITS[@]}"; do
systemctl --no-pager --full status "$unit" || true
done
@ -45,6 +50,8 @@ print_failure_logs() {
fi
journalctl -u dronedetector-compose.service -n 150 --no-pager || true
journalctl -u "${NN_SWITCHER_SERVICE}" -n 150 --no-pager || true
journalctl -u "${NN_SWITCHER_TIMER}" -n 50 --no-pager || true
for unit in "${SDR_UNITS[@]}"; do
journalctl -u "$unit" -n 120 --no-pager || true
done
@ -103,6 +110,11 @@ install_host_non_python_deps() {
gr-osmosdr
}
prepare_runtime_dir() {
log "Preparing runtime directory"
install -d -m 0775 -o "${RUN_USER}" -g "${RUN_GROUP}" "${PROJECT_ROOT}/runtime"
}
setup_sdr_python_env() {
log "Setting up SDR python environment"
local venv_path="${PROJECT_ROOT}/.venv-sdr"
@ -168,13 +180,19 @@ build_and_run_compose() {
docker compose -f "$COMPOSE_FILE" up -d --build
}
apply_nn_profile_schedule() {
log "Applying NN profile schedule"
python3 "${PROJECT_ROOT}/scripts/nn_profile_switcher.py" --project-root "${PROJECT_ROOT}"
}
install_systemd_units() {
log "Installing systemd units"
install -m 0755 "${PROJECT_ROOT}/deploy/systemd/precheck-sdr.sh" /usr/local/bin/dronedetector-precheck-sdr.sh
local src dst
for src in "${PROJECT_ROOT}"/deploy/systemd/*.service; do
for src in "${PROJECT_ROOT}"/deploy/systemd/*.service "${PROJECT_ROOT}"/deploy/systemd/*.timer; do
[[ -f "$src" ]] || continue
dst="${SYSTEMD_TARGET_DIR}/$(basename "$src")"
sed \
-e "s|__PROJECT_ROOT__|${PROJECT_ROOT}|g" \
@ -186,6 +204,7 @@ install_systemd_units() {
systemctl daemon-reload
systemctl enable dronedetector-compose.service
systemctl restart dronedetector-compose.service
systemctl enable --now "${NN_SWITCHER_TIMER}"
for unit in "${SDR_UNITS[@]}"; do
systemctl enable "$unit"
@ -212,6 +231,7 @@ verify_installation() {
log "Verifying services"
wait_for_systemd_active dronedetector-compose.service 30 || die "dronedetector-compose.service is not active"
wait_for_systemd_active "${NN_SWITCHER_TIMER}" 15 || die "${NN_SWITCHER_TIMER} is not active"
for unit in "${SDR_UNITS[@]}"; do
wait_for_systemd_active "$unit" 45 || die "$unit is not active"
done
@ -223,6 +243,7 @@ verify_installation() {
printf '%s\n' "$running_services" | grep -Fxq "dronedetector-server-to-master" || die "server_to_master is not running"
printf '%s\n' "$running_services" | grep -Fxq "dronedetector-nn-server" || die "NN_server is not running"
printf '%s\n' "$running_services" | grep -Fxq "dronedetector-telemetry-server" || die "telemetry_server is not running"
log "Verification completed"
}
@ -235,10 +256,12 @@ main() {
preflight
install_host_non_python_deps
prepare_runtime_dir
setup_sdr_python_env
install_docker_if_needed
install_nvidia_container_toolkit
build_and_run_compose
apply_nn_profile_schedule
install_systemd_units
verify_installation

@ -0,0 +1,135 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
import subprocess
import sys
from tempfile import NamedTemporaryFile
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from common.nn_profile_schedule import ( # noqa: E402
DEFAULT_PROFILE,
format_active_profile_env,
get_requested_active_profile,
load_simple_env_file,
normalize_profile_name,
resolve_active_profile,
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Switch DroneDetector NN model profile by schedule")
parser.add_argument(
"--project-root",
default=str(PROJECT_ROOT),
help="Path to repository root (default: script parent project)",
)
parser.add_argument(
"--no-restart",
action="store_true",
help="Update runtime profile file without restarting dronedetector-nn-server",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
project_root = Path(args.project_root).resolve()
env_path = project_root / ".env"
runtime_path = project_root / "runtime" / "nn_active_profile.env"
compose_file = project_root / "deploy" / "docker" / "docker-compose.yml"
if not env_path.exists():
raise SystemExit(f"Missing root .env: {env_path}")
if not compose_file.exists():
raise SystemExit(f"Missing docker-compose file: {compose_file}")
config = load_simple_env_file(env_path)
requested_profile = get_requested_active_profile(config)
desired_profile = resolve_active_profile(config)
runtime_exists = runtime_path.exists()
current_profile = read_current_profile(runtime_path)
if requested_profile != desired_profile:
print(
f"[nn_profile_switcher] requested profile {requested_profile} is not configured, "
f"falling back to {desired_profile}"
)
if current_profile == desired_profile and runtime_exists:
print(f"[nn_profile_switcher] profile already active: {desired_profile}")
return 0
previous_contents = runtime_path.read_text(encoding="utf-8") if runtime_path.exists() else None
write_runtime_profile(runtime_path, desired_profile)
if runtime_exists:
print(
f"[nn_profile_switcher] updated runtime profile {current_profile or DEFAULT_PROFILE} -> {desired_profile}"
)
else:
print(f"[nn_profile_switcher] initialized runtime profile: {desired_profile}")
if current_profile == desired_profile:
return 0
if args.no_restart:
print("[nn_profile_switcher] --no-restart set, skipping container restart")
return 0
try:
restart_nn_server(project_root, compose_file)
except Exception:
restore_runtime_profile(runtime_path, previous_contents)
raise
print("[nn_profile_switcher] restarted dronedetector-nn-server")
return 0
def read_current_profile(runtime_path: Path) -> str:
runtime_values = load_simple_env_file(runtime_path)
return normalize_profile_name(runtime_values.get("NN_ACTIVE_PROFILE"))
def write_runtime_profile(runtime_path: Path, profile: str) -> None:
runtime_path.parent.mkdir(parents=True, exist_ok=True)
contents = format_active_profile_env(profile)
with NamedTemporaryFile("w", encoding="utf-8", dir=runtime_path.parent, delete=False) as handle:
handle.write(contents)
temp_path = Path(handle.name)
temp_path.replace(runtime_path)
def restore_runtime_profile(runtime_path: Path, previous_contents: str | None) -> None:
if previous_contents is None:
runtime_path.unlink(missing_ok=True)
return
with NamedTemporaryFile("w", encoding="utf-8", dir=runtime_path.parent, delete=False) as handle:
handle.write(previous_contents)
temp_path = Path(handle.name)
temp_path.replace(runtime_path)
def restart_nn_server(project_root: Path, compose_file: Path) -> None:
subprocess.run(
[
"docker",
"compose",
"-f",
str(compose_file),
"restart",
"dronedetector-nn-server",
],
cwd=project_root,
check=True,
)
if __name__ == "__main__":
raise SystemExit(main())

@ -0,0 +1,84 @@
from datetime import datetime
from pathlib import Path
from tempfile import TemporaryDirectory
import unittest
from common.nn_profile_schedule import (
DEFAULT_PROFILE,
format_active_profile_env,
get_available_profiles,
get_profile_model_entries,
get_requested_active_profile,
load_simple_env_file,
parse_schedule,
resolve_active_profile,
)
MODEL_2400 = "model_2400 && cfg && ex && out && Resnet18_1_2400 && build && pre && infer && post && [drone,noise] && 10 && 1 && /dataset"
MODEL_915 = "model_915 && cfg && ex && out && ensemble_915 && build && pre && infer && post && [drone,noise] && 10 && 1 && /dataset"
class NNProfileScheduleTests(unittest.TestCase):
def test_parse_schedule_supports_wraparound(self):
rules = parse_schedule("08:00-20:00=DAY;20:00-08:00=NIGHT")
self.assertEqual(len(rules), 2)
self.assertEqual(rules[0].profile, "DAY")
self.assertEqual(rules[1].profile, "NIGHT")
def test_requested_profile_uses_local_time_schedule(self):
config = {"NN_SCHEDULE": "08:00-20:00=DAY;20:00-08:00=NIGHT", "NN_ACTIVE_PROFILE": "DEFAULT"}
self.assertEqual(
get_requested_active_profile(config, now=datetime(2026, 4, 23, 9, 0)),
"DAY",
)
self.assertEqual(
get_requested_active_profile(config, now=datetime(2026, 4, 23, 21, 0)),
"NIGHT",
)
def test_resolve_active_profile_falls_back_to_default(self):
config = {
"NN_ACTIVE_PROFILE": "DAY",
"NN_1": MODEL_2400,
}
self.assertEqual(resolve_active_profile(config), DEFAULT_PROFILE)
def test_available_profiles_include_default_and_named(self):
config = {
"NN_1": MODEL_2400,
"NN_PROFILE_DAY_21": MODEL_915,
"NN_PROFILE_NIGHT_22": MODEL_2400,
}
self.assertEqual(get_available_profiles(config), {"DEFAULT", "DAY", "NIGHT"})
def test_get_profile_model_entries_renumbers_to_logical_nn_keys(self):
config = {
"NN_PROFILE_DAY_21": MODEL_915,
"NN_PROFILE_DAY_1": MODEL_2400,
}
entries = get_profile_model_entries(config, "day")
self.assertEqual(entries[0][0], "NN_1")
self.assertEqual(entries[1][0], "NN_21")
def test_load_simple_env_file_strips_quotes(self):
with TemporaryDirectory() as tmpdir:
env_path = Path(tmpdir) / "profile.env"
env_path.write_text("NN_ACTIVE_PROFILE='night'\n", encoding="utf-8")
values = load_simple_env_file(env_path)
self.assertEqual(values["NN_ACTIVE_PROFILE"], "night")
def test_format_active_profile_env_normalizes_value(self):
self.assertEqual(format_active_profile_env("night"), "NN_ACTIVE_PROFILE=NIGHT\n")
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save