- 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.
270 lines
8.6 KiB
C++
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;
|
|
}
|