415 lines
8.7 KiB
Python
415 lines
8.7 KiB
Python
import json
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import paho.mqtt.client as mqtt
|
|
|
|
from app.my_dataclasses import (
|
|
QINGPING_SAMPLE_TIMEOUT,
|
|
QINGPING_RECOVERY_TIMEOUT,
|
|
)
|
|
from app.qingping.service import QingpingService
|
|
|
|
|
|
class FakeClock:
|
|
def __init__(self):
|
|
self.now = 0.0
|
|
|
|
def monotonic(self) -> float:
|
|
return self.now
|
|
|
|
def advance(self, seconds: float) -> None:
|
|
self.now += seconds
|
|
|
|
|
|
class FakeMqttClient:
|
|
def __init__(self):
|
|
self.subscriptions = []
|
|
self.published = []
|
|
|
|
def subscribe(self, topic):
|
|
self.subscriptions.append(topic)
|
|
|
|
def publish(
|
|
self,
|
|
topic,
|
|
payload,
|
|
):
|
|
self.published.append(
|
|
(
|
|
topic,
|
|
json.loads(payload),
|
|
)
|
|
)
|
|
|
|
return SimpleNamespace(
|
|
rc=mqtt.MQTT_ERR_SUCCESS
|
|
)
|
|
|
|
|
|
class FakeMessage:
|
|
def __init__(self, payload: dict):
|
|
self.payload = json.dumps(
|
|
payload
|
|
).encode("utf-8")
|
|
|
|
|
|
def sensor_message(
|
|
timestamp: int,
|
|
co2: int,
|
|
) -> FakeMessage:
|
|
return FakeMessage(
|
|
{
|
|
"type": 17,
|
|
"sensorData": [
|
|
{
|
|
"timestamp": {
|
|
"value": timestamp
|
|
},
|
|
"temperature": {
|
|
"value": 25.0
|
|
},
|
|
"humidity": {
|
|
"value": 50.0
|
|
},
|
|
"co2": {
|
|
"value": co2
|
|
},
|
|
"pm25": {
|
|
"value": 1
|
|
},
|
|
"pm10": {
|
|
"value": 1
|
|
},
|
|
"battery": {
|
|
"value": 100
|
|
},
|
|
}
|
|
],
|
|
}
|
|
)
|
|
|
|
|
|
def service_message() -> FakeMessage:
|
|
return FakeMessage(
|
|
{
|
|
"type": 13,
|
|
"wifi_info": (
|
|
"TestWiFi,-50,1,"
|
|
"00:00:00:00:00:00"
|
|
),
|
|
"sw_version": "4.8.5",
|
|
}
|
|
)
|
|
|
|
|
|
def assert_state(
|
|
service: QingpingService,
|
|
*,
|
|
online: bool,
|
|
recovery_waiting: bool,
|
|
co2,
|
|
):
|
|
status = service.status()
|
|
|
|
assert status["online"] is online, status
|
|
assert (
|
|
status["recovery_waiting"]
|
|
is recovery_waiting
|
|
), status
|
|
|
|
assert status["co2"] == co2, status
|
|
|
|
|
|
def test_double_recovery_cycle() -> None:
|
|
print()
|
|
print("TEST — DOUBLE RECOVERY CYCLE")
|
|
|
|
clock = FakeClock()
|
|
|
|
service = QingpingService(
|
|
host="127.0.0.1",
|
|
)
|
|
|
|
client = FakeMqttClient()
|
|
|
|
# _send_recovery() использует self._client.
|
|
service._client = client
|
|
|
|
with patch(
|
|
"app.qingping.service.time.monotonic",
|
|
clock.monotonic,
|
|
):
|
|
|
|
# --------------------------------------------------
|
|
# START
|
|
# --------------------------------------------------
|
|
|
|
print("1. MQTT connect")
|
|
|
|
service._on_connect(
|
|
client,
|
|
None,
|
|
None,
|
|
0,
|
|
None,
|
|
)
|
|
|
|
assert len(client.published) == 1
|
|
|
|
assert_state(
|
|
service,
|
|
online=False,
|
|
recovery_waiting=True,
|
|
co2=None,
|
|
)
|
|
|
|
# --------------------------------------------------
|
|
# Первый type 17.
|
|
# --------------------------------------------------
|
|
|
|
print("2. First sensor sample")
|
|
|
|
service._on_message(
|
|
None,
|
|
None,
|
|
sensor_message(
|
|
timestamp=1000,
|
|
co2=900,
|
|
),
|
|
)
|
|
|
|
assert_state(
|
|
service,
|
|
online=True,
|
|
recovery_waiting=False,
|
|
co2=900,
|
|
)
|
|
|
|
# --------------------------------------------------
|
|
# Первый отказ.
|
|
# --------------------------------------------------
|
|
|
|
print("3. First sensor timeout")
|
|
|
|
clock.advance(
|
|
QINGPING_SAMPLE_TIMEOUT + 1
|
|
)
|
|
|
|
service._watchdog_tick(
|
|
clock.monotonic()
|
|
)
|
|
|
|
assert_state(
|
|
service,
|
|
online=False,
|
|
recovery_waiting=False,
|
|
co2=900,
|
|
)
|
|
|
|
# Recovery пока НЕ отправлялся.
|
|
assert len(client.published) == 1
|
|
|
|
# --------------------------------------------------
|
|
# Устройство снова появляется.
|
|
# Любой пакет запускает recovery.
|
|
# --------------------------------------------------
|
|
|
|
print("4. First recovery")
|
|
|
|
service._on_message(
|
|
None,
|
|
None,
|
|
service_message(),
|
|
)
|
|
|
|
assert len(client.published) == 2
|
|
|
|
assert_state(
|
|
service,
|
|
online=True,
|
|
recovery_waiting=True,
|
|
co2=None,
|
|
)
|
|
|
|
# --------------------------------------------------
|
|
# Новый type 17.
|
|
# --------------------------------------------------
|
|
|
|
print("5. First recovery completed")
|
|
|
|
service._on_message(
|
|
None,
|
|
None,
|
|
sensor_message(
|
|
timestamp=1015,
|
|
co2=1000,
|
|
),
|
|
)
|
|
|
|
assert_state(
|
|
service,
|
|
online=True,
|
|
recovery_waiting=False,
|
|
co2=1000,
|
|
)
|
|
|
|
# --------------------------------------------------
|
|
# Второй отказ.
|
|
# --------------------------------------------------
|
|
|
|
print("6. Second sensor timeout")
|
|
|
|
clock.advance(
|
|
QINGPING_SAMPLE_TIMEOUT + 1
|
|
)
|
|
|
|
service._watchdog_tick(
|
|
clock.monotonic()
|
|
)
|
|
|
|
assert_state(
|
|
service,
|
|
online=False,
|
|
recovery_waiting=False,
|
|
co2=1000,
|
|
)
|
|
|
|
assert len(client.published) == 2
|
|
|
|
# --------------------------------------------------
|
|
# Второе восстановление.
|
|
# --------------------------------------------------
|
|
|
|
print("7. Second recovery")
|
|
|
|
service._on_message(
|
|
None,
|
|
None,
|
|
service_message(),
|
|
)
|
|
|
|
assert len(client.published) == 3
|
|
|
|
assert_state(
|
|
service,
|
|
online=True,
|
|
recovery_waiting=True,
|
|
co2=None,
|
|
)
|
|
|
|
service._on_message(
|
|
None,
|
|
None,
|
|
sensor_message(
|
|
timestamp=1030,
|
|
co2=1100,
|
|
),
|
|
)
|
|
|
|
assert_state(
|
|
service,
|
|
online=True,
|
|
recovery_waiting=False,
|
|
co2=1100,
|
|
)
|
|
|
|
print("8. Second recovery completed")
|
|
|
|
|
|
def test_recovery_timeout_and_retry() -> None:
|
|
print()
|
|
print("TEST — RECOVERY TIMEOUT AND RETRY")
|
|
|
|
clock = FakeClock()
|
|
|
|
service = QingpingService(
|
|
host="127.0.0.1",
|
|
)
|
|
|
|
client = FakeMqttClient()
|
|
|
|
service._client = client
|
|
|
|
with patch(
|
|
"app.qingping.service.time.monotonic",
|
|
clock.monotonic,
|
|
):
|
|
|
|
# Startup recovery.
|
|
service._on_connect(
|
|
client,
|
|
None,
|
|
None,
|
|
0,
|
|
None,
|
|
)
|
|
|
|
assert len(client.published) == 1
|
|
|
|
assert_state(
|
|
service,
|
|
online=False,
|
|
recovery_waiting=True,
|
|
co2=None,
|
|
)
|
|
|
|
# Type 17 так и не пришёл.
|
|
clock.advance(
|
|
QINGPING_RECOVERY_TIMEOUT + 1
|
|
)
|
|
|
|
service._watchdog_tick(
|
|
clock.monotonic()
|
|
)
|
|
|
|
assert_state(
|
|
service,
|
|
online=False,
|
|
recovery_waiting=False,
|
|
co2=None,
|
|
)
|
|
|
|
# Любой следующий пакет должен разрешить
|
|
# новую recovery-попытку.
|
|
service._on_message(
|
|
None,
|
|
None,
|
|
service_message(),
|
|
)
|
|
|
|
assert len(client.published) == 2
|
|
|
|
assert_state(
|
|
service,
|
|
online=True,
|
|
recovery_waiting=True,
|
|
co2=None,
|
|
)
|
|
|
|
# Теперь приходит type 17.
|
|
service._on_message(
|
|
None,
|
|
None,
|
|
sensor_message(
|
|
timestamp=2000,
|
|
co2=1200,
|
|
),
|
|
)
|
|
|
|
assert_state(
|
|
service,
|
|
online=True,
|
|
recovery_waiting=False,
|
|
co2=1200,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
test_double_recovery_cycle()
|
|
test_recovery_timeout_and_retry()
|
|
|
|
print()
|
|
print(
|
|
"ALL QINGPING RECOVERY TESTS PASSED"
|
|
) |