feat(tv-power): implement server PR1 with tests and documentation
This commit is contained in:
191
scheduler/test_power_intent_scheduler.py
Normal file
191
scheduler/test_power_intent_scheduler.py
Normal file
@@ -0,0 +1,191 @@
|
||||
import json
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from scheduler.scheduler import (
|
||||
_publish_group_power_intents,
|
||||
_republish_cached_power_intents,
|
||||
)
|
||||
|
||||
|
||||
class _FakePublishResult:
|
||||
def __init__(self, rc=0):
|
||||
self.rc = rc
|
||||
self.wait_timeout = None
|
||||
|
||||
def wait_for_publish(self, timeout=None):
|
||||
self.wait_timeout = timeout
|
||||
|
||||
|
||||
class _FakeMqttClient:
|
||||
def __init__(self, rc=0):
|
||||
self.rc = rc
|
||||
self.calls = []
|
||||
|
||||
def publish(self, topic, payload, qos=0, retain=False):
|
||||
result = _FakePublishResult(rc=self.rc)
|
||||
self.calls.append(
|
||||
{
|
||||
"topic": topic,
|
||||
"payload": payload,
|
||||
"qos": qos,
|
||||
"retain": retain,
|
||||
"result": result,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class PowerIntentSchedulerTests(unittest.TestCase):
|
||||
def test_transition_then_heartbeat_reuses_intent_id(self):
|
||||
client = _FakeMqttClient(rc=0)
|
||||
last_power_intents = {}
|
||||
metrics = {
|
||||
"intent_transitions_total": 0,
|
||||
"publish_success_total": 0,
|
||||
"publish_error_total": 0,
|
||||
"heartbeat_republish_total": 0,
|
||||
"retained_republish_total": 0,
|
||||
}
|
||||
|
||||
events = [
|
||||
{
|
||||
"id": 101,
|
||||
"group_id": 12,
|
||||
"start": "2026-03-31T10:00:00+00:00",
|
||||
"end": "2026-03-31T10:30:00+00:00",
|
||||
}
|
||||
]
|
||||
|
||||
now_first = datetime(2026, 3, 31, 10, 5, 0, tzinfo=timezone.utc)
|
||||
_publish_group_power_intents(
|
||||
client=client,
|
||||
events=events,
|
||||
now=now_first,
|
||||
poll_interval=15,
|
||||
heartbeat_enabled=True,
|
||||
expiry_multiplier=3,
|
||||
min_expiry_seconds=90,
|
||||
last_power_intents=last_power_intents,
|
||||
power_intent_metrics=metrics,
|
||||
)
|
||||
|
||||
first_payload = json.loads(client.calls[0]["payload"])
|
||||
first_intent_id = first_payload["intent_id"]
|
||||
|
||||
now_second = now_first + timedelta(seconds=15)
|
||||
_publish_group_power_intents(
|
||||
client=client,
|
||||
events=events,
|
||||
now=now_second,
|
||||
poll_interval=15,
|
||||
heartbeat_enabled=True,
|
||||
expiry_multiplier=3,
|
||||
min_expiry_seconds=90,
|
||||
last_power_intents=last_power_intents,
|
||||
power_intent_metrics=metrics,
|
||||
)
|
||||
|
||||
self.assertEqual(len(client.calls), 2)
|
||||
second_payload = json.loads(client.calls[1]["payload"])
|
||||
|
||||
self.assertEqual(first_payload["desired_state"], "on")
|
||||
self.assertEqual(second_payload["desired_state"], "on")
|
||||
self.assertEqual(first_intent_id, second_payload["intent_id"])
|
||||
self.assertEqual(client.calls[0]["topic"], "infoscreen/groups/12/power/intent")
|
||||
self.assertEqual(client.calls[0]["qos"], 1)
|
||||
self.assertTrue(client.calls[0]["retain"])
|
||||
|
||||
self.assertEqual(metrics["intent_transitions_total"], 1)
|
||||
self.assertEqual(metrics["heartbeat_republish_total"], 1)
|
||||
self.assertEqual(metrics["publish_success_total"], 2)
|
||||
self.assertEqual(metrics["publish_error_total"], 0)
|
||||
|
||||
def test_state_change_creates_new_intent_id(self):
|
||||
client = _FakeMqttClient(rc=0)
|
||||
last_power_intents = {}
|
||||
metrics = {
|
||||
"intent_transitions_total": 0,
|
||||
"publish_success_total": 0,
|
||||
"publish_error_total": 0,
|
||||
"heartbeat_republish_total": 0,
|
||||
"retained_republish_total": 0,
|
||||
}
|
||||
|
||||
events_on = [
|
||||
{
|
||||
"id": 88,
|
||||
"group_id": 3,
|
||||
"start": "2026-03-31T10:00:00+00:00",
|
||||
"end": "2026-03-31T10:30:00+00:00",
|
||||
}
|
||||
]
|
||||
now_on = datetime(2026, 3, 31, 10, 5, 0, tzinfo=timezone.utc)
|
||||
_publish_group_power_intents(
|
||||
client=client,
|
||||
events=events_on,
|
||||
now=now_on,
|
||||
poll_interval=15,
|
||||
heartbeat_enabled=True,
|
||||
expiry_multiplier=3,
|
||||
min_expiry_seconds=90,
|
||||
last_power_intents=last_power_intents,
|
||||
power_intent_metrics=metrics,
|
||||
)
|
||||
|
||||
first_payload = json.loads(client.calls[0]["payload"])
|
||||
|
||||
events_off = [
|
||||
{
|
||||
"id": 88,
|
||||
"group_id": 3,
|
||||
"start": "2026-03-31T10:00:00+00:00",
|
||||
"end": "2026-03-31T10:30:00+00:00",
|
||||
}
|
||||
]
|
||||
now_off = datetime(2026, 3, 31, 10, 35, 0, tzinfo=timezone.utc)
|
||||
_publish_group_power_intents(
|
||||
client=client,
|
||||
events=events_off,
|
||||
now=now_off,
|
||||
poll_interval=15,
|
||||
heartbeat_enabled=True,
|
||||
expiry_multiplier=3,
|
||||
min_expiry_seconds=90,
|
||||
last_power_intents=last_power_intents,
|
||||
power_intent_metrics=metrics,
|
||||
)
|
||||
|
||||
second_payload = json.loads(client.calls[1]["payload"])
|
||||
self.assertNotEqual(first_payload["intent_id"], second_payload["intent_id"])
|
||||
self.assertEqual(second_payload["desired_state"], "off")
|
||||
self.assertEqual(metrics["intent_transitions_total"], 2)
|
||||
|
||||
def test_republish_cached_power_intents(self):
|
||||
client = _FakeMqttClient(rc=0)
|
||||
metrics = {
|
||||
"intent_transitions_total": 0,
|
||||
"publish_success_total": 0,
|
||||
"publish_error_total": 0,
|
||||
"heartbeat_republish_total": 0,
|
||||
"retained_republish_total": 0,
|
||||
}
|
||||
cache = {
|
||||
5: {
|
||||
"fingerprint": "abc",
|
||||
"intent_id": "intent-1",
|
||||
"payload": '{"group_id":5,"desired_state":"on"}',
|
||||
}
|
||||
}
|
||||
|
||||
_republish_cached_power_intents(client, cache, metrics)
|
||||
|
||||
self.assertEqual(len(client.calls), 1)
|
||||
self.assertEqual(client.calls[0]["topic"], "infoscreen/groups/5/power/intent")
|
||||
self.assertEqual(client.calls[0]["qos"], 1)
|
||||
self.assertTrue(client.calls[0]["retain"])
|
||||
self.assertEqual(metrics["retained_republish_total"], 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user