core(M3): aircon-слой — шаблоны A/B/F, конверсии+override, публичный API + C-API
- 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).
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// Тесты таблиц шаблонов: состав против PROTOCOL.md §8.2, типы, RO, диапазоны.
|
||||
#include "doctest/doctest.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "fgl-aircon/templates.hpp"
|
||||
|
||||
using fgl::aircon::Prop;
|
||||
using fgl::aircon::prop_info_count;
|
||||
using fgl::aircon::prop_info_find;
|
||||
using fgl::aircon::prop_info_find_by_name;
|
||||
using fgl::aircon::Template;
|
||||
using fgl::aircon::template_detect;
|
||||
|
||||
namespace {
|
||||
|
||||
// Список имён свойств шаблона (для сверки с протоколом).
|
||||
std::string names_joined(Template t) {
|
||||
std::string out;
|
||||
for (auto* p = fgl::aircon::prop_info_begin(t);
|
||||
p != fgl::aircon::prop_info_begin(t) + prop_info_count(t); p++) {
|
||||
if (!out.empty()) out += ",";
|
||||
out += p->name;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool has(Template t, const char* name) {
|
||||
return prop_info_find_by_name(t, name) != nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Шаблон A: состав против PROTOCOL §8.2 (все 33)") {
|
||||
const char* expected[] = {
|
||||
"operation_mode", "fan_speed", "adjust_temperature",
|
||||
"af_vertical_direction", "af_vertical_swing", "af_horizontal_direction",
|
||||
"af_horizontal_swing", "outdoor_low_noise", "indoor_fan_control",
|
||||
"human_det_auto_save", "min_heat", "powerful_mode", "coil_dry_mode",
|
||||
"economy_mode", "master_timer_on_off_1", "master_timer_on_off_2",
|
||||
"error_code", "demand_control", "filter_sign_reset_display", "op_status",
|
||||
"device_name", "building_name", "wifi_led_enable",
|
||||
"service_contact_name", "service_contact_phone", "service_contact_email",
|
||||
"af_horizontal_num_dir", "af_vertical_num_dir", "device_capabilities",
|
||||
"display_temperature", "get_prop", "human_det", "refresh"};
|
||||
for (const char* n : expected) {
|
||||
CAPTURE(n);
|
||||
CHECK(has(Template::kA, n));
|
||||
}
|
||||
CHECK(prop_info_count(Template::kA) == 33);
|
||||
}
|
||||
|
||||
TEST_CASE("Шаблон B: состав (20) и отличия от A") {
|
||||
CHECK(prop_info_count(Template::kB) == 20);
|
||||
CHECK(has(Template::kB, "af_vertical_move_step1"));
|
||||
CHECK(has(Template::kB, "af_horizontal_move_step1"));
|
||||
CHECK_FALSE(has(Template::kB, "af_vertical_direction"));
|
||||
CHECK_FALSE(has(Template::kB, "display_temperature"));
|
||||
CHECK_FALSE(has(Template::kB, "powerful_mode"));
|
||||
CHECK_FALSE(has(Template::kB, "min_heat"));
|
||||
}
|
||||
|
||||
TEST_CASE("Шаблон F: = A + monitor1 + filter_sign_reset (без display-варианта)") {
|
||||
CHECK(prop_info_count(Template::kF) == 34);
|
||||
CHECK(has(Template::kF, "monitor1"));
|
||||
CHECK(has(Template::kF, "filter_sign_reset"));
|
||||
CHECK_FALSE(has(Template::kF, "filter_sign_reset_display"));
|
||||
CHECK(has(Template::kF, "display_temperature"));
|
||||
}
|
||||
|
||||
TEST_CASE("template_detect: oem_model → шаблон (PROTOCOL §8.1)") {
|
||||
CHECK(template_detect("AP-WC1E") == Template::kA);
|
||||
CHECK(template_detect("AP-WA3E") == Template::kA);
|
||||
CHECK(template_detect("AP-WB2E") == Template::kB);
|
||||
CHECK(template_detect("AP-WF4E") == Template::kF);
|
||||
CHECK(template_detect("UNKNOWN-MODEL") == Template::kA); // как legacy
|
||||
CHECK(template_detect(nullptr) == Template::kA);
|
||||
}
|
||||
|
||||
TEST_CASE("Атрибуты свойств: base_type/read_only/диапазоны") {
|
||||
auto* om = prop_info_find_by_name(Template::kA, "operation_mode");
|
||||
REQUIRE(om != nullptr);
|
||||
CHECK(std::string(om->base_type) == "integer");
|
||||
CHECK_FALSE(om->read_only);
|
||||
CHECK(om->raw_min == 0);
|
||||
CHECK(om->raw_max == 6);
|
||||
|
||||
auto* err = prop_info_find_by_name(Template::kA, "error_code");
|
||||
REQUIRE(err != nullptr);
|
||||
CHECK(err->read_only);
|
||||
CHECK(err->raw_max == 4095);
|
||||
|
||||
auto* eco = prop_info_find_by_name(Template::kA, "economy_mode");
|
||||
REQUIRE(eco != nullptr);
|
||||
CHECK(std::string(eco->base_type) == "boolean");
|
||||
CHECK(eco->kind == fgl::aircon::ValueKind::kBool);
|
||||
|
||||
auto* dn = prop_info_find_by_name(Template::kA, "device_name");
|
||||
REQUIRE(dn != nullptr);
|
||||
CHECK(dn->kind == fgl::aircon::ValueKind::kString);
|
||||
CHECK(std::string(dn->base_type) == "string");
|
||||
|
||||
// prop_info_find по enum согласован с find_by_name.
|
||||
auto* by_enum = prop_info_find(Template::kA, Prop::FanSpeed);
|
||||
REQUIRE(by_enum != nullptr);
|
||||
CHECK(std::string(by_enum->name) == "fan_speed");
|
||||
CHECK(prop_info_find(Template::kB, Prop::AfVerticalDirection) == nullptr);
|
||||
CHECK(prop_info_find(Template::kA, Prop::kCount) == nullptr);
|
||||
CHECK(prop_info_find_by_name(Template::kA, "no_such") == nullptr);
|
||||
CHECK(prop_info_find_by_name(Template::kA, nullptr) == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("Таблицы: без дублей имен и enum'ов внутри шаблона") {
|
||||
for (Template t : {Template::kA, Template::kB, Template::kF}) {
|
||||
size_t n = prop_info_count(t);
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
for (size_t j = i + 1; j < n; j++) {
|
||||
auto* a = fgl::aircon::prop_info_begin(t) + i;
|
||||
auto* b = fgl::aircon::prop_info_begin(t) + j;
|
||||
CHECK_FALSE(std::string(a->name) == std::string(b->name));
|
||||
CHECK_FALSE(a->prop == b->prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("af_*_swing: base_type integer (PROTOCOL §8.3), kind bool в API") {
|
||||
auto* vs = prop_info_find_by_name(Template::kA, "af_vertical_swing");
|
||||
REQUIRE(vs != nullptr);
|
||||
CHECK(std::string(vs->base_type) == "integer");
|
||||
CHECK(vs->kind == fgl::aircon::ValueKind::kBool);
|
||||
}
|
||||
Reference in New Issue
Block a user