- 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-часть
181 lines
6.8 KiB
C++
181 lines
6.8 KiB
C++
// Тесты публичной сессии (aircon-слой) против mock-модуля: маппинг
|
|
// имя↔enum, конверсии в событиях, оптимистичный кэш, batch, C-API.
|
|
// Запускается python-оркестратором (tests/aircon/test_aircon_mock.py).
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <thread>
|
|
|
|
#include "fgl-aircon/c_api.h"
|
|
#include "fgl-aircon/session.hpp"
|
|
#include "fgl-aircon/templates.hpp"
|
|
|
|
using fgl::aircon::Prop;
|
|
using fgl::aircon::Session;
|
|
using fgl::aircon::State;
|
|
|
|
static const char* state_name(State st) {
|
|
switch (st) {
|
|
case State::kIdle: return "idle";
|
|
case State::kRegistering: return "registering";
|
|
case State::kOnline: return "online";
|
|
case State::kRecovering: return "recovering";
|
|
case State::kOffline: return "offline";
|
|
case State::kKeyError: return "key_error";
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
static void on_state(void*, State st, fgl::aircon::Error err) {
|
|
printf("STATE %s %d\n", state_name(st), static_cast<int>(err));
|
|
fflush(stdout);
|
|
}
|
|
|
|
static void on_property(void*, const fgl::aircon::PropertyEvent& ev) {
|
|
const char* n = fgl::aircon::prop_enum_name(ev.prop);
|
|
if (ev.value.kind == fgl::aircon::ValueKind::kString) {
|
|
printf("PROP %s %d %d s:%s\n", n, ev.cmd_id, ev.status, ev.value.s);
|
|
} else {
|
|
printf("PROP %s %d %d %c:%lld\n", n, ev.cmd_id, ev.status,
|
|
ev.value.kind == fgl::aircon::ValueKind::kBool ? 'b' : 'i',
|
|
static_cast<long long>(ev.value.i));
|
|
}
|
|
fflush(stdout);
|
|
}
|
|
|
|
static uint32_t env_u32(const char* name, uint32_t def) {
|
|
const char* v = getenv(name);
|
|
return v != nullptr ? static_cast<uint32_t>(atoi(v)) : def;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc < 6) {
|
|
fprintf(stderr,
|
|
"usage: %s <host> <device_port> <listen_port> <dsn> <lanip_key> "
|
|
"<key_id> <duration_sec> [prop-enum-name ...]\n",
|
|
argv[0]);
|
|
return 2;
|
|
}
|
|
fgl::aircon::Config cfg{};
|
|
cfg.host = argv[1];
|
|
cfg.device_port = static_cast<uint16_t>(atoi(argv[2]));
|
|
cfg.listen_port = static_cast<uint16_t>(atoi(argv[3]));
|
|
cfg.dsn = argv[4];
|
|
cfg.lanip_key = argv[5];
|
|
cfg.lanip_key_id = static_cast<uint32_t>(atoi(argv[6]));
|
|
cfg.tmpl = fgl::aircon::Template::kA;
|
|
int duration_sec = atoi(argv[7]);
|
|
|
|
fgl::aircon::Callbacks cbs{};
|
|
cbs.on_state = on_state;
|
|
cbs.on_property = on_property;
|
|
|
|
Session* s = Session::create(cfg, cbs);
|
|
if (s == nullptr) {
|
|
printf("CREATE_FAILED\n");
|
|
return 2;
|
|
}
|
|
// Тест C-API интроспекции.
|
|
printf("CVER %s\n", fgl_version());
|
|
printf("CNAME %s\n", fgl_prop_name(FGL_TEMPLATE_A, FGL_PROP_FAN_SPEED));
|
|
printf("CCONV %lld\n", static_cast<long long>(
|
|
fgl_convert_to_display(FGL_TEMPLATE_A,
|
|
FGL_PROP_DISPLAY_TEMPERATURE,
|
|
7000)));
|
|
int64_t range_min = 0, range_max = 0;
|
|
int range_ok = fgl_prop_raw_range(FGL_TEMPLATE_A,
|
|
FGL_PROP_ADJUST_TEMPERATURE,
|
|
&range_min, &range_max);
|
|
printf("CRANGE %d %lld %lld\n", range_ok,
|
|
static_cast<long long>(range_min), static_cast<long long>(range_max));
|
|
printf("CRANGE_NONE %d\n",
|
|
fgl_prop_raw_range(FGL_TEMPLATE_A, FGL_PROP_DEVICE_NAME, nullptr,
|
|
nullptr));
|
|
|
|
if (!s->start()) {
|
|
printf("START_FAILED\n");
|
|
return 2;
|
|
}
|
|
|
|
// Начальная синхронизация: пакет GET всех свойств шаблона A (батч).
|
|
if (argc > 8) {
|
|
s->batch_begin();
|
|
for (int i = 8; i < argc; i++) {
|
|
// argv: имена enum'ов ("OperationMode")
|
|
for (uint8_t p = 0; p < static_cast<uint8_t>(Prop::kCount); p++) {
|
|
if (strcmp(fgl::aircon::prop_enum_name(static_cast<Prop>(p)),
|
|
argv[i]) == 0) {
|
|
if (!s->get_prop(static_cast<Prop>(p))) {
|
|
printf("GET_FAIL %s\n", argv[i]);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!s->batch_commit()) printf("BATCH_PARTIAL\n");
|
|
} else {
|
|
// По умолчанию — все int/bool свойства шаблола A одним батчем
|
|
// (строковые пропускаются: модуль-мок их не генерирует; на приборе
|
|
// строковый push проверялся вручную через aircon-события).
|
|
s->batch_begin();
|
|
for (auto* info = fgl::aircon::prop_info_begin(cfg.tmpl);
|
|
info != fgl::aircon::prop_info_begin(cfg.tmpl) +
|
|
fgl::aircon::prop_info_count(cfg.tmpl);
|
|
info++) {
|
|
if (info->kind != fgl::aircon::ValueKind::kString) {
|
|
if (!s->get_prop(info->prop)) {
|
|
printf("GET_FAIL %s\n", fgl::aircon::prop_enum_name(info->prop));
|
|
}
|
|
}
|
|
}
|
|
if (!s->batch_commit()) printf("BATCH_PARTIAL\n");
|
|
}
|
|
|
|
const char* set_prop = getenv("RUNNER_SET_PROP"); // enum-имя
|
|
const char* set_val = getenv("RUNNER_SET_VALUE");
|
|
uint32_t set_at = env_u32("RUNNER_SET_AT", 0);
|
|
if (set_prop != nullptr && set_val != nullptr && set_at > 0) {
|
|
std::this_thread::sleep_for(std::chrono::seconds(set_at));
|
|
for (uint8_t p = 0; p < static_cast<uint8_t>(Prop::kCount); p++) {
|
|
if (strcmp(fgl::aircon::prop_enum_name(static_cast<Prop>(p)),
|
|
set_prop) == 0) {
|
|
bool ok = s->set_int(static_cast<Prop>(p), atoll(set_val));
|
|
printf("SET_DONE %s=%s ok=%d\n", set_prop, set_val, ok ? 1 : 0);
|
|
// Оптимистичный кэш сразу доступен.
|
|
fgl::aircon::Value v{};
|
|
if (s->cached(static_cast<Prop>(p), &v)) {
|
|
printf("CACHED %s %lld\n", set_prop,
|
|
static_cast<long long>(v.i));
|
|
}
|
|
fflush(stdout);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto deadline = std::chrono::steady_clock::now() +
|
|
std::chrono::seconds(duration_sec);
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
}
|
|
|
|
// Проверки отказов (после duration, до stop):
|
|
// - read-only свойство (ErrorCode) — отказ;
|
|
// - свойство не из шаблона A (AfVerticalMoveStep1, только B) — отказ;
|
|
// - клэмпинг: AdjustTemperature=1000 -> raw max 450.
|
|
printf("RO_SET %d\n", s->set_int(Prop::ErrorCode, 5) ? 1 : 0);
|
|
printf("NOTMPL_SET %d\n",
|
|
s->set_int(Prop::AfVerticalMoveStep1, 1) ? 1 : 0);
|
|
printf("CLAMP_SET %d\n", s->set_int(Prop::AdjustTemperature, 1000) ? 1 : 0);
|
|
|
|
s->stop();
|
|
printf("DELETED\n");
|
|
printf("STATS rekeys=%u pushes_ok=%u pushes_bad=%u cmds=%u state=%s\n",
|
|
s->rekey_count(), s->pushes_ok(), s->pushes_bad(),
|
|
s->commands_served(), state_name(s->state()));
|
|
fflush(stdout);
|
|
delete s;
|
|
return 0;
|
|
}
|