- include/fgl-aircon: types.hpp (Prop/State/Error/Value/Template/ Conversion-linear-custom/PropOverride/Config с задокументированными lifetime-контрактами), templates.hpp (PropInfo-таблицы, интроспекция, конверсии c контрактом Linear num/den!=0), session.hpp (Session: set_int/bool/string, get_prop, batch, cached, set_log_sink с контрактом), c_api.h (extern "C" для cffi/pyfglair). - src/aircon: tables.cpp (шаблоны A=33/B=20/F=34 по PROTOCOL §8.2, template_detect + template_is_known; конверсии: default тождественно, DisplayTemperature raw 0.01°C(+5000)→API 0.1°C; linear с инверсией raw=(api-offset)*den/num и насыщением __builtin_*_overflow; custom fn; af_*_swing base_type integer, kind bool), session.cpp (маппинг имя↔Prop, коэрсинг int-push к kBool по таблице, кэш со спинлоком, оптимистичный кэш, валидация overrides: терминатор/linear/custom_fn), c_api.cpp (шейм, static_assert'ы на все enum-значения, fgl_template_detect -1 для неизвестных). - ayla: set_property_string (Command.str_value, coalescing копирует строку в т.ч. в batch-ветке); kMaxQueue 48 + static_assert (полный батч A=33); commit_batch всегда ставит notify; oversized-строка дропает событие (кэш не затирается); max_queue default 40. - tests/aircon: test_tables (составы/атрибуты/дубли/template_detect/ swings-integer), test_convert (default/linear/custom/диапазоны/насыщение в обе стороны/UB-экстремумы), aircon_runner + test_aircon_mock (маппинг+конверсии+batch=1-notify с ожиданием async REG; SET+кэш+ RO/не-шаблон отказы+клэмпинг raw 450 на модуле; C-API smoke). - Прибор AP-WC1E: полный батч шаблона A одним notify — 28 int/bool свойств (DisplayTemperature 7000→200=20.0°C, DeviceCapabilities=5119), boolean SET (JSON true) принят, RO/клэмп/NOTMPL отказы корректны. - CI: 10/10 ×3 стабильно (3 полных прогона); ESP-IDF esp32 build complete. Ревью независимым агентом: 3 круга — B1 kMaxQueue<батча, B2 linear div/0, B3 c_api контракт -1, B4 batch-строки, BL1 INT64_MIN negation UB, BL2 DisplayTemperature экстремумы, flake aircon_mock — всё закрыто; APPROVED (условие круга 3: фикс + зелёный CI ×3).
168 lines
6.1 KiB
C++
168 lines
6.1 KiB
C++
// Публичные типы fgl-aircon (уровень aircon: свойства, конверсии, конфиг).
|
|
// Протокольные детали скрыты в src/ayla; этот заголовок — стабильный API
|
|
// для интеграций (ESPHome, HA/pyfglair, CLI).
|
|
#pragma once
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
|
|
namespace fgl::aircon {
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Состояние сессии (зеркалирует ayla-слой).
|
|
// ---------------------------------------------------------------------------
|
|
enum class State : uint8_t {
|
|
kIdle = 0, // создан, не запущен
|
|
kRegistering, // local_reg отправлен, ждём key exchange
|
|
kOnline, // сессия активна
|
|
kRecovering, // самолечение (тишина → re-key)
|
|
kOffline, // модуль недоступен (backoff) или нет слотов
|
|
kKeyError, // lanip_key_id не совпал — смена конфига вручную
|
|
};
|
|
|
|
enum class Error : int {
|
|
kNone = 0,
|
|
kNoSlot = 1,
|
|
kUnreachable = 2,
|
|
kKeyMismatch = 3,
|
|
kBadKeyExchange = 4,
|
|
kActivationTimeout = 5,
|
|
kDecryptFailed = 6,
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Свойства (шаблоны A/B/F; PROTOCOL.md §8.2)
|
|
// ---------------------------------------------------------------------------
|
|
enum class Prop : uint8_t {
|
|
OperationMode = 0,
|
|
FanSpeed,
|
|
AdjustTemperature,
|
|
DisplayTemperature,
|
|
AfVerticalDirection,
|
|
AfVerticalSwing,
|
|
AfHorizontalDirection,
|
|
AfHorizontalSwing,
|
|
AfVerticalMoveStep1,
|
|
AfHorizontalMoveStep1,
|
|
OutdoorLowNoise,
|
|
IndoorFanControl,
|
|
HumanDetAutoSave,
|
|
MinHeat,
|
|
PowerfulMode,
|
|
CoilDryMode,
|
|
EconomyMode,
|
|
MasterTimerOnOff1,
|
|
MasterTimerOnOff2,
|
|
ErrorCode,
|
|
DemandControl,
|
|
FilterSignResetDisplay,
|
|
FilterSignReset,
|
|
OpStatus,
|
|
DeviceName,
|
|
BuildingName,
|
|
WifiLedEnable,
|
|
ServiceContactName,
|
|
ServiceContactPhone,
|
|
ServiceContactEmail,
|
|
AfHorizontalNumDir,
|
|
AfVerticalNumDir,
|
|
DeviceCapabilities,
|
|
GetProp,
|
|
HumanDet,
|
|
Monitor1,
|
|
Refresh,
|
|
kCount
|
|
};
|
|
|
|
// Тип значения свойства.
|
|
enum class ValueKind : uint8_t { kInt = 0, kBool, kString };
|
|
|
|
struct Value {
|
|
ValueKind kind = ValueKind::kInt;
|
|
int64_t i = 0; // kInt / kBool (0|1)
|
|
char s[64] = {}; // kString
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Шаблоны устройств (oem_model → шаблон, PROTOCOL.md §8.1)
|
|
// ---------------------------------------------------------------------------
|
|
enum class Template : uint8_t { kA = 0, kB, kF };
|
|
|
|
// Определение шаблона по oem_model ("AP-WC1E" → kA). Неизвестные модели
|
|
// трактуются как kA (как и legacy) — различить можно template_is_known.
|
|
Template template_detect(const char* oem_model);
|
|
// Известна ли модель (false → template_detect вернёт kA как fallback).
|
|
bool template_is_known(const char* oem_model);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Конверсии (PROTOCOL.md §8.3; требования плана §4)
|
|
// ---------------------------------------------------------------------------
|
|
enum class ConvKind : uint8_t {
|
|
kTemplateDefault = 0, // из таблицы шаблона
|
|
kLinear, // disp = raw*num/den + offset (int64, округление к нулю)
|
|
kCustomFn, // disp = fn(raw, ctx)
|
|
};
|
|
|
|
struct Linear {
|
|
int64_t num = 1;
|
|
int64_t den = 1;
|
|
int64_t offset = 0;
|
|
// to_display: api = raw*num/den + offset
|
|
// from_input: raw = (api - offset)*den/num (инверсия)
|
|
};
|
|
|
|
using ConvFn = int64_t (*)(int64_t raw, void* ctx);
|
|
|
|
struct Conversion {
|
|
ConvKind kind = ConvKind::kTemplateDefault;
|
|
Linear linear{}; // при kLinear
|
|
ConvFn fn = nullptr; // при kCustomFn
|
|
void* ctx = nullptr; // контекст kCustomFn
|
|
};
|
|
|
|
// Точечная настройка одного свойства (nullptr-terminnated массив в конфиге).
|
|
struct PropOverride {
|
|
Prop prop;
|
|
bool has_range = false;
|
|
int64_t min = 0; // в API-единицах (после конверсии)
|
|
int64_t max = 0;
|
|
Conversion to_display; // raw -> API
|
|
Conversion from_input; // API -> raw
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Конфигурация и колбэки сессии
|
|
// ---------------------------------------------------------------------------
|
|
struct Config {
|
|
// host/dsn/lanip_key копируются при create; overrides — нет (см. ниже).
|
|
const char* host = nullptr; // DNS-имя или IP модуля
|
|
uint16_t device_port = 80;
|
|
const char* dsn = nullptr; // "AC000W00XXXXXXX"
|
|
const char* lanip_key = nullptr; // base64-строка как есть
|
|
uint32_t lanip_key_id = 0;
|
|
Template tmpl = Template::kA;
|
|
uint16_t listen_port = 10275;
|
|
uint32_t keepalive_ms = 15000;
|
|
uint8_t max_queue = 40; // >= полного батча шаблона (33)
|
|
// Переопределения конверсий. МАССИВ ДОЛЖЕН ПЕРЕЖИТЬ СЕССИЮ (не копируется;
|
|
// читается при каждом set/push). Терминатор: элемент с prop == kCount.
|
|
const PropOverride* overrides = nullptr;
|
|
};
|
|
|
|
struct PropertyEvent {
|
|
Prop prop;
|
|
Value value; // уже сконвертированное (to_display)
|
|
int cmd_id = -1; // ответ на GET; -1 — спонтанное обновление
|
|
int status = 0;
|
|
};
|
|
|
|
struct Callbacks {
|
|
// КОНТРАКТ: вызываются из потоков ядра; быстрые и реентерабельные;
|
|
// вызывать stop() из колбэка запрещено.
|
|
void (*on_state)(void* ctx, State st, Error err);
|
|
void (*on_property)(void* ctx, const PropertyEvent& ev);
|
|
void* ctx = nullptr;
|
|
};
|
|
|
|
} // namespace fgl::aircon
|