// fglctl — CLI для fglair-core (пример использования публичного API). // // fglctl monitor [сек] — события/состояния/статистика // fglctl get — прочитать свойства // fglctl set <знач> — записать свойство // fglctl status — краткий статус и выход // // config.json — формат tools/fglair-discover / config_*.json: // {"ip_address": "...", "dsn": "...", "lanip_key": "...", // "lanip_key_id": N, "model": "AP-WC1E", ...} #include #include #include #include #include #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(len), msg); } void on_state(void* /*ctx*/, State st, fgl::aircon::Error err) { printf("STATE %s err=%d\n", state_name(st), static_cast(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(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 = !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(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(Prop::kCount); p++) { if (strcmp(fgl::aircon::prop_enum_name(static_cast(p)), name) == 0) { return static_cast(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 monitor [sec] | get | " "set | 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 = argc > 3 ? atoi(argv[3]) : 60; 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 требует \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(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(v.i)); } } else { fprintf(stderr, "Неизвестная команда: %s\n", cmd); rc = 2; } print_stats(s); s->stop(); delete s; return rc; }