#include "unity.h" #include "app_controller.h" #include "midea_protocol.h" #include "zigbee_zcl.h" #include // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- // Build a valid MideaUART status response frame (same layout the decoder and // status_monitor tests expect). static size_t build_status_frame(uint8_t *buf, uint8_t mode, uint8_t power, int16_t indoor, int16_t target, uint8_t fan, uint8_t error, uint16_t alarm) { size_t o = 0; buf[o++] = 0xAA; buf[o++] = 0x55; buf[o++] = 8; // length buf[o++] = 0x07; // status command buf[o++] = (mode & 0x0F) | ((power & 0x01) << 4); buf[o++] = indoor & 0xFF; buf[o++] = (indoor >> 8) & 0xFF; buf[o++] = target & 0xFF; buf[o++] = (target >> 8) & 0xFF; buf[o++] = (fan & 0x0F) | ((error & 0x0F) << 4); buf[o++] = alarm & 0xFF; buf[o++] = (alarm >> 8) & 0xFF; uint8_t chk = 0; for (size_t i = 2; i < o; i++) chk ^= buf[i]; buf[o++] = chk; return o; } static app_controller_t app; void setUp(void) { memset(&app, 0, sizeof(app)); } void tearDown(void) { app_controller_deinit(&app); } // ------------------------------------------------------------------------- // Init / deinit // ------------------------------------------------------------------------- void test_app_init_defaults(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); TEST_ASSERT_TRUE(app.initialized); TEST_ASSERT_TRUE(uart_driver_is_initialized(&app.uart)); TEST_ASSERT_TRUE(app.integration.initialized); TEST_ASSERT_TRUE(app.status_monitor.initialized); // Defaults: 9600 baud, 50ms spacing. TEST_ASSERT_EQUAL_INT(9600, app.config.uart_config.baud_rate); TEST_ASSERT_EQUAL_UINT32(50, app.integration.command_spacing_ms); } void test_app_init_null(void) { TEST_ASSERT_FALSE(app_controller_init(NULL, NULL)); } void test_app_init_custom_config(void) { app_controller_config_t cfg; memset(&cfg, 0, sizeof(cfg)); cfg.uart_config.baud_rate = 9600; cfg.uart_config.data_bits = 8; cfg.uart_config.parity = 0; cfg.uart_config.stop_bits = 1; cfg.status_config.poll_interval_ms = 2000; cfg.status_config.timeout_ms = 500; cfg.status_config.max_retries = 2; cfg.command_spacing_ms = 75; TEST_ASSERT_TRUE(app_controller_init(&app, &cfg)); TEST_ASSERT_EQUAL_UINT32(2000, app.status_monitor.config.poll_interval_ms); TEST_ASSERT_EQUAL_UINT8(2, app.status_monitor.config.max_retries); TEST_ASSERT_EQUAL_UINT32(75, app.integration.command_spacing_ms); } void test_app_deinit(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); app_controller_deinit(&app); TEST_ASSERT_FALSE(app.initialized); TEST_ASSERT_FALSE(uart_driver_is_initialized(&app.uart)); } // ------------------------------------------------------------------------- // Command path: HA -> Zigbee -> UART -> AC // ------------------------------------------------------------------------- void test_command_path_produces_uart_frame(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); // Zigbee setpoint command: [mode=Cool(3), temp_lsb, temp_msb] -> 2500 (25.00C) uint8_t payload[] = {0x03, (uint8_t)(2500 & 0xFF), (uint8_t)((2500 >> 8) & 0xFF)}; uint8_t uart_buf[64]; size_t uart_len = sizeof(uart_buf); bool ok = app_controller_process_zigbee_command(&app, 1, 0x0201, 0x02, payload, sizeof(payload), uart_buf, &uart_len); TEST_ASSERT_TRUE(ok); TEST_ASSERT_GREATER_THAN(0, uart_len); TEST_ASSERT_EQUAL_UINT8(0xAA, uart_buf[0]); TEST_ASSERT_EQUAL_UINT8(0x55, uart_buf[1]); TEST_ASSERT_EQUAL_UINT32(1, app.commands_sent); // The encoded frame must decode back to the requested cool @ 25.00C. midea_status_t decoded; TEST_ASSERT_TRUE(midea_protocol_decode(uart_buf, uart_len, &decoded)); TEST_ASSERT_EQUAL_INT16(2500, decoded.target_temp); TEST_ASSERT_EQUAL_UINT8(MODE_COOL, decoded.mode); } void test_command_path_rejects_when_uninitialized(void) { // Not initialized (setUp zeroed it). uint8_t payload[] = {0x03, 0x00, 0x00}; uint8_t uart_buf[64]; size_t uart_len = sizeof(uart_buf); bool ok = app_controller_process_zigbee_command(&app, 1, 0x0201, 0x02, payload, sizeof(payload), uart_buf, &uart_len); TEST_ASSERT_FALSE(ok); TEST_ASSERT_EQUAL_UINT32(0, uart_len); } // ------------------------------------------------------------------------- // Feedback path: AC -> Status -> Zigbee -> HA // ------------------------------------------------------------------------- void test_feedback_path_updates_zigbee_attrs(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); // Simulated AC status: cooling, on, indoor 24.00C, target 22.00C. uint8_t frame[32]; size_t len = build_status_frame(frame, MODE_COOL, 1, 2400, 2200, 3, 0, 0); zcl_thermostat_attrs_t attrs; bool ok = app_controller_process_ac_status(&app, frame, len, 1000, &attrs); TEST_ASSERT_TRUE(ok); TEST_ASSERT_EQUAL_INT16(2400, attrs.local_temperature); TEST_ASSERT_EQUAL_UINT8(0x03, attrs.system_mode); // Cooling // Verify the ZCL cluster (what HA reads) was actually updated. TEST_ASSERT_EQUAL_INT16(2400, zigbee_zcl_get_local_temperature()); TEST_ASSERT_EQUAL_UINT8(0x03, zigbee_zcl_get_system_mode()); TEST_ASSERT_EQUAL_UINT32(1, app.status_updates); } void test_feedback_path_rejects_bad_frame(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); uint8_t junk[] = {0x00, 0x11, 0x22, 0x33}; bool ok = app_controller_process_ac_status(&app, junk, sizeof(junk), 1000, NULL); TEST_ASSERT_FALSE(ok); TEST_ASSERT_EQUAL_UINT32(0, app.status_updates); } void test_feedback_path_detects_fault(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); // error code set -> fault. uint8_t frame[32]; size_t len = build_status_frame(frame, MODE_COOL, 1, 2400, 2200, 3, 0x5, 0); TEST_ASSERT_TRUE(app_controller_process_ac_status(&app, frame, len, 1000, NULL)); TEST_ASSERT_TRUE(status_monitor_has_fault(&app.status_monitor)); TEST_ASSERT_FALSE(app_controller_is_healthy(&app, 1000)); } // ------------------------------------------------------------------------- // Full end-to-end round trip: HA -> ... -> AC -> ... -> HA // ------------------------------------------------------------------------- void test_end_to_end_round_trip(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); // 1. HA requests Heat @ 21.00C. uint8_t payload[] = {0x04, (uint8_t)(2100 & 0xFF), (uint8_t)((2100 >> 8) & 0xFF)}; uint8_t uart_buf[64]; size_t uart_len = sizeof(uart_buf); TEST_ASSERT_TRUE(app_controller_process_zigbee_command(&app, 1, 0x0201, 0x02, payload, sizeof(payload), uart_buf, &uart_len)); TEST_ASSERT_GREATER_THAN(0, uart_len); // 2. AC responds with a status frame reflecting the new state. // (heating mode maps to MODE_HEAT, indoor 20.50C, target 21.00C) uint8_t status_frame[32]; size_t status_len = build_status_frame(status_frame, MODE_HEAT, 1, 2050, 2100, 2, 0, 0); // 3. Feedback flows back to the Zigbee cluster. zcl_thermostat_attrs_t attrs; TEST_ASSERT_TRUE(app_controller_process_ac_status(&app, status_frame, status_len, 2000, &attrs)); TEST_ASSERT_EQUAL_INT16(2050, zigbee_zcl_get_local_temperature()); TEST_ASSERT_EQUAL_UINT8(0x04, zigbee_zcl_get_system_mode()); // Heating TEST_ASSERT_TRUE(app_controller_is_healthy(&app, 2000)); } // ------------------------------------------------------------------------- // Timing / load // ------------------------------------------------------------------------- void test_timing_under_load(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); // Fire a burst of commands; every one must encode cleanly and be counted. const int N = 100; for (int i = 0; i < N; i++) { int16_t temp = (int16_t)(1600 + (i % 16) * 100); // 16.00C .. 31.00C uint8_t mode = (i % 2) ? 0x03 : 0x04; // alternate cool/heat uint8_t payload[] = {mode, (uint8_t)(temp & 0xFF), (uint8_t)((temp >> 8) & 0xFF)}; uint8_t uart_buf[64]; size_t uart_len = sizeof(uart_buf); bool ok = app_controller_process_zigbee_command(&app, 1, 0x0201, 0x02, payload, sizeof(payload), uart_buf, &uart_len); TEST_ASSERT_TRUE(ok); TEST_ASSERT_GREATER_THAN(0, uart_len); midea_status_t decoded; TEST_ASSERT_TRUE(midea_protocol_decode(uart_buf, uart_len, &decoded)); TEST_ASSERT_EQUAL_INT16(temp, decoded.target_temp); } TEST_ASSERT_EQUAL_UINT32((uint32_t)N, app.commands_sent); // Rate-limit spacing constraint preserved throughout. TEST_ASSERT_EQUAL_UINT32(50, app.integration.command_spacing_ms); } // ------------------------------------------------------------------------- // Reset / recovery // ------------------------------------------------------------------------- void test_reset_restores_state(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); // Dirty some state. uint8_t frame[32]; size_t len = build_status_frame(frame, MODE_COOL, 1, 2400, 2200, 3, 0x5, 0); app_controller_process_ac_status(&app, frame, len, 1000, NULL); TEST_ASSERT_TRUE(status_monitor_has_fault(&app.status_monitor)); // Reset clears the fault and re-initializes subsystems. TEST_ASSERT_TRUE(app_controller_reset(&app)); TEST_ASSERT_TRUE(app.initialized); TEST_ASSERT_FALSE(status_monitor_has_fault(&app.status_monitor)); TEST_ASSERT_TRUE(uart_driver_is_initialized(&app.uart)); TEST_ASSERT_TRUE(app.integration.initialized); } void test_recovery_on_comm_timeout(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); // Force the watchdog to trip by exhausting retries. for (int i = 0; i < 5; i++) { status_monitor_handle_error(&app.status_monitor); } TEST_ASSERT_TRUE(app.status_monitor.comm_timeout); bool recovered = app_controller_recover_if_needed(&app, 100000); TEST_ASSERT_TRUE(recovered); TEST_ASSERT_EQUAL_UINT32(1, app.recovery_count); TEST_ASSERT_FALSE(app.status_monitor.comm_timeout); // cleared by reset } void test_no_recovery_at_startup(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); // Fresh init, never polled, no errors -> must not churn recovery. bool recovered = app_controller_recover_if_needed(&app, 100000); TEST_ASSERT_FALSE(recovered); TEST_ASSERT_EQUAL_UINT32(0, app.recovery_count); } void test_healthy_after_good_poll(void) { TEST_ASSERT_TRUE(app_controller_init(&app, NULL)); uint8_t frame[32]; size_t len = build_status_frame(frame, MODE_AUTO, 1, 2300, 2300, 2, 0, 0); TEST_ASSERT_TRUE(app_controller_process_ac_status(&app, frame, len, 1000, NULL)); // Within the watchdog window immediately after a good status. TEST_ASSERT_TRUE(app_controller_is_healthy(&app, 1000)); } // ------------------------------------------------------------------------- int main(void) { UNITY_BEGIN(); RUN_TEST(test_app_init_defaults); RUN_TEST(test_app_init_null); RUN_TEST(test_app_init_custom_config); RUN_TEST(test_app_deinit); RUN_TEST(test_command_path_produces_uart_frame); RUN_TEST(test_command_path_rejects_when_uninitialized); RUN_TEST(test_feedback_path_updates_zigbee_attrs); RUN_TEST(test_feedback_path_rejects_bad_frame); RUN_TEST(test_feedback_path_detects_fault); RUN_TEST(test_end_to_end_round_trip); RUN_TEST(test_timing_under_load); RUN_TEST(test_reset_restores_state); RUN_TEST(test_recovery_on_comm_timeout); RUN_TEST(test_no_recovery_at_startup); RUN_TEST(test_healthy_after_good_poll); return UNITY_END(); }