work: добавил автоматический режим

This commit is contained in:
Fedorov Dmitriy
2026-09-18 23:59:42 +03:00
parent 7dbfd8b069
commit cdec9b0996
24 changed files with 3449 additions and 33 deletions
+101
View File
@@ -0,0 +1,101 @@
from pathlib import Path
from app.auto.config import (
load_auto_config,
)
PROJECT_ROOT = (
Path(__file__)
.resolve()
.parents[1]
)
AUTO_CONFIG_FILE = (
PROJECT_ROOT
/ "config"
/ "auto.yaml"
)
def main():
config = load_auto_config(
AUTO_CONFIG_FILE
)
print()
print("=" * 70)
print("AUTO CONFIG")
print("=" * 70)
print(
"Version:",
config.version,
)
print(
"Check interval:",
config.check_interval,
)
print(
"Base speed:",
config.co2.base_speed,
)
print(
"Hysteresis:",
config.co2.hysteresis,
)
print(
"Thresholds:"
)
for ppm, speed in (
config.co2.thresholds
):
print(
f" CO2 >= {ppm:<4} "
f"-> speed {speed}"
)
assert config.version == 1
assert (
config.check_interval
== 5.0
)
assert (
config.co2.base_speed
== 1
)
assert (
config.co2.hysteresis
== 100
)
assert (
config.co2.thresholds
== (
(800, 2),
(1000, 3),
(1300, 4),
(1600, 5),
(2000, 6),
)
)
print()
print("=" * 70)
print(
"AUTO CONFIG TEST PASSED"
)
print("=" * 70)
if __name__ == "__main__":
main()
+884
View File
@@ -0,0 +1,884 @@
import asyncio
from types import SimpleNamespace
from app.auto.co2_policy import Co2SpeedPolicy
from app.auto.controller import AutoController
# ============================================================
# Fake ScheduleService
# ============================================================
class FakeScheduleService:
def __init__(
self,
*,
enabled: bool = True,
auto_active: bool = False,
fallback_speed: int | None = None,
):
self.enabled = enabled
self.auto_active = auto_active
self.fallback_speed = fallback_speed
self.override_active = False
def resolve(self, now):
return SimpleNamespace(
enabled=self.enabled,
auto_active=self.auto_active,
auto_fallback_speed=self.fallback_speed,
)
# ============================================================
# Fake QingpingService
# ============================================================
class FakeQingpingService:
def __init__(
self,
*,
online: bool = False,
co2: int | None = None,
):
self.online = online
self.state = SimpleNamespace(
co2=co2,
)
# ============================================================
# Fake Tion
# ============================================================
class FakeTionController:
def __init__(self):
self.calls = []
async def set_speed(
self,
speed: int,
):
self.calls.append(
("set_speed", speed)
)
class FakeTionService:
def __init__(self):
self.controller = FakeTionController()
async def execute(
self,
operation,
):
return await operation(
self.controller
)
# ============================================================
# Helper
# ============================================================
def print_state(
title: str,
auto: AutoController,
tion: FakeTionService,
):
print()
print("=" * 70)
print(title)
print("=" * 70)
print(
"Auto status:",
auto.status(),
)
print(
"Tion calls:",
tion.controller.calls,
)
# ============================================================
# Main test
# ============================================================
async def main():
schedule = FakeScheduleService()
qingping = FakeQingpingService()
tion = FakeTionService()
policy = Co2SpeedPolicy(
base_speed=1,
thresholds=(
(800, 2),
(1000, 3),
(1300, 4),
(1600, 5),
(2000, 6),
),
hysteresis=100,
)
auto = AutoController(
schedule_service=schedule,
qingping_service=qingping,
tion_service=tion,
policy=policy,
)
# ========================================================
# TEST 1
#
# AUTO не активен.
#
# AutoController не должен ничего делать.
# ========================================================
schedule.enabled = True
schedule.auto_active = False
schedule.fallback_speed = None
schedule.override_active = False
qingping.online = True
qingping.state.co2 = 1200
await auto._process()
print_state(
"TEST 1 — AUTO INACTIVE",
auto,
tion,
)
assert (
auto.status()["state"]
== "inactive"
)
assert (
auto.status()["reason"]
is None
)
assert (
auto.status()["target_speed"]
is None
)
assert (
auto.status()["auto_speed"]
is None
)
assert tion.controller.calls == []
# ========================================================
# TEST 2
#
# AUTO активен.
# Qingping работает.
# CO2 = 700.
#
# Ожидаем speed 1.
# ========================================================
schedule.auto_active = True
schedule.fallback_speed = 2
qingping.online = True
qingping.state.co2 = 700
await auto._process()
print_state(
"TEST 2 — CO2 700",
auto,
tion,
)
assert (
auto.status()["state"]
== "active"
)
assert (
auto.status()["reason"]
is None
)
assert (
auto.status()["auto_speed"]
== 1
)
assert (
auto.status()["target_speed"]
== 1
)
assert tion.controller.calls == [
("set_speed", 1),
]
# ========================================================
# TEST 3
#
# CO2 вырос до 850.
#
# Ожидаем переход:
#
# speed 1 -> speed 2
# ========================================================
qingping.state.co2 = 850
await auto._process()
print_state(
"TEST 3 — CO2 850",
auto,
tion,
)
assert (
auto.status()["auto_speed"]
== 2
)
assert (
auto.status()["target_speed"]
== 2
)
assert tion.controller.calls == [
("set_speed", 1),
("set_speed", 2),
]
# ========================================================
# TEST 4
#
# CO2 вырос до 1050.
#
# Ожидаем:
#
# speed 2 -> speed 3
# ========================================================
qingping.state.co2 = 1050
await auto._process()
print_state(
"TEST 4 — CO2 1050",
auto,
tion,
)
assert (
auto.status()["auto_speed"]
== 3
)
assert (
auto.status()["target_speed"]
== 3
)
assert tion.controller.calls == [
("set_speed", 1),
("set_speed", 2),
("set_speed", 3),
]
# ========================================================
# TEST 5
#
# CO2 опустился до 950.
#
# Порог speed 3:
#
# вверх = 1000
# вниз = 900
#
# Поэтому остаёмся на speed 3.
# ========================================================
qingping.state.co2 = 950
await auto._process()
print_state(
"TEST 5 — HYSTERESIS CO2 950",
auto,
tion,
)
assert (
auto.status()["auto_speed"]
== 3
)
assert (
auto.status()["target_speed"]
== 3
)
# Новой команды быть не должно.
assert tion.controller.calls == [
("set_speed", 1),
("set_speed", 2),
("set_speed", 3),
]
# ========================================================
# TEST 6
#
# CO2 дошёл до 900.
#
# Теперь переходим:
#
# speed 3 -> speed 2
# ========================================================
qingping.state.co2 = 900
await auto._process()
print_state(
"TEST 6 — CO2 900",
auto,
tion,
)
assert (
auto.status()["auto_speed"]
== 2
)
assert (
auto.status()["target_speed"]
== 2
)
assert tion.controller.calls == [
("set_speed", 1),
("set_speed", 2),
("set_speed", 3),
("set_speed", 2),
]
# ========================================================
# TEST 7
#
# Qingping offline.
#
# AUTO должен перейти в fallback.
#
# fallback speed = 2
#
# Но Tion уже находится на speed 2,
# поэтому повторная команда не нужна.
# ========================================================
qingping.online = False
await auto._process()
print_state(
"TEST 7 — QINGPING OFFLINE",
auto,
tion,
)
assert (
auto.status()["state"]
== "fallback"
)
assert (
auto.status()["reason"]
== "qingping_offline"
)
assert (
auto.status()["auto_speed"]
is None
)
assert (
auto.status()["target_speed"]
== 2
)
assert tion.controller.calls == [
("set_speed", 1),
("set_speed", 2),
("set_speed", 3),
("set_speed", 2),
]
# ========================================================
# TEST 8
#
# Qingping всё ещё offline.
#
# Одинаковую fallback-команду повторять нельзя.
# ========================================================
await auto._process()
print_state(
"TEST 8 — FALLBACK NOT REPEATED",
auto,
tion,
)
assert (
auto.status()["state"]
== "fallback"
)
assert tion.controller.calls == [
("set_speed", 1),
("set_speed", 2),
("set_speed", 3),
("set_speed", 2),
]
# ========================================================
# TEST 9
#
# Qingping online,
# но CO2 отсутствует.
#
# Это тоже fallback.
# ========================================================
qingping.online = True
qingping.state.co2 = None
await auto._process()
print_state(
"TEST 9 — CO2 MISSING",
auto,
tion,
)
assert (
auto.status()["state"]
== "fallback"
)
assert (
auto.status()["reason"]
== "co2_missing"
)
assert (
auto.status()["target_speed"]
== 2
)
assert (
auto.status()["auto_speed"]
is None
)
assert tion.controller.calls == [
("set_speed", 1),
("set_speed", 2),
("set_speed", 3),
("set_speed", 2),
]
# ========================================================
# TEST 10
#
# Qingping восстановился.
#
# CO2 = 1700.
#
# После fallback auto_speed был сброшен,
# поэтому скорость выбирается заново.
#
# Ожидаем speed 5.
# ========================================================
qingping.online = True
qingping.state.co2 = 1700
await auto._process()
print_state(
"TEST 10 — RECOVERY CO2 1700",
auto,
tion,
)
assert (
auto.status()["state"]
== "active"
)
assert (
auto.status()["reason"]
is None
)
assert (
auto.status()["auto_speed"]
== 5
)
assert (
auto.status()["target_speed"]
== 5
)
assert tion.controller.calls == [
("set_speed", 1),
("set_speed", 2),
("set_speed", 3),
("set_speed", 2),
("set_speed", 5),
]
# ========================================================
# TEST 11
#
# Проверяем максимальную скорость 6.
#
# CO2 = 2200
# ========================================================
qingping.state.co2 = 2200
await auto._process()
print_state(
"TEST 11 — CO2 2200",
auto,
tion,
)
assert (
auto.status()["auto_speed"]
== 6
)
assert (
auto.status()["target_speed"]
== 6
)
assert tion.controller.calls[-1] == (
"set_speed",
6,
)
# ========================================================
# TEST 12
#
# Manual override.
#
# Пользователь управляет Tion вручную.
#
# AUTO должен полностью отойти в сторону.
# ========================================================
schedule.override_active = True
await auto._process()
print_state(
"TEST 12 — MANUAL OVERRIDE",
auto,
tion,
)
assert (
auto.status()["state"]
== "suspended"
)
assert (
auto.status()["reason"]
== "manual_override"
)
assert (
auto.status()["target_speed"]
is None
)
assert (
auto.status()["auto_speed"]
is None
)
# Никаких новых команд.
assert tion.controller.calls[-1] == (
"set_speed",
6,
)
# ========================================================
# TEST 13
#
# Manual override закончился.
#
# AUTO должен заново выбрать скорость
# по текущему CO2.
#
# CO2 остаётся 2200 -> speed 6.
# ========================================================
schedule.override_active = False
await auto._process()
print_state(
"TEST 13 — OVERRIDE FINISHED",
auto,
tion,
)
assert (
auto.status()["state"]
== "active"
)
assert (
auto.status()["auto_speed"]
== 6
)
assert (
auto.status()["target_speed"]
== 6
)
# target_speed был сброшен во время override,
# поэтому команда должна быть отправлена заново.
assert tion.controller.calls[-1] == (
"set_speed",
6,
)
# ========================================================
# TEST 14
#
# Qingping снова падает.
#
# fallback = 2.
# ========================================================
qingping.online = False
await auto._process()
print_state(
"TEST 14 — SECOND FAILURE",
auto,
tion,
)
assert (
auto.status()["state"]
== "fallback"
)
assert (
auto.status()["target_speed"]
== 2
)
assert (
auto.status()["auto_speed"]
is None
)
assert tion.controller.calls[-1] == (
"set_speed",
2,
)
# ========================================================
# TEST 15
#
# Началась другая AUTO-точка расписания.
#
# Новый fallback = 3.
#
# Qingping по-прежнему offline.
#
# Ожидаем смену fallback:
#
# 2 -> 3
# ========================================================
schedule.fallback_speed = 3
await auto._process()
print_state(
"TEST 15 — FALLBACK CHANGED",
auto,
tion,
)
assert (
auto.status()["state"]
== "fallback"
)
assert (
auto.status()["target_speed"]
== 3
)
assert tion.controller.calls[-1] == (
"set_speed",
3,
)
# ========================================================
# TEST 16
#
# AUTO закончился.
#
# AutoController перестаёт управлять Tion.
# ========================================================
schedule.auto_active = False
schedule.fallback_speed = None
await auto._process()
print_state(
"TEST 16 — AUTO FINISHED",
auto,
tion,
)
assert (
auto.status()["state"]
== "inactive"
)
assert (
auto.status()["reason"]
is None
)
assert (
auto.status()["target_speed"]
is None
)
assert (
auto.status()["auto_speed"]
is None
)
# ========================================================
# TEST 17
#
# CO2 policy отсутствует.
#
# Например, auto.yaml повреждён.
# AUTO обязан использовать fallback.
# ========================================================
schedule2 = FakeScheduleService(
enabled=True,
auto_active=True,
fallback_speed=4,
)
qingping2 = FakeQingpingService(
online=True,
co2=2500,
)
tion2 = FakeTionService()
auto2 = AutoController(
schedule_service=schedule2,
qingping_service=qingping2,
tion_service=tion2,
policy=None,
)
await auto2._process()
print_state(
"TEST 17 — POLICY UNAVAILABLE",
auto2,
tion2,
)
assert (
auto2.status()["state"]
== "fallback"
)
assert (
auto2.status()["reason"]
== "auto_policy_unavailable"
)
assert (
auto2.status()["target_speed"]
== 4
)
assert (
auto2.status()["auto_speed"]
is None
)
assert tion2.controller.calls == [
("set_speed", 4),
]
print()
print("=" * 70)
print(
"ALL AUTO CONTROLLER TESTS PASSED"
)
print("=" * 70)
if __name__ == "__main__":
asyncio.run(main())
+27
View File
@@ -0,0 +1,27 @@
import asyncio
from bleak import BleakScanner
async def main():
print("Scanning for 20 seconds...")
devices = await BleakScanner.discover(
timeout=20.0,
return_adv=True,
)
print()
print(f"Found: {len(devices)} devices")
print()
for address, (device, adv) in devices.items():
print("ADDRESS:", address)
print("NAME:", device.name)
print("RSSI:", adv.rssi)
print("UUIDS:", adv.service_uuids)
print("SERVICE DATA:", adv.service_data)
print("-" * 60)
asyncio.run(main())
+261
View File
@@ -0,0 +1,261 @@
from app.auto.co2_policy import (
Co2SpeedPolicy,
)
def main():
policy = Co2SpeedPolicy(
base_speed=1,
thresholds=(
(800, 2),
(1000, 3),
(1300, 4),
(1600, 5),
(2000, 6),
),
hysteresis=100,
)
print()
print("=" * 70)
print("TEST 1 — INITIAL SPEED")
print("=" * 70)
cases = (
(500, 1),
(799, 1),
(800, 2),
(999, 2),
(1000, 3),
(1299, 3),
(1300, 4),
(1599, 4),
(1600, 5),
(1999, 5),
(2000, 6),
(2500, 6),
)
for co2, expected in cases:
result = policy.select_speed(
co2,
current_speed=None,
)
print(
f"CO2={co2:<4} "
f"-> speed={result}"
)
assert result == expected
# ========================================================
# TEST 2
#
# Переход 1 -> 2 при 800 ppm.
# ========================================================
print()
print("=" * 70)
print("TEST 2 — SPEED UP")
print("=" * 70)
speed = 1
speed = policy.select_speed(
790,
speed,
)
assert speed == 1
speed = policy.select_speed(
805,
speed,
)
assert speed == 2
print(
"790 -> speed 1"
)
print(
"805 -> speed 2"
)
# ========================================================
# TEST 3
#
# CO2 немного упал ниже 800.
#
# Без гистерезиса получили бы:
# 2 -> 1.
#
# Но пока CO2 > 700,
# остаёмся на второй скорости.
# ========================================================
print()
print("=" * 70)
print("TEST 3 — HYSTERESIS")
print("=" * 70)
speed = policy.select_speed(
790,
current_speed=2,
)
print(
"speed=2, CO2=790 "
f"-> speed={speed}"
)
assert speed == 2
speed = policy.select_speed(
750,
current_speed=2,
)
print(
"speed=2, CO2=750 "
f"-> speed={speed}"
)
assert speed == 2
speed = policy.select_speed(
700,
current_speed=2,
)
print(
"speed=2, CO2=700 "
f"-> speed={speed}"
)
assert speed == 1
# ========================================================
# TEST 4
#
# Гистерезис между speed 2 и speed 3.
#
# Вверх:
# 1000 ppm.
#
# Вниз:
# 900 ppm.
# ========================================================
print()
print("=" * 70)
print("TEST 4 — SPEED 2 / SPEED 3")
print("=" * 70)
speed = policy.select_speed(
1005,
current_speed=2,
)
assert speed == 3
print(
"speed=2, CO2=1005 "
f"-> speed={speed}"
)
speed = policy.select_speed(
950,
current_speed=3,
)
assert speed == 3
print(
"speed=3, CO2=950 "
f"-> speed={speed}"
)
speed = policy.select_speed(
900,
current_speed=3,
)
assert speed == 2
print(
"speed=3, CO2=900 "
f"-> speed={speed}"
)
# ========================================================
# TEST 5
#
# Резкий рост CO2.
#
# Не нужно ждать:
# 1 -> 2 -> 3 -> 4 -> 5
#
# Можно сразу выбрать необходимую скорость.
# ========================================================
print()
print("=" * 70)
print("TEST 5 — LARGE CO2 JUMP")
print("=" * 70)
speed = policy.select_speed(
2200,
current_speed=1,
)
print(
"speed=1, CO2=2200 "
f"-> speed={speed}"
)
assert speed == 6
# ========================================================
# TEST 6
#
# Аналогично при сильном падении CO2
# разрешаем сразу снизить несколько ступеней.
# ========================================================
print()
print("=" * 70)
print("TEST 6 — LARGE CO2 DROP")
print("=" * 70)
speed = policy.select_speed(
650,
current_speed=6,
)
print(
"speed=6, CO2=650 "
f"-> speed={speed}"
)
assert speed == 1
print()
print("=" * 70)
print(
"ALL CO2 POLICY TESTS PASSED"
)
print("=" * 70)
if __name__ == "__main__":
main()
+32
View File
@@ -0,0 +1,32 @@
import asyncio
import json
from app.qingping.service import QingpingService
async def main():
service = QingpingService(
host="192.168.7.3",
port=1883,
mac="CCB5D131BA93",
)
await service.start()
try:
while True:
await asyncio.sleep(15)
print(
json.dumps(
service.status(),
indent=2,
ensure_ascii=False,
)
)
finally:
await service.stop()
asyncio.run(main())
+60
View File
@@ -0,0 +1,60 @@
import json
import paho.mqtt.client as mqtt
HOST = "192.168.7.3"
PORT = 1883
TOPIC = "qingping/CCB5D131BA93/up"
def on_connect(client, userdata, flags, reason_code, properties):
print("Connected:", reason_code)
client.subscribe(TOPIC)
def on_message(client, userdata, message):
try:
payload = json.loads(message.payload.decode("utf-8"))
except Exception:
print("RAW:", message.payload)
return
message_type = str(payload.get("type"))
message_id = payload.get("id")
message_timestamp = payload.get("timestamp")
sensor_timestamps = []
sensor_data = payload.get("sensorData")
if isinstance(sensor_data, list):
for sample in sensor_data:
timestamp = sample.get("timestamp")
if isinstance(timestamp, dict):
timestamp = timestamp.get("value")
sensor_timestamps.append(timestamp)
print(
f"id={message_id!s:<4} "
f"type={message_type:<3} "
f"msg_ts={message_timestamp!s:<12} "
f"samples={sensor_timestamps}"
)
client = mqtt.Client(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
client_id="qingping-raw-debug",
)
client.on_connect = on_connect
client.on_message = on_message
client.connect(HOST, PORT, keepalive=60)
print(f"Listening: {TOPIC}")
client.loop_forever()
+30
View File
@@ -0,0 +1,30 @@
import asyncio
from app.my_dataclasses import QINGPING_MAC
from app.qingping.service import QingpingService
async def main():
service = QingpingService(
QINGPING_MAC
)
async with service:
print("Qingping service started")
while True:
state = service.state
print(
"online:",
service.online,
"| state:",
state.to_dict()
if state
else None,
)
await asyncio.sleep(5)
asyncio.run(main())
+432
View File
@@ -0,0 +1,432 @@
import asyncio
from datetime import datetime
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
from schedule import (
ScheduleActionType,
ScheduleService,
load_schedule,
)
SCHEDULE_YAML = """
version: 1
enabled: true
timezone: local
templates:
test:
- time: "00:00"
action:
type: set
power: off
- time: "09:00"
action:
type: set
power: on
speed: 1
- time: "10:00"
action:
type: auto
speed: 3
- time: "12:00"
action:
type: set
speed: 2
days:
mon: test
tue: test
wed: test
thu: test
fri: test
sat: test
sun: test
"""
class FakeTionController:
def __init__(self):
self.calls = []
async def power_on(self):
self.calls.append(
("power_on", None)
)
async def power_off(self):
self.calls.append(
("power_off", None)
)
async def set_speed(self, speed: int):
self.calls.append(
("set_speed", speed)
)
class FakeTionService:
def __init__(self):
self.controller = FakeTionController()
async def execute(self, operation):
return await operation(
self.controller
)
def load_test_schedule():
temp_dir = TemporaryDirectory()
path = (
Path(temp_dir.name)
/ "schedule.yaml"
)
path.write_text(
SCHEDULE_YAML,
encoding="utf-8",
)
config = load_schedule(path)
return temp_dir, config
def print_resolution(
title,
resolution,
):
print()
print("=" * 70)
print(title)
print("=" * 70)
print(
"Current:",
resolution.current.when
if resolution.current
else None,
)
print(
"Action:",
resolution.current.point.action.type
if resolution.current
else None,
)
print(
"Scheduled speed:",
resolution.scheduled_settings.speed,
)
print(
"AUTO:",
resolution.auto_active,
)
print(
"Fallback:",
resolution.auto_fallback_speed,
)
def test_resolve(service: ScheduleService):
# --------------------------------------------------
# 09:30
# Обычный SET speed=1
# --------------------------------------------------
resolution = service.resolve(
datetime(
2026,
9,
14,
9,
30,
)
)
print_resolution(
"TEST 1 — SET",
resolution,
)
assert resolution.auto_active is False
assert (
resolution.auto_fallback_speed
is None
)
assert (
resolution.scheduled_settings.speed
== 1
)
# --------------------------------------------------
# 10:30
# AUTO speed=3
#
# scheduled_settings.speed всё ещё 1,
# потому что AUTO не участвует
# в накоплении SET-настроек.
#
# Но fallback должен быть 3.
# --------------------------------------------------
resolution = service.resolve(
datetime(
2026,
9,
14,
10,
30,
)
)
print_resolution(
"TEST 2 — AUTO",
resolution,
)
assert (
resolution.current.point.action.type
== ScheduleActionType.AUTO
)
assert resolution.auto_active is True
assert (
resolution.auto_fallback_speed
== 3
)
assert (
resolution.scheduled_settings.speed
== 1
)
# --------------------------------------------------
# 12:30
# AUTO закончился.
# SET speed=2.
# --------------------------------------------------
resolution = service.resolve(
datetime(
2026,
9,
14,
12,
30,
)
)
print_resolution(
"TEST 3 — AFTER AUTO",
resolution,
)
assert resolution.auto_active is False
assert (
resolution.auto_fallback_speed
is None
)
assert (
resolution.scheduled_settings.speed
== 2
)
async def test_process(
service: ScheduleService,
tion: FakeTionService,
):
# --------------------------------------------------
# 09:30
#
# Обычный SET.
# ScheduleService должен применить speed=1.
# --------------------------------------------------
fixed_now = datetime(
2026,
9,
14,
9,
30,
)
with patch(
"schedule.service.datetime",
wraps=datetime,
) as mocked_datetime:
mocked_datetime.now.return_value = (
fixed_now
)
await service._process()
print()
print(
"09:30 calls:",
tion.controller.calls,
)
assert (
"set_speed",
1,
) in tion.controller.calls
# Очищаем историю команд.
tion.controller.calls.clear()
# --------------------------------------------------
# 10:30
#
# AUTO.
#
# В расписании fallback speed=3,
# но ScheduleService НЕ должен
# отправить ни speed=1, ни speed=3.
#
# Скорость теперь принадлежит
# будущему AutoController.
# --------------------------------------------------
fixed_now = datetime(
2026,
9,
14,
10,
30,
)
with patch(
"schedule.service.datetime",
wraps=datetime,
) as mocked_datetime:
mocked_datetime.now.return_value = (
fixed_now
)
await service._process()
print()
print(
"10:30 AUTO calls:",
tion.controller.calls,
)
speed_calls = [
call
for call in tion.controller.calls
if call[0] == "set_speed"
]
assert speed_calls == []
# Очищаем историю.
tion.controller.calls.clear()
# --------------------------------------------------
# 12:30
#
# AUTO закончился.
# Расписание снова должно поставить speed=2.
# --------------------------------------------------
fixed_now = datetime(
2026,
9,
14,
12,
30,
)
with patch(
"schedule.service.datetime",
wraps=datetime,
) as mocked_datetime:
mocked_datetime.now.return_value = (
fixed_now
)
await service._process()
print()
print(
"12:30 calls:",
tion.controller.calls,
)
assert (
"set_speed",
2,
) in tion.controller.calls
async def main():
temp_dir, config = (
load_test_schedule()
)
try:
# ----------------------------------------------
# Проверка resolve()
# ----------------------------------------------
service = ScheduleService(
config
)
test_resolve(service)
# ----------------------------------------------
# Проверка реального _process(),
# но через FakeTion.
# ----------------------------------------------
tion = FakeTionService()
service = ScheduleService(
config,
tion=tion,
)
await test_process(
service,
tion,
)
finally:
temp_dir.cleanup()
print()
print("=" * 70)
print("ALL AUTO SCHEDULE TESTS PASSED")
print("=" * 70)
if __name__ == "__main__":
asyncio.run(main())