| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- """HA "Run when printer turns on/off" scripts follow automatic switching too.
- They ran only when someone switched the printer's plug by hand. Auto-off
- (delayed, by temperature, resumed after a restart), the plug schedule and AI
- failure detection switched the printer without them. Print start already runs
- the "on" scripts itself, as linked plugs with auto-on, so it is not routed
- through here; nor is the queue's power-on, which print start follows.
- """
- from types import SimpleNamespace
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from backend.app.services import smart_plug_manager as spm
- from backend.app.services.smart_plug_manager import SmartPlugManager
- @pytest.fixture(autouse=True)
- def _fresh_run_memory():
- spm._recent_automatic_script_runs.clear()
- yield
- spm._recent_automatic_script_runs.clear()
- def _plug(**fields):
- base = {
- "id": 1,
- "name": "Plug",
- "plug_type": "tasmota",
- "ha_entity_id": None,
- "printer_id": 7,
- "controls_printer_power": True,
- "auto_on": False,
- "auto_off": False,
- "enabled": True,
- }
- base.update(fields)
- return SimpleNamespace(**base)
- def _script(name, **fields):
- return _plug(name=name, plug_type="homeassistant", ha_entity_id=f"script.{name}", **fields)
- class TestWhichScriptsRun:
- @pytest.mark.asyncio
- @pytest.mark.parametrize(("state", "ran"), [("ON", ["lights_on"]), ("OFF", ["fan_off"])])
- async def test_by_the_direction_of_the_switch(self, state, ran):
- plugs = [_plug(), _script("lights_on", auto_on=True), _script("fan_off", auto_off=True)]
- db = MagicMock()
- db.execute = AsyncMock(return_value=MagicMock(scalars=lambda: MagicMock(all=lambda: plugs)))
- service = MagicMock(turn_on=AsyncMock(return_value=True))
- with patch.object(spm.smart_plug_manager, "get_service_for_plug", AsyncMock(return_value=service)):
- await spm.run_linked_scripts(7, state, db)
- assert [call.args[0].name for call in service.turn_on.await_args_list] == ran
- class TestOnlyThePrintersPowerPlug:
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- ("plug", "printer_id", "runs"),
- [
- (_plug(), 7, True),
- (_plug(controls_printer_power=False), 7, False), # an accessory, e.g. a lamp
- (_script("x"), 7, False),
- (_plug(), None, False),
- ],
- )
- async def test_after_an_automatic_switch(self, plug, printer_id, runs):
- session = MagicMock()
- session.return_value.__aenter__ = AsyncMock(return_value=MagicMock())
- session.return_value.__aexit__ = AsyncMock(return_value=False)
- run = AsyncMock()
- with (
- patch("backend.app.core.database.async_session", session),
- patch.object(spm, "run_linked_scripts", run),
- ):
- await spm.run_linked_scripts_after_automatic_switch(printer_id, plug, "OFF")
- assert run.await_count == (1 if runs else 0)
- @pytest.fixture
- def scripts():
- run = AsyncMock()
- with patch.object(spm, "run_linked_scripts_after_automatic_switch", run):
- yield run
- def _service():
- return MagicMock(turn_on=AsyncMock(return_value=True), turn_off=AsyncMock(return_value=True))
- class TestEveryAutomaticOff:
- @pytest.mark.asyncio
- @pytest.mark.parametrize("feeds_printer", [True, False])
- async def test_the_delayed_auto_off(self, scripts, feeds_printer):
- manager = SmartPlugManager()
- with (
- patch.object(spm, "printer_manager") as pm,
- patch.object(spm, "publish_plug_state", AsyncMock()),
- patch.object(manager, "get_service_for_plug", AsyncMock(return_value=_service())),
- patch.object(manager, "_mark_auto_off_executed", AsyncMock()),
- ):
- pm.is_print_active.return_value = False
- await manager._delayed_off(
- 3, "tasmota", "1.2.3.4", None, None, None, 7, 0, controls_printer_power=feeds_printer
- )
- printer_id, plug, state = scripts.await_args.args
- assert (printer_id, state, plug.controls_printer_power) == (7, "OFF", feeds_printer)
- @pytest.mark.asyncio
- async def test_the_temperature_auto_off(self, scripts):
- manager = SmartPlugManager()
- with (
- patch.object(spm, "printer_manager") as pm,
- patch.object(spm, "publish_plug_state", AsyncMock()),
- patch.object(spm.asyncio, "sleep", AsyncMock()),
- patch.object(manager, "get_service_for_plug", AsyncMock(return_value=_service())),
- patch.object(manager, "_mark_auto_off_executed", AsyncMock()),
- ):
- pm.get_status.return_value = MagicMock(state="FINISH", temperatures={"nozzle": 30})
- pm.is_print_active.return_value = False
- await manager._temp_based_off(3, "tasmota", "1.2.3.4", None, None, None, printer_id=7, temp_threshold=55)
- assert scripts.await_args.args[0] == 7 and scripts.await_args.args[2] == "OFF"
- @pytest.mark.asyncio
- async def test_a_resumed_auto_off(self, scripts):
- manager = SmartPlugManager()
- plug = MagicMock(
- id=1, printer_id=7, off_delay_mode="time", controls_printer_power=True, auto_off_pending_since=None
- )
- db = AsyncMock()
- db.execute = AsyncMock(return_value=MagicMock(scalars=lambda: MagicMock(all=lambda: [plug])))
- session = MagicMock()
- session.return_value.__aenter__ = AsyncMock(return_value=db)
- session.return_value.__aexit__ = AsyncMock(return_value=False)
- with (
- patch("backend.app.core.database.async_session", session),
- patch.object(spm, "printer_manager") as pm,
- patch.object(spm, "publish_plug_state", AsyncMock()),
- patch.object(manager, "get_service_for_plug", AsyncMock(return_value=_service())),
- patch.object(manager, "_mark_auto_off_executed", AsyncMock()),
- ):
- pm.is_print_active.return_value = False
- await manager.resume_pending_auto_offs()
- scripts.assert_awaited_once_with(7, plug, "OFF")
- class TestTheSchedule:
- @pytest.mark.asyncio
- @pytest.mark.parametrize(("now", "state"), [("08:00", "ON"), ("22:00", "OFF")])
- async def test_a_scheduled_switch(self, scripts, now, state):
- manager = SmartPlugManager()
- plug = MagicMock(
- id=1, printer_id=7, schedule_on_time="08:00", schedule_off_time="22:00", controls_printer_power=True
- )
- db = AsyncMock()
- db.execute = AsyncMock(return_value=MagicMock(scalars=lambda: MagicMock(all=lambda: [plug])))
- session = MagicMock()
- session.return_value.__aenter__ = AsyncMock(return_value=db)
- session.return_value.__aexit__ = AsyncMock(return_value=False)
- with (
- patch("backend.app.core.database.async_session", session),
- patch.object(spm, "datetime") as clock,
- patch.object(spm, "printer_manager"),
- patch.object(spm, "publish_plug_state", AsyncMock()),
- patch.object(manager, "get_service_for_plug", AsyncMock(return_value=_service())),
- ):
- clock.now.return_value = MagicMock(strftime=MagicMock(return_value=now))
- await manager._check_schedules()
- scripts.assert_awaited_once_with(7, plug, state)
- class TestFailureDetection:
- @pytest.mark.asyncio
- async def test_scripts_run_once_and_are_not_turned_off(self, scripts):
- """Two plugs feed the printer; the linked script is run, not switched."""
- from backend.app.services import obico_actions
- mains = [_plug(id=1), _plug(id=2)]
- script = _script("fan_off", auto_off=True, id=3)
- service = _service()
- session = MagicMock()
- session.return_value.__aenter__ = AsyncMock(return_value=MagicMock())
- session.return_value.__aexit__ = AsyncMock(return_value=False)
- with (
- patch.object(obico_actions, "async_session", session),
- patch.object(spm.smart_plug_manager, "_get_plugs_for_printer", AsyncMock(return_value=[*mains, script])),
- patch.object(spm.smart_plug_manager, "get_service_for_plug", AsyncMock(return_value=service)),
- patch.object(spm, "publish_plug_state", AsyncMock()),
- ):
- await obico_actions._turn_off_linked_plugs(7)
- assert [call.args[0].id for call in service.turn_off.await_args_list] == [1, 2]
- assert scripts.await_count == 1
- assert scripts.await_args.args[0] == 7 and scripts.await_args.args[2] == "OFF"
- class TestReviewCases:
- @pytest.mark.asyncio
- async def test_a_script_whose_automation_is_off_does_not_run(self):
- plugs = [_script("paused", auto_off=True, enabled=False), _script("live", auto_off=True)]
- db = MagicMock()
- db.execute = AsyncMock(return_value=MagicMock(scalars=lambda: MagicMock(all=lambda: plugs)))
- service = MagicMock(turn_on=AsyncMock(return_value=True))
- with patch.object(spm.smart_plug_manager, "get_service_for_plug", AsyncMock(return_value=service)):
- await spm.run_linked_scripts(7, "OFF", db)
- assert [call.args[0].name for call in service.turn_on.await_args_list] == ["live"]
- @pytest.mark.asyncio
- async def test_two_plugs_switching_the_printer_run_the_scripts_once(self):
- """Two plugs feed one printer and both auto-off after a print."""
- session = MagicMock()
- session.return_value.__aenter__ = AsyncMock(return_value=MagicMock())
- session.return_value.__aexit__ = AsyncMock(return_value=False)
- run = AsyncMock()
- with (
- patch("backend.app.core.database.async_session", session),
- patch.object(spm, "run_linked_scripts", run),
- ):
- await spm.run_linked_scripts_after_automatic_switch(7, _plug(id=1), "OFF")
- await spm.run_linked_scripts_after_automatic_switch(7, _plug(id=2), "OFF")
- await spm.run_linked_scripts_after_automatic_switch(7, _plug(id=1), "ON")
- assert [call.args[1] for call in run.await_args_list] == ["OFF", "ON"]
- @pytest.mark.asyncio
- async def test_failure_detection_that_switched_no_power_plug_runs_nothing(self, scripts):
- from backend.app.services import obico_actions
- lamp = _plug(id=4, controls_printer_power=False)
- session = MagicMock()
- session.return_value.__aenter__ = AsyncMock(return_value=MagicMock())
- session.return_value.__aexit__ = AsyncMock(return_value=False)
- with (
- patch.object(obico_actions, "async_session", session),
- patch.object(spm.smart_plug_manager, "_get_plugs_for_printer", AsyncMock(return_value=[lamp])),
- patch.object(spm.smart_plug_manager, "get_service_for_plug", AsyncMock(return_value=_service())),
- patch.object(spm, "publish_plug_state", AsyncMock()),
- ):
- await obico_actions._turn_off_linked_plugs(7)
- scripts.assert_not_awaited()
|