Files
petr.polezhaev 97842276c8 core(M4): полировка по нитам финального ревью
- fglair-discover: fchmod 0600 и при перезаписи существующего --out.
- fglctl: #include <cstdlib>; точная проверка обрезки config
  (n == sizeof-1 && !feof); валидация 'monitor <sec>' (endptr/errno).
- docs: отчёт soak — формулировка «ответы на batch GET»; индекс reports.
Ревью M4 независимым агентом: 2 круга, APPROVED.
2026-09-29 01:30:30 +03:00

270 lines
8.6 KiB
C++

// fglctl — CLI для fglair-core (пример использования публичного API).
//
// fglctl <config.json> monitor [сек] — события/состояния/статистика
// fglctl <config.json> get <PropEnum...> — прочитать свойства
// fglctl <config.json> set <PropEnum> <знач> — записать свойство
// fglctl <config.json> status — краткий статус и выход
//
// config.json — формат tools/fglair-discover / config_*.json:
// {"ip_address": "...", "dsn": "...", "lanip_key": "...",
// "lanip_key_id": N, "model": "AP-WC1E", ...}
#include <cerrno>
#include <chrono>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <thread>
#include "fgl-aircon/session.hpp"
#include "fgl-aircon/templates.hpp"
#include "ayla/json.hpp"
using fgl::aircon::Prop;
using fgl::aircon::Session;
using fgl::aircon::State;
using fgl::aircon::Template;
using fgl::aircon::Value;
namespace {
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 "?";
}
void log_sink(int level, const char* msg, size_t len, void*) {
const char* tag = level == 0 ? "D" : level == 1 ? "I" : level == 2 ? "W" : "E";
fprintf(stderr, "[%s] %.*s\n", tag, static_cast<int>(len), msg);
}
void on_state(void* /*ctx*/, State st, fgl::aircon::Error err) {
printf("STATE %s err=%d\n", state_name(st), static_cast<int>(err));
fflush(stdout);
}
void on_property(void* /*ctx*/, 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 = \"%s\"", n, ev.value.s);
} else {
printf("PROP %s = %lld", n, static_cast<long long>(ev.value.i));
if (ev.value.kind == fgl::aircon::ValueKind::kBool) printf(" (bool)");
}
if (ev.cmd_id >= 0) printf(" [cmd_id=%d status=%d]", ev.cmd_id, ev.status);
printf("\n");
fflush(stdout);
}
bool load_config(const char* path, fgl::aircon::Config* cfg,
Template* tmpl) {
FILE* f = fopen(path, "rb");
if (f == nullptr) {
fprintf(stderr, "Не открыть %s\n", path);
return false;
}
static char buf[4096];
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
bool truncated = n == sizeof(buf) - 1 && !feof(f);
fclose(f);
buf[n] = '\0';
if (truncated) {
fprintf(stderr, "ВНИМАНИЕ: config обрезан (> %zu байт)\n",
sizeof(buf) - 1);
}
fgl::ayla::json::Doc doc;
if (!doc.parse(buf)) {
fprintf(stderr, "config не разобран (ожидался JSON-объект)\n");
return false;
}
static char host[64], dsn[48], key[64], model[24];
if (!doc.get_string("ip_address", host, sizeof(host)) ||
!doc.get_string("dsn", dsn, sizeof(dsn)) ||
!doc.get_string("lanip_key", key, sizeof(key))) {
fprintf(stderr,
"config обязан содержать ip_address, dsn, lanip_key, lanip_key_id\n");
return false;
}
int64_t key_id = 0;
if (!doc.get_int("lanip_key_id", &key_id)) {
fprintf(stderr, "config: нет lanip_key_id\n");
return false;
}
*cfg = fgl::aircon::Config{};
cfg->device_port = 80;
cfg->listen_port = 10275;
cfg->keepalive_ms = 15000;
cfg->max_queue = 40;
cfg->host = host;
cfg->dsn = dsn;
cfg->lanip_key = key;
cfg->lanip_key_id = static_cast<uint32_t>(key_id);
// Модель опциональна; неизвестные → A (PROTOCOL §8.1, как legacy).
if (doc.get_string("model", model, sizeof(model))) {
*tmpl = fgl::aircon::template_detect(model);
if (!fgl::aircon::template_is_known(model)) {
fprintf(stderr, "ВНИМАНИЕ: модель %s не распознана, используется "
"шаблон A\n", model);
}
} else {
*tmpl = Template::kA;
}
return true;
}
Prop prop_from_name(const char* name) {
for (uint8_t p = 0; p < static_cast<uint8_t>(Prop::kCount); p++) {
if (strcmp(fgl::aircon::prop_enum_name(static_cast<Prop>(p)), name) == 0) {
return static_cast<Prop>(p);
}
}
return Prop::kCount;
}
int wait_online(Session* s, uint32_t timeout_ms) {
auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(timeout_ms);
while (std::chrono::steady_clock::now() < deadline) {
if (s->state() == State::kOnline) return 0;
if (s->state() == State::kKeyError) return 2;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
return 1;
}
void print_stats(Session* s) {
printf("STATS state=%s rekeys=%u pushes_ok=%u pushes_bad=%u cmds=%u\n",
state_name(s->state()), s->rekey_count(), s->pushes_ok(),
s->pushes_bad(), s->commands_served());
}
} // namespace
int main(int argc, char** argv) {
if (argc < 3) {
fprintf(stderr,
"usage: %s <config.json> monitor [sec] | get <Prop...> | "
"set <Prop> <value> | status\n"
" свойства: имена enum (OperationMode, FanSpeed, "
"AdjustTemperature, ...)\n",
argv[0]);
return 2;
}
fgl::aircon::set_log_sink(log_sink, nullptr);
fgl::aircon::set_log_level(1);
fgl::aircon::Config cfg{};
Template tmpl = Template::kA;
if (!load_config(argv[1], &cfg, &tmpl)) return 2;
cfg.tmpl = tmpl;
fgl::aircon::Callbacks cbs{};
cbs.on_state = on_state;
cbs.on_property = on_property;
Session* s = Session::create(cfg, cbs);
if (s == nullptr) {
fprintf(stderr, "Session::create failed (конфиг?)\n");
return 2;
}
if (!s->start()) {
fprintf(stderr, "start failed (порт занят?)\n");
delete s;
return 2;
}
const char* cmd = argv[2];
int rc = 0;
if (strcmp(cmd, "monitor") == 0) {
int secs = 60;
if (argc > 3) {
char* end = nullptr;
errno = 0;
long v = strtol(argv[3], &end, 10);
if (errno != 0 || end == argv[3] || *end != '\0' || v < 0) {
fprintf(stderr, "Некорректная длительность: %s\n", argv[3]);
rc = 2;
goto cleanup;
}
secs = static_cast<int>(v);
}
if ((rc = wait_online(s, 15000)) == 0) {
s->batch_begin();
for (auto* info = fgl::aircon::prop_info_begin(tmpl);
info != fgl::aircon::prop_info_begin(tmpl) +
fgl::aircon::prop_info_count(tmpl);
info++) {
s->get_prop(info->prop);
}
s->batch_commit();
auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(secs);
while (std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}
} else if (strcmp(cmd, "get") == 0) {
if ((rc = wait_online(s, 15000)) == 0 && argc > 3) {
s->batch_begin();
for (int i = 3; i < argc; i++) {
Prop p = prop_from_name(argv[i]);
if (p == Prop::kCount) {
fprintf(stderr, "Неизвестное свойство: %s\n", argv[i]);
rc = 2;
} else {
s->get_prop(p);
}
}
s->batch_commit();
std::this_thread::sleep_for(std::chrono::seconds(3));
}
} else if (strcmp(cmd, "set") == 0) {
if (argc < 5) {
fprintf(stderr, "set требует <Prop> <value>\n");
rc = 2;
} else if ((rc = wait_online(s, 15000)) == 0) {
Prop p = prop_from_name(argv[3]);
if (p == Prop::kCount) {
fprintf(stderr, "Неизвестное свойство: %s\n", argv[3]);
rc = 2;
} else {
char* end = nullptr;
errno = 0;
int64_t v = strtoll(argv[4], &end, 10);
if (errno != 0 || end == argv[4] || *end != '\0') {
fprintf(stderr, "Некорректное значение: %s\n", argv[4]);
rc = 2;
} else {
bool ok = s->set_int(p, v);
printf("SET %s %lld -> %s\n", argv[3],
static_cast<long long>(v), ok ? "ok" : "REJECTED");
if (!ok) rc = 3;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
}
}
} else if (strcmp(cmd, "status") == 0) {
rc = wait_online(s, 10000);
Value v{};
if (s->cached(Prop::AdjustTemperature, &v)) {
printf("AdjustTemperature = %lld (0.1°C)\n",
static_cast<long long>(v.i));
}
} else {
fprintf(stderr, "Неизвестная команда: %s\n", cmd);
rc = 2;
}
cleanup:
print_stats(s);
s->stop();
delete s;
return rc;
}