- CMake: shared-таргет fgl-aircon-shared (FGL_BUILD_SHARED, PIC, SOVERSION) - C-API: fgl_prop_raw_range + кольцевые очереди событий/логов (fgl_session_poll/wait_events, fgl_log_poll/wait_events): прямые колбэки из потоков ядра со стеком 8 КБ небезопасны для интерпретатора Python - pyfglair: _corebuild (cmake->wheel), _cffi (ABI), Session (reader-поток -> loop.call_soon_threadsafe), templates (интроспекция C-таблиц), provision (aiohttp), CLI discover/monitor + esphome-secrets - setup.py/pyproject: платформенный wheel py3-none-linux_x86_64 с .so - tests/pyfglair: templates, provision (мок-облако), session + CLI против mock_ac.py; scripts/py-ci.sh; ci.sh запускает python-часть
116 lines
5.1 KiB
Python
116 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Интеграционные сценарии aircon-слоя (публичный API) против mock-модуля.
|
|
|
|
usage: test_aircon_mock.py <aircon_runner> <mock_ac.py>
|
|
Проверяет: маппинг enum↔имя, конверсию в событиях (DisplayTemperature!),
|
|
оптимистичный кэш, batch=1 notify, RO/не-шаблонные отказы, клэмпинг.
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "ayla"))
|
|
from test_session_mock import ( # noqa: E402
|
|
FAILURES, Proc, check, free_port, LANIP_KEY, KEY_ID, DSN, run_mock)
|
|
|
|
RUNNER = sys.argv[1]
|
|
MOCK = sys.argv[2]
|
|
|
|
|
|
def run_runner(mock_port, listen, duration, props=None, env_extra=None,
|
|
timeout=None):
|
|
env = dict(os.environ)
|
|
if env_extra:
|
|
env.update(env_extra)
|
|
argv = [RUNNER, "127.0.0.1", str(mock_port), str(listen), DSN, LANIP_KEY,
|
|
str(KEY_ID), str(duration)]
|
|
if props:
|
|
argv += props
|
|
p = Proc(argv, env=env)
|
|
return p, (timeout or duration + 25)
|
|
|
|
|
|
def scenario_mapping_and_batch():
|
|
print("== aircon: маппинг/конверсии, batch=1 notify, полный шаблон A")
|
|
mock_port, listen = free_port(), free_port()
|
|
mock = run_mock([], mock_port)
|
|
runner, timeout = run_runner(mock_port, listen, 5,
|
|
props=["OperationMode", "FanSpeed",
|
|
"DisplayTemperature"])
|
|
check(runner.wait_line("STATE online", 10) is not None, "online")
|
|
p1 = runner.wait_line("PROP OperationMode", 10)
|
|
check(p1 is not None and " i:6" in p1, f"operation_mode: {p1}")
|
|
p2 = runner.wait_line("PROP FanSpeed", 10)
|
|
check(p2 is not None and " i:4" in p2, f"fan_speed: {p2}")
|
|
# Конверсия: mock шлёт display_temperature=7000? Нет — mock вернёт
|
|
# значение из своих props (нет в словаре -> 0) => raw 0 -> api -500.
|
|
p3 = runner.wait_line("PROP DisplayTemperature", 10)
|
|
check(p3 is not None and " i:-500" in p3, f"display_temp raw0 -> -500: {p3}")
|
|
# C-API smoke.
|
|
check(runner.wait_line("CVER 0.", 5) is not None, "c_api version")
|
|
cname = runner.wait_line("CNAME", 5)
|
|
check(cname is not None and "fan_speed" in cname, "c_api prop_name")
|
|
conv = runner.wait_line("CCONV 200", 5)
|
|
check(conv is not None, "c_api convert 7000 -> 200")
|
|
rng = runner.wait_line("CRANGE 1 -100 450", 5)
|
|
check(rng is not None, f"c_api raw range adjust_temperature: {rng}")
|
|
check(runner.wait_line("CRANGE_NONE 0", 5) is not None,
|
|
"c_api: строковое свойство без диапазона")
|
|
# batch=1 notify: ждём >=2 REG (второй — асинхронный put после выдачи
|
|
# батча), затем ассертим ровно один notify=True.
|
|
import time as _time
|
|
deadline = _time.time() + 5
|
|
while _time.time() < deadline:
|
|
regs = [l for l in mock.all_lines() if l.startswith("REG")]
|
|
if len(regs) >= 2:
|
|
break
|
|
_time.sleep(0.2)
|
|
notifies = [l for l in regs if "notify=True" in l]
|
|
check(len(regs) >= 2 and len(notifies) == 1,
|
|
f"один notify=True на батч: regs={regs}")
|
|
runner.wait_line("DELETED", timeout)
|
|
runner.stop()
|
|
mock.stop()
|
|
|
|
|
|
def scenario_set_cache_clamp():
|
|
print("== aircon: SET + оптимистичный кэш + клэмпинг + отказы")
|
|
mock_port, listen = free_port(), free_port()
|
|
mock = run_mock([], mock_port)
|
|
runner, timeout = run_runner(mock_port, listen, 5,
|
|
env_extra={"RUNNER_SET_PROP": "FanSpeed",
|
|
"RUNNER_SET_VALUE": "3",
|
|
"RUNNER_SET_AT": "1"})
|
|
check(runner.wait_line("STATE online", 10) is not None, "online")
|
|
check(runner.wait_line("~SET_DONE FanSpeed=3 ok=1", 10) is not None,
|
|
"SET выполнена")
|
|
check(runner.wait_line("CACHED FanSpeed 3", 5) is not None,
|
|
"оптимистичный кэш")
|
|
check(mock.wait_line("~CMD SET fan_speed=3", 10) is not None,
|
|
"mock применил SET (имя из таблицы)")
|
|
# Отказы и клэмпинг печатаются перед DELETED.
|
|
check(runner.wait_line("RO_SET 0", timeout) is not None, "RO отказ")
|
|
check(runner.wait_line("NOTMPL_SET 0", 5) is not None,
|
|
"не-шаблонное свойство отказ")
|
|
check(runner.wait_line("CLAMP_SET 1", 5) is not None, "клэмпинг применён")
|
|
check(mock.wait_line("~CMD SET adjust_temperature=450", 15) is not None,
|
|
"клэмпинг: 1000 -> raw max 450 на модуле")
|
|
runner.wait_line("DELETED", 10)
|
|
runner.stop()
|
|
mock.stop()
|
|
|
|
|
|
def main():
|
|
scenario_mapping_and_batch()
|
|
scenario_set_cache_clamp()
|
|
print()
|
|
if FAILURES:
|
|
print("ПРОВАЛЕНО:", len(FAILURES))
|
|
for f in FAILURES:
|
|
print(" -", f)
|
|
sys.exit(1)
|
|
print("ВСЕ СЦЕНАРИИ ПРОЙДЕНЫ")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|