feat: v.1.1.0 work: добавил режим ручного управления. Теперь можно отключать расписание!
POST api/schedule/pause POST api/schedule/resume
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.auto.controller import AutoController
|
||||
|
||||
|
||||
class FakeScheduleService:
|
||||
|
||||
def __init__(self):
|
||||
self.paused = True
|
||||
self.override_active = False
|
||||
|
||||
def resolve(self, now):
|
||||
return SimpleNamespace(
|
||||
enabled=True,
|
||||
auto_active=True,
|
||||
auto_fallback_speed=1,
|
||||
auto_target_temp=20,
|
||||
)
|
||||
|
||||
|
||||
class FakeQingpingService:
|
||||
|
||||
def __init__(self):
|
||||
self.online = True
|
||||
|
||||
self.state = SimpleNamespace(
|
||||
co2=1200,
|
||||
temperature=19.0,
|
||||
)
|
||||
|
||||
|
||||
class FakeTionController:
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
async def set_speed(self, speed):
|
||||
self.calls.append(
|
||||
("set_speed", speed)
|
||||
)
|
||||
|
||||
async def heater_on(self):
|
||||
self.calls.append(
|
||||
("heater_on", None)
|
||||
)
|
||||
|
||||
async def heater_off(self):
|
||||
self.calls.append(
|
||||
("heater_off", None)
|
||||
)
|
||||
|
||||
async def set_target_temperature(
|
||||
self,
|
||||
temperature,
|
||||
):
|
||||
self.calls.append(
|
||||
(
|
||||
"set_target_temperature",
|
||||
temperature,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class FakeTionService:
|
||||
|
||||
def __init__(self):
|
||||
self.online = True
|
||||
|
||||
self.state = SimpleNamespace(
|
||||
in_temp=19,
|
||||
)
|
||||
|
||||
self.controller = FakeTionController()
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
operation,
|
||||
):
|
||||
return await operation(
|
||||
self.controller
|
||||
)
|
||||
|
||||
|
||||
async def test_paused_schedule_suspends_auto():
|
||||
|
||||
schedule = FakeScheduleService()
|
||||
qingping = FakeQingpingService()
|
||||
tion = FakeTionService()
|
||||
|
||||
controller = AutoController(
|
||||
schedule_service=schedule,
|
||||
qingping_service=qingping,
|
||||
tion_service=tion,
|
||||
)
|
||||
|
||||
# Имитируем состояние,
|
||||
# которое AUTO помнил до pause.
|
||||
controller._target_speed = 3
|
||||
controller._auto_speed = 3
|
||||
|
||||
controller._target_heater = True
|
||||
controller._auto_heater = True
|
||||
|
||||
controller._target_temperature = 30
|
||||
|
||||
controller._temperature = 19.0
|
||||
controller._temperature_source = (
|
||||
"qingping"
|
||||
)
|
||||
|
||||
await controller._process()
|
||||
|
||||
status = controller.status()
|
||||
|
||||
# AUTO должен быть остановлен.
|
||||
assert (
|
||||
status["state"]
|
||||
== "suspended"
|
||||
)
|
||||
|
||||
assert (
|
||||
status["reason"]
|
||||
== "schedule_paused"
|
||||
)
|
||||
|
||||
# Никаких команд Tion.
|
||||
assert tion.controller.calls == []
|
||||
|
||||
# Старое владение AUTO забыто.
|
||||
assert (
|
||||
controller._target_speed
|
||||
is None
|
||||
)
|
||||
|
||||
assert (
|
||||
controller._auto_speed
|
||||
is None
|
||||
)
|
||||
|
||||
assert (
|
||||
controller._target_heater
|
||||
is None
|
||||
)
|
||||
|
||||
assert (
|
||||
controller._auto_heater
|
||||
is None
|
||||
)
|
||||
|
||||
assert (
|
||||
controller._target_temperature
|
||||
is None
|
||||
)
|
||||
|
||||
assert (
|
||||
controller._temperature
|
||||
is None
|
||||
)
|
||||
|
||||
assert (
|
||||
controller._temperature_source
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
await test_paused_schedule_suspends_auto()
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
print(
|
||||
"ALL AUTO SCHEDULE PAUSE "
|
||||
"TESTS PASSED"
|
||||
)
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,203 @@
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from schedule import (
|
||||
ScheduleService,
|
||||
ScheduledSettings,
|
||||
load_schedule,
|
||||
)
|
||||
|
||||
|
||||
SCHEDULE_YAML = """
|
||||
version: 1
|
||||
|
||||
enabled: true
|
||||
timezone: local
|
||||
|
||||
templates:
|
||||
|
||||
test:
|
||||
|
||||
- time: "00: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 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
|
||||
|
||||
|
||||
async def test_paused_schedule_does_not_control_tion():
|
||||
|
||||
temp_dir, config = (
|
||||
load_test_schedule()
|
||||
)
|
||||
|
||||
try:
|
||||
tion = FakeTionService()
|
||||
|
||||
service = ScheduleService(
|
||||
config,
|
||||
tion=tion,
|
||||
)
|
||||
|
||||
assert service.paused is False
|
||||
|
||||
await service.pause()
|
||||
|
||||
assert service.paused is True
|
||||
|
||||
# Даже если фоновый цикл вызывает _process(),
|
||||
# никаких команд быть не должно.
|
||||
await service._process()
|
||||
|
||||
assert tion.controller.calls == []
|
||||
|
||||
# Возвращаем расписание.
|
||||
#
|
||||
# resume() должен сразу применить
|
||||
# текущее состояние расписания.
|
||||
await service.resume()
|
||||
|
||||
assert service.paused is False
|
||||
|
||||
assert (
|
||||
"set_speed",
|
||||
2,
|
||||
) in tion.controller.calls
|
||||
|
||||
finally:
|
||||
temp_dir.cleanup()
|
||||
|
||||
|
||||
async def test_pause_clears_override():
|
||||
|
||||
temp_dir, config = (
|
||||
load_test_schedule()
|
||||
)
|
||||
|
||||
try:
|
||||
service = ScheduleService(
|
||||
config
|
||||
)
|
||||
|
||||
# Имитируем уже существующий
|
||||
# temporary override.
|
||||
service._override_settings = (
|
||||
ScheduledSettings(
|
||||
speed=5,
|
||||
)
|
||||
)
|
||||
|
||||
service._override_until = (
|
||||
datetime.now()
|
||||
+ timedelta(hours=1)
|
||||
)
|
||||
|
||||
service._override_pending = True
|
||||
|
||||
assert (
|
||||
service.override_settings.speed
|
||||
== 5
|
||||
)
|
||||
|
||||
await service.pause()
|
||||
|
||||
assert service.paused is True
|
||||
|
||||
assert (
|
||||
service.override_settings.speed
|
||||
is None
|
||||
)
|
||||
|
||||
assert (
|
||||
service.override_until
|
||||
is None
|
||||
)
|
||||
|
||||
assert (
|
||||
service._override_pending
|
||||
is False
|
||||
)
|
||||
|
||||
finally:
|
||||
temp_dir.cleanup()
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
await test_paused_schedule_does_not_control_tion()
|
||||
|
||||
await test_pause_clears_override()
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
print(
|
||||
"ALL SCHEDULE PAUSE TESTS PASSED"
|
||||
)
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,179 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from schedule import (
|
||||
ScheduleService,
|
||||
load_schedule,
|
||||
)
|
||||
|
||||
|
||||
SCHEDULE_YAML = """
|
||||
version: 1
|
||||
|
||||
enabled: true
|
||||
timezone: local
|
||||
|
||||
templates:
|
||||
|
||||
test:
|
||||
|
||||
- time: "00:00"
|
||||
action:
|
||||
type: set
|
||||
speed: 2
|
||||
|
||||
days:
|
||||
mon: test
|
||||
tue: test
|
||||
wed: test
|
||||
thu: test
|
||||
fri: test
|
||||
sat: test
|
||||
sun: test
|
||||
"""
|
||||
|
||||
|
||||
def create_config(
|
||||
directory: Path,
|
||||
):
|
||||
|
||||
schedule_path = (
|
||||
directory
|
||||
/ "schedule.yaml"
|
||||
)
|
||||
|
||||
schedule_path.write_text(
|
||||
SCHEDULE_YAML,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
return load_schedule(
|
||||
schedule_path
|
||||
)
|
||||
|
||||
|
||||
async def test_pause_survives_restart():
|
||||
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
|
||||
directory = Path(temp_dir)
|
||||
|
||||
config = create_config(
|
||||
directory
|
||||
)
|
||||
|
||||
state_path = (
|
||||
directory
|
||||
/ "schedule_state.json"
|
||||
)
|
||||
|
||||
# --------------------------------------
|
||||
# Первый экземпляр сервиса.
|
||||
# --------------------------------------
|
||||
|
||||
service1 = ScheduleService(
|
||||
config,
|
||||
state_path=state_path,
|
||||
)
|
||||
|
||||
assert service1.paused is False
|
||||
|
||||
await service1.pause()
|
||||
|
||||
assert service1.paused is True
|
||||
assert state_path.exists()
|
||||
|
||||
# --------------------------------------
|
||||
# Имитируем перезапуск приложения:
|
||||
# создаём новый ScheduleService.
|
||||
# --------------------------------------
|
||||
|
||||
service2 = ScheduleService(
|
||||
config,
|
||||
state_path=state_path,
|
||||
)
|
||||
|
||||
assert service2.paused is True
|
||||
|
||||
|
||||
async def test_resume_survives_restart():
|
||||
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
|
||||
directory = Path(temp_dir)
|
||||
|
||||
config = create_config(
|
||||
directory
|
||||
)
|
||||
|
||||
state_path = (
|
||||
directory
|
||||
/ "schedule_state.json"
|
||||
)
|
||||
|
||||
service1 = ScheduleService(
|
||||
config,
|
||||
state_path=state_path,
|
||||
)
|
||||
|
||||
await service1.pause()
|
||||
|
||||
assert service1.paused is True
|
||||
|
||||
await service1.resume()
|
||||
|
||||
assert service1.paused is False
|
||||
|
||||
# Новый экземпляр после resume
|
||||
# тоже должен быть активным.
|
||||
service2 = ScheduleService(
|
||||
config,
|
||||
state_path=state_path,
|
||||
)
|
||||
|
||||
assert service2.paused is False
|
||||
|
||||
|
||||
async def test_missing_state_file_defaults_to_active():
|
||||
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
|
||||
directory = Path(temp_dir)
|
||||
|
||||
config = create_config(
|
||||
directory
|
||||
)
|
||||
|
||||
state_path = (
|
||||
directory
|
||||
/ "missing_state.json"
|
||||
)
|
||||
|
||||
assert not state_path.exists()
|
||||
|
||||
service = ScheduleService(
|
||||
config,
|
||||
state_path=state_path,
|
||||
)
|
||||
|
||||
assert service.paused is False
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
await test_pause_survives_restart()
|
||||
await test_resume_survives_restart()
|
||||
await test_missing_state_file_defaults_to_active()
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
print(
|
||||
"ALL SCHEDULE PAUSE "
|
||||
"PERSISTENCE TESTS PASSED"
|
||||
)
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user