diff --git a/Makefile b/Makefile index 26f2c20..f357b72 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Makefile for Ballu AC ESP32-C6 Controller -.PHONY: all build test clean test-uart test-midea-protocol test-zigbee-zcl test-integration-layer help +.PHONY: all build test clean test-uart test-midea-protocol test-zigbee-zcl test-integration-layer test-status-monitor help # Directories SRC_DIR := src @@ -31,7 +31,7 @@ $(BUILD_DIR)/app: $(SRC_FILES) $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) # Build and run tests -test: test-uart test-midea-protocol test-zigbee-zcl test-integration-layer +test: test-uart test-midea-protocol test-zigbee-zcl test-integration-layer test-status-monitor @echo "All tests passed!" # Build individual test executables @@ -51,6 +51,10 @@ $(TEST_DIR)/integration_layer_test: $(TEST_DIR)/integration_layer_test.c $(SRC_F @mkdir -p $(BUILD_DIR) $(CC) $(CFLAGS) $< $(SRC_FILES) $(UNITY_SRC) -o $@ $(LDFLAGS) +$(TEST_DIR)/status_monitor_test: $(TEST_DIR)/status_monitor_test.c $(SRC_FILES) $(UNITY_SRC) + @mkdir -p $(BUILD_DIR) + $(CC) $(CFLAGS) $< $(SRC_FILES) $(UNITY_SRC) -o $@ $(LDFLAGS) + # Run individual tests test-uart: $(TEST_DIR)/uart_driver_test @echo "Running UART driver tests..." @@ -68,6 +72,10 @@ test-integration-layer: $(TEST_DIR)/integration_layer_test @echo "Running integration layer tests..." @./$(TEST_DIR)/integration_layer_test +test-status-monitor: $(TEST_DIR)/status_monitor_test + @echo "Running status monitor tests..." + @./$(TEST_DIR)/status_monitor_test + # Clean build artifacts clean: rm -rf $(BUILD_DIR) diff --git a/Readme.md b/Readme.md index b1fab5e..57f59f9 100644 --- a/Readme.md +++ b/Readme.md @@ -11,7 +11,7 @@ This project implements a bridge between Zigbee (Home Assistant) and UART-based - UART communication at 9600 baud 8N1 for MideaUART protocol - Zigbee ZCL Thermostat cluster implementation - Bidirectional communication between Zigbee and UART layers -- Status monitoring and error handling +- Status monitoring with periodic AC polling, fault detection, and a communication watchdog - Configurable timing controls (50ms command spacing) ## Directory Structure @@ -22,6 +22,7 @@ src/ midea_protocol.c/h - MideaUART protocol encoding/decoding zigbee_zcl.c/h - Zigbee ZCL Thermostat cluster integration_layer.c/h- Integration between Zigbee and UART layers + status_monitor.c/h - AC status polling, fault detection, and comms watchdog main.c - Application entry point test/ @@ -29,6 +30,7 @@ test/ midea_protocol_test.c - Unit tests for MideaUART protocol zigbee_zcl_test.c - Unit tests for Zigbee ZCL integration_layer_test.c - Unit tests for integration layer + status_monitor_test.c - Unit tests for status monitoring docs/ protocol.md - MideaUART protocol details @@ -62,6 +64,23 @@ make upload make test ``` +## Status Monitoring + +The `status_monitor` module (see `src/status_monitor.c/h`) provides: + +- Periodic AC status polling: sends a MideaUART status-request frame over the UART + driver, receives the response, and decodes it into a `midea_status_t`. +- ZCL mapping: decoded status is mapped to ZCL Thermostat attributes + (`local_temperature`, `system_mode`) for Home Assistant reporting. +- Fault detection: any non-zero `error_code` or `alarm_mask` from the AC raises a + fault flag (`status_monitor_has_fault`) and records the fault code. +- Communication watchdog: tracks the time of the last successful poll and trips a + timeout when the poll interval (plus per-attempt timeout) is exceeded or when + `max_retries` consecutive attempts fail. + +Configuration (`status_monitor_config_t`) exposes `poll_interval_ms` (default +5000ms), `timeout_ms` (default 1000ms), and `max_retries` (default 3). + ## Implementation Progress See `docs/plans/2026-07-05-ballu-ac-esp32c6-controller-implementation.md` for detailed implementation plan and progress tracking. diff --git a/docs/plans/2026-07-05-ballu-ac-esp32c6-controller-implementation.md b/docs/plans/2026-07-05-ballu-ac-esp32c6-controller-implementation.md index 8206386..c4f2c0c 100644 --- a/docs/plans/2026-07-05-ballu-ac-esp32c6-controller-implementation.md +++ b/docs/plans/2026-07-05-ballu-ac-esp32c6-controller-implementation.md @@ -62,13 +62,13 @@ Implementation of ESP32-C6 based AC controller that bridges Zigbee (Home Assista - [x] Update Readme.md ### Task 5: Status Monitoring and Feedback -- [ ] Implement AC status polling via UART (temperature, mode, etc.) -- [ ] Map MideaUART status to ZCL attributes for reporting -- [ ] Implement error handling and fault detection -- [ ] Add watchdog for communication timeouts -- [ ] Write unit tests for status monitoring and error paths -- [ ] Run tests - must pass before next task -- [ ] Update Readme.md +- [x] Implement AC status polling via UART (temperature, mode, etc.) +- [x] Map MideaUART status to ZCL attributes for reporting +- [x] Implement error handling and fault detection +- [x] Add watchdog for communication timeouts +- [x] Write unit tests for status monitoring and error paths +- [x] Run tests - must pass before next task +- [x] Update Readme.md ### Task 6: End-to-End Integration and Testing - [ ] Integrate all layers: Zigbee ←→ Integration ←→ UART diff --git a/src/status_monitor.c b/src/status_monitor.c new file mode 100644 index 0000000..9808960 --- /dev/null +++ b/src/status_monitor.c @@ -0,0 +1,259 @@ +#include "status_monitor.h" +#include "uart_driver.h" +#include "midea_protocol.h" +#include + +// Default configuration values +#define DEFAULT_POLL_INTERVAL_MS 5000 +#define DEFAULT_TIMEOUT_MS 1000 +#define DEFAULT_MAX_RETRIES 3 + +// Maximum expected response frame size +#define STATUS_RESPONSE_MAX 32 + +bool status_monitor_init(status_monitor_t *monitor, const status_monitor_config_t *config) { + if (!monitor) { + return false; + } + + memset(monitor, 0, sizeof(status_monitor_t)); + + monitor->initialized = true; + monitor->last_poll_success = false; + monitor->last_poll_time = 0; + monitor->last_attempt_time = 0; + monitor->retry_count = 0; + monitor->error_count = 0; + monitor->comm_timeout = false; + monitor->fault_detected = false; + monitor->fault_code = 0; + + // Apply configuration with fallback to defaults + if (config) { + monitor->config.poll_interval_ms = + config->poll_interval_ms ? config->poll_interval_ms : DEFAULT_POLL_INTERVAL_MS; + monitor->config.timeout_ms = + config->timeout_ms ? config->timeout_ms : DEFAULT_TIMEOUT_MS; + monitor->config.max_retries = + config->max_retries ? config->max_retries : DEFAULT_MAX_RETRIES; + } else { + monitor->config.poll_interval_ms = DEFAULT_POLL_INTERVAL_MS; + monitor->config.timeout_ms = DEFAULT_TIMEOUT_MS; + monitor->config.max_retries = DEFAULT_MAX_RETRIES; + } + + return true; +} + +void status_monitor_deinit(status_monitor_t *monitor) { + if (monitor) { + memset(monitor, 0, sizeof(status_monitor_t)); + monitor->initialized = false; + } +} + +size_t status_monitor_build_request(uint8_t *buffer, size_t buffer_size) { + if (!buffer || buffer_size < 5) { + return 0; + } + + // MideaUART status-request frame: + // [Header 0xAA 0x55][Length 1][Command 0x07 = query][Checksum] + size_t offset = 0; + buffer[offset++] = 0xAA; + buffer[offset++] = 0x55; + buffer[offset++] = 1; // length of command payload + buffer[offset++] = 0x07; // query/status-request command + + // Checksum: XOR of length + command bytes + uint8_t checksum = 0; + for (size_t i = 2; i < offset; i++) { + checksum ^= buffer[i]; + } + buffer[offset++] = checksum; + + return offset; +} + +// Internal: apply a freshly decoded status to monitor state. +static void status_monitor_apply_status(status_monitor_t *monitor, + const midea_status_t *status, + uint32_t current_time) { + monitor->last_poll_success = true; + monitor->last_poll_time = current_time; + monitor->retry_count = 0; + monitor->comm_timeout = false; + + memcpy(&monitor->last_status, status, sizeof(midea_status_t)); + + // Fault detection: any error code or alarm bit indicates a fault. + if (status->error_code != 0 || status->alarm_mask != 0) { + monitor->fault_detected = true; + monitor->fault_code = + (uint16_t)(status->error_code) | status->alarm_mask; + } else { + monitor->fault_detected = false; + monitor->fault_code = 0; + } + + // Keep ZCL attributes in sync for reporting. + status_monitor_map_to_zcl(status, &monitor->last_zcl_attrs); +} + +bool status_monitor_process_response(status_monitor_t *monitor, + const uint8_t *buffer, size_t length, + uint32_t current_time, midea_status_t *status) { + if (!monitor || !monitor->initialized || !status) { + return false; + } + + monitor->last_attempt_time = current_time; + + midea_status_t decoded; + memset(&decoded, 0, sizeof(decoded)); + + if (!buffer || length == 0 || !midea_protocol_decode(buffer, length, &decoded)) { + // Decode failed -> treat as a communication error. + status_monitor_handle_error(monitor); + return false; + } + + status_monitor_apply_status(monitor, &decoded, current_time); + memcpy(status, &decoded, sizeof(midea_status_t)); + return true; +} + +bool status_monitor_poll(status_monitor_t *monitor, uart_driver_t *uart, + uint32_t current_time, midea_status_t *status) { + if (!monitor || !monitor->initialized || !uart || !status) { + return false; + } + + monitor->last_attempt_time = current_time; + + // Build and send a status-request frame. + uint8_t request[8]; + size_t request_len = status_monitor_build_request(request, sizeof(request)); + if (request_len == 0 || + !uart_driver_send(uart, request, request_len, monitor->config.timeout_ms)) { + status_monitor_handle_error(monitor); + return false; + } + + // Receive the response. + uint8_t response[STATUS_RESPONSE_MAX]; + int received = uart_driver_receive(uart, response, sizeof(response), + monitor->config.timeout_ms); + if (received <= 0) { + // No data / error -> communication failure. + status_monitor_handle_error(monitor); + return false; + } + + return status_monitor_process_response(monitor, response, (size_t)received, + current_time, status); +} + +void status_monitor_map_to_zcl(const midea_status_t *status, zcl_thermostat_attrs_t *zcl_attrs) { + if (!status || !zcl_attrs) { + return; + } + + // Map MideaUART status to ZCL Thermostat attributes + zcl_attrs->local_temperature = status->indoor_temp; + + // Map power state and mode to ZCL system_mode + if (status->power_state == 0x00) { + // AC is OFF + zcl_attrs->system_mode = 0x00; // Off + } else { + // AC is ON, map the mode + switch (status->mode) { + case MODE_COOL: + zcl_attrs->system_mode = 0x03; // Cooling + break; + case MODE_HEAT: + zcl_attrs->system_mode = 0x04; // Heating + break; + case MODE_AUTO: + zcl_attrs->system_mode = 0x01; // Auto + break; + case MODE_DRY: + zcl_attrs->system_mode = 0x08; // Dry + break; + case MODE_FAN: + zcl_attrs->system_mode = 0x00; // Off (treat fan only as off for HVAC) + break; + default: + zcl_attrs->system_mode = 0x00; // Default to Off + break; + } + } + + // Set control sequence (simplified) + zcl_attrs->control_sequence = 0x00; // Not used in basic implementation +} + +bool status_monitor_check_timeout(const status_monitor_t *monitor, uint32_t current_time) { + if (!monitor || !monitor->initialized) { + return true; // Treat uninitialized monitor as timed out + } + + // Never successfully polled -> considered timed out. + if (!monitor->last_poll_success) { + return true; + } + + // Watchdog window: one poll interval plus the per-attempt timeout allowance. + uint32_t window = monitor->config.poll_interval_ms + monitor->config.timeout_ms; + + // Guard against clock going backwards. + if (current_time < monitor->last_poll_time) { + return false; + } + + return (current_time - monitor->last_poll_time) > window; +} + +void status_monitor_handle_error(status_monitor_t *monitor) { + if (!monitor) { + return; + } + + monitor->last_poll_success = false; + monitor->error_count++; + + if (monitor->retry_count < 0xFF) { + monitor->retry_count++; + } + + // Trip the watchdog once retries are exhausted. + if (monitor->retry_count >= monitor->config.max_retries) { + monitor->comm_timeout = true; + } +} + +bool status_monitor_has_fault(const status_monitor_t *monitor) { + if (!monitor) { + return false; + } + return monitor->fault_detected; +} + +bool status_monitor_get_last_status(const status_monitor_t *monitor, midea_status_t *status) { + if (!monitor || !status) { + return false; + } + + memcpy(status, &monitor->last_status, sizeof(midea_status_t)); + return true; +} + +bool status_monitor_get_last_zcl_attrs(const status_monitor_t *monitor, zcl_thermostat_attrs_t *zcl_attrs) { + if (!monitor || !zcl_attrs) { + return false; + } + + memcpy(zcl_attrs, &monitor->last_zcl_attrs, sizeof(zcl_thermostat_attrs_t)); + return true; +} diff --git a/src/status_monitor.h b/src/status_monitor.h new file mode 100644 index 0000000..2970d82 --- /dev/null +++ b/src/status_monitor.h @@ -0,0 +1,77 @@ +#ifndef STATUS_MONITOR_H +#define STATUS_MONITOR_H + +#include +#include +#include "midea_protocol.h" +#include "zigbee_zcl.h" +#include "uart_driver.h" + +// Status monitoring configuration +typedef struct { + uint32_t poll_interval_ms; // How often to poll for status (default: 5000ms) + uint32_t timeout_ms; // Communication timeout per attempt (default: 1000ms) + uint8_t max_retries; // Maximum retry attempts before comm failure (default: 3) +} status_monitor_config_t; + +// Status monitoring state +typedef struct { + bool initialized; + bool last_poll_success; + uint32_t last_poll_time; // Timestamp (ms) of last SUCCESSFUL poll + uint32_t last_attempt_time; // Timestamp (ms) of last poll attempt + uint8_t retry_count; // Consecutive failed attempts + uint32_t error_count; // Total accumulated communication errors + bool comm_timeout; // Watchdog tripped (retries exhausted / no response) + bool fault_detected; // AC reported an error_code or alarm_mask + uint16_t fault_code; // Last fault value (error_code | alarm_mask bits) + status_monitor_config_t config; + midea_status_t last_status; + zcl_thermostat_attrs_t last_zcl_attrs; +} status_monitor_t; + +// Initialize status monitor. If config is NULL, defaults are applied. +bool status_monitor_init(status_monitor_t *monitor, const status_monitor_config_t *config); + +// Deinitialize status monitor +void status_monitor_deinit(status_monitor_t *monitor); + +// Poll AC status via UART: sends a status request, receives and decodes the +// response, then updates internal state (fault detection, watchdog). +// current_time is the current monotonic time in milliseconds. +// Returns true on a successful poll, false on communication/decode failure. +bool status_monitor_poll(status_monitor_t *monitor, uart_driver_t *uart, + uint32_t current_time, midea_status_t *status); + +// Process a raw UART response buffer (decode + fault detection + state update). +// Exposed separately so it can be exercised without live hardware. +// Returns true if the buffer decoded into a valid status. +bool status_monitor_process_response(status_monitor_t *monitor, + const uint8_t *buffer, size_t length, + uint32_t current_time, midea_status_t *status); + +// Map MideaUART status to ZCL attributes +void status_monitor_map_to_zcl(const midea_status_t *status, zcl_thermostat_attrs_t *zcl_attrs); + +// Watchdog: returns true if the time since the last successful poll exceeds the +// allowed window (poll_interval_ms + timeout_ms), or if the monitor has never +// polled successfully. +bool status_monitor_check_timeout(const status_monitor_t *monitor, uint32_t current_time); + +// Handle a communication error: increments retry/error counters and trips the +// watchdog once max_retries is reached. +void status_monitor_handle_error(status_monitor_t *monitor); + +// Returns true if the AC currently reports a fault. +bool status_monitor_has_fault(const status_monitor_t *monitor); + +// Get last known status. Returns false on bad args. +bool status_monitor_get_last_status(const status_monitor_t *monitor, midea_status_t *status); + +// Get last known ZCL attributes. Returns false on bad args. +bool status_monitor_get_last_zcl_attrs(const status_monitor_t *monitor, zcl_thermostat_attrs_t *zcl_attrs); + +// Build a MideaUART status-request frame into buffer. Returns bytes written (0 on error). +size_t status_monitor_build_request(uint8_t *buffer, size_t buffer_size); + +#endif // STATUS_MONITOR_H diff --git a/test/integration_layer_test b/test/integration_layer_test index 89ac21f..2dbe7c4 100755 Binary files a/test/integration_layer_test and b/test/integration_layer_test differ diff --git a/test/status_monitor_test b/test/status_monitor_test new file mode 100755 index 0000000..8398bfd Binary files /dev/null and b/test/status_monitor_test differ diff --git a/test/status_monitor_test.c b/test/status_monitor_test.c new file mode 100644 index 0000000..4cb1093 --- /dev/null +++ b/test/status_monitor_test.c @@ -0,0 +1,333 @@ +#include "unity.h" +#include "status_monitor.h" +#include "midea_protocol.h" +#include "uart_driver.h" +#include + +void setUp(void) {} +void tearDown(void) {} + +// Build a valid MideaUART status response frame for testing. +// data[0..7] follow the decoder layout: +// d0: (mode & 0x0F) | (power << 4) +// d1,d2: indoor_temp (LE) +// d3,d4: target_temp (LE) +// d5: (fan & 0x0F) | (error << 4) +// d6: alarm low, d7: alarm high +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; +} + +void test_status_monitor_init_defaults(void) { + status_monitor_t m; + TEST_ASSERT_TRUE(status_monitor_init(&m, NULL)); + TEST_ASSERT_TRUE(m.initialized); + TEST_ASSERT_EQUAL_UINT32(5000, m.config.poll_interval_ms); + TEST_ASSERT_EQUAL_UINT32(1000, m.config.timeout_ms); + TEST_ASSERT_EQUAL_UINT8(3, m.config.max_retries); + TEST_ASSERT_FALSE(m.last_poll_success); + TEST_ASSERT_FALSE(m.fault_detected); +} + +void test_status_monitor_init_custom_config(void) { + status_monitor_config_t cfg = { .poll_interval_ms = 2000, .timeout_ms = 500, .max_retries = 5 }; + status_monitor_t m; + TEST_ASSERT_TRUE(status_monitor_init(&m, &cfg)); + TEST_ASSERT_EQUAL_UINT32(2000, m.config.poll_interval_ms); + TEST_ASSERT_EQUAL_UINT32(500, m.config.timeout_ms); + TEST_ASSERT_EQUAL_UINT8(5, m.config.max_retries); +} + +void test_status_monitor_init_null(void) { + TEST_ASSERT_FALSE(status_monitor_init(NULL, NULL)); +} + +void test_build_request_frame(void) { + uint8_t req[8]; + size_t len = status_monitor_build_request(req, sizeof(req)); + TEST_ASSERT_EQUAL_UINT(5, len); + TEST_ASSERT_EQUAL_UINT8(0xAA, req[0]); + TEST_ASSERT_EQUAL_UINT8(0x55, req[1]); + TEST_ASSERT_EQUAL_UINT8(0x07, req[3]); + // checksum = len ^ cmd = 1 ^ 0x07 + TEST_ASSERT_EQUAL_UINT8((uint8_t)(1 ^ 0x07), req[4]); +} + +void test_build_request_buffer_too_small(void) { + uint8_t req[3]; + TEST_ASSERT_EQUAL_UINT(0, status_monitor_build_request(req, sizeof(req))); + TEST_ASSERT_EQUAL_UINT(0, status_monitor_build_request(NULL, 8)); +} + +void test_process_response_success(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); + + uint8_t frame[16]; + size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 0, 0); + + midea_status_t status; + TEST_ASSERT_TRUE(status_monitor_process_response(&m, frame, len, 1000, &status)); + TEST_ASSERT_EQUAL_UINT8(MODE_COOL, status.mode); + TEST_ASSERT_EQUAL_INT16(2300, status.indoor_temp); + TEST_ASSERT_EQUAL_INT16(2400, status.target_temp); + TEST_ASSERT_TRUE(m.last_poll_success); + TEST_ASSERT_EQUAL_UINT32(1000, m.last_poll_time); + TEST_ASSERT_EQUAL_UINT8(0, m.retry_count); + TEST_ASSERT_FALSE(m.fault_detected); +} + +void test_process_response_decode_failure(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); + + // Bad header -> decode fails -> counts as error. + uint8_t bad[12] = { 0x00, 0x00, 8, 0x07, 0, 0, 0, 0, 0, 0, 0, 0 }; + midea_status_t status; + TEST_ASSERT_FALSE(status_monitor_process_response(&m, bad, sizeof(bad), 500, &status)); + TEST_ASSERT_FALSE(m.last_poll_success); + TEST_ASSERT_EQUAL_UINT8(1, m.retry_count); + TEST_ASSERT_EQUAL_UINT32(1, m.error_count); +} + +void test_process_response_null_buffer(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); + midea_status_t status; + TEST_ASSERT_FALSE(status_monitor_process_response(&m, NULL, 0, 100, &status)); + TEST_ASSERT_EQUAL_UINT8(1, m.retry_count); +} + +void test_fault_detection_error_code(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); + + uint8_t frame[16]; + size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 3, 0); + midea_status_t status; + TEST_ASSERT_TRUE(status_monitor_process_response(&m, frame, len, 1000, &status)); + TEST_ASSERT_EQUAL_UINT8(3, status.error_code); + TEST_ASSERT_TRUE(status_monitor_has_fault(&m)); + TEST_ASSERT_TRUE(m.fault_code != 0); +} + +void test_fault_detection_alarm_mask(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); + + uint8_t frame[16]; + size_t len = build_status_frame(frame, MODE_HEAT, 1, 2000, 2100, 1, 0, 0x0004); + midea_status_t status; + TEST_ASSERT_TRUE(status_monitor_process_response(&m, frame, len, 1000, &status)); + TEST_ASSERT_EQUAL_UINT16(0x0004, status.alarm_mask); + TEST_ASSERT_TRUE(status_monitor_has_fault(&m)); +} + +void test_fault_cleared_on_good_status(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); + + uint8_t frame[16]; + // First a faulty status. + size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 5, 0); + midea_status_t status; + status_monitor_process_response(&m, frame, len, 1000, &status); + TEST_ASSERT_TRUE(status_monitor_has_fault(&m)); + + // Then a clean status clears the fault. + len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 0, 0); + status_monitor_process_response(&m, frame, len, 2000, &status); + TEST_ASSERT_FALSE(status_monitor_has_fault(&m)); + TEST_ASSERT_EQUAL_UINT16(0, m.fault_code); +} + +void test_handle_error_trips_watchdog(void) { + status_monitor_config_t cfg = { .poll_interval_ms = 5000, .timeout_ms = 1000, .max_retries = 3 }; + status_monitor_t m; + status_monitor_init(&m, &cfg); + + status_monitor_handle_error(&m); + TEST_ASSERT_EQUAL_UINT8(1, m.retry_count); + TEST_ASSERT_FALSE(m.comm_timeout); + + status_monitor_handle_error(&m); + TEST_ASSERT_EQUAL_UINT8(2, m.retry_count); + TEST_ASSERT_FALSE(m.comm_timeout); + + status_monitor_handle_error(&m); + TEST_ASSERT_EQUAL_UINT8(3, m.retry_count); + TEST_ASSERT_TRUE(m.comm_timeout); + TEST_ASSERT_EQUAL_UINT32(3, m.error_count); +} + +void test_check_timeout_never_polled(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); + TEST_ASSERT_TRUE(status_monitor_check_timeout(&m, 0)); + TEST_ASSERT_TRUE(status_monitor_check_timeout(&m, 100000)); +} + +void test_check_timeout_within_window(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); // window = 5000 + 1000 = 6000 + + uint8_t frame[16]; + size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 0, 0); + midea_status_t status; + status_monitor_process_response(&m, frame, len, 1000, &status); + + // 1000 -> 5000 : within 6000ms window + TEST_ASSERT_FALSE(status_monitor_check_timeout(&m, 5000)); +} + +void test_check_timeout_exceeded(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); // window = 6000 + + uint8_t frame[16]; + size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 0, 0); + midea_status_t status; + status_monitor_process_response(&m, frame, len, 1000, &status); + + // 1000 -> 8000 = 7000ms elapsed > 6000ms window + TEST_ASSERT_TRUE(status_monitor_check_timeout(&m, 8000)); +} + +void test_check_timeout_uninitialized(void) { + status_monitor_t m; + memset(&m, 0, sizeof(m)); + TEST_ASSERT_TRUE(status_monitor_check_timeout(&m, 100)); + TEST_ASSERT_TRUE(status_monitor_check_timeout(NULL, 100)); +} + +void test_poll_uart_no_data(void) { + // The mock UART receive returns 0 bytes -> poll should fail and count error. + status_monitor_t m; + status_monitor_init(&m, NULL); + + uart_config_t ucfg = { .tx_pin = 1, .rx_pin = 2, .baud_rate = 9600, + .data_bits = 8, .parity = 0, .stop_bits = 1 }; + uart_driver_t uart; + TEST_ASSERT_TRUE(uart_driver_init(&uart, &ucfg)); + + midea_status_t status; + TEST_ASSERT_FALSE(status_monitor_poll(&m, &uart, 1000, &status)); + TEST_ASSERT_FALSE(m.last_poll_success); + TEST_ASSERT_EQUAL_UINT8(1, m.retry_count); + TEST_ASSERT_EQUAL_UINT32(1000, m.last_attempt_time); +} + +void test_poll_null_args(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); + midea_status_t status; + TEST_ASSERT_FALSE(status_monitor_poll(&m, NULL, 0, &status)); + TEST_ASSERT_FALSE(status_monitor_poll(NULL, NULL, 0, &status)); +} + +void test_map_to_zcl_modes(void) { + midea_status_t s; + zcl_thermostat_attrs_t z; + memset(&s, 0, sizeof(s)); + + // Power off -> Off regardless of mode + s.power_state = 0; s.mode = MODE_COOL; s.indoor_temp = 2500; + status_monitor_map_to_zcl(&s, &z); + TEST_ASSERT_EQUAL_UINT8(0x00, z.system_mode); + TEST_ASSERT_EQUAL_INT16(2500, z.local_temperature); + + s.power_state = 1; + s.mode = MODE_COOL; status_monitor_map_to_zcl(&s, &z); + TEST_ASSERT_EQUAL_UINT8(0x03, z.system_mode); + s.mode = MODE_HEAT; status_monitor_map_to_zcl(&s, &z); + TEST_ASSERT_EQUAL_UINT8(0x04, z.system_mode); + s.mode = MODE_AUTO; status_monitor_map_to_zcl(&s, &z); + TEST_ASSERT_EQUAL_UINT8(0x01, z.system_mode); + s.mode = MODE_DRY; status_monitor_map_to_zcl(&s, &z); + TEST_ASSERT_EQUAL_UINT8(0x08, z.system_mode); +} + +void test_get_last_status_and_zcl(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); + + uint8_t frame[16]; + size_t len = build_status_frame(frame, MODE_HEAT, 1, 2100, 2200, 3, 0, 0); + midea_status_t status; + status_monitor_process_response(&m, frame, len, 1000, &status); + + midea_status_t got; + TEST_ASSERT_TRUE(status_monitor_get_last_status(&m, &got)); + TEST_ASSERT_EQUAL_UINT8(MODE_HEAT, got.mode); + TEST_ASSERT_EQUAL_INT16(2100, got.indoor_temp); + + zcl_thermostat_attrs_t z; + TEST_ASSERT_TRUE(status_monitor_get_last_zcl_attrs(&m, &z)); + TEST_ASSERT_EQUAL_UINT8(0x04, z.system_mode); + TEST_ASSERT_EQUAL_INT16(2100, z.local_temperature); + + TEST_ASSERT_FALSE(status_monitor_get_last_status(&m, NULL)); + TEST_ASSERT_FALSE(status_monitor_get_last_status(NULL, &got)); +} + +void test_successful_poll_resets_retry(void) { + status_monitor_t m; + status_monitor_init(&m, NULL); + + // Accumulate errors + status_monitor_handle_error(&m); + status_monitor_handle_error(&m); + TEST_ASSERT_EQUAL_UINT8(2, m.retry_count); + + // A good response resets retry_count and clears comm_timeout + uint8_t frame[16]; + size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 0, 0); + midea_status_t status; + TEST_ASSERT_TRUE(status_monitor_process_response(&m, frame, len, 3000, &status)); + TEST_ASSERT_EQUAL_UINT8(0, m.retry_count); + TEST_ASSERT_FALSE(m.comm_timeout); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_status_monitor_init_defaults); + RUN_TEST(test_status_monitor_init_custom_config); + RUN_TEST(test_status_monitor_init_null); + RUN_TEST(test_build_request_frame); + RUN_TEST(test_build_request_buffer_too_small); + RUN_TEST(test_process_response_success); + RUN_TEST(test_process_response_decode_failure); + RUN_TEST(test_process_response_null_buffer); + RUN_TEST(test_fault_detection_error_code); + RUN_TEST(test_fault_detection_alarm_mask); + RUN_TEST(test_fault_cleared_on_good_status); + RUN_TEST(test_handle_error_trips_watchdog); + RUN_TEST(test_check_timeout_never_polled); + RUN_TEST(test_check_timeout_within_window); + RUN_TEST(test_check_timeout_exceeded); + RUN_TEST(test_check_timeout_uninitialized); + RUN_TEST(test_poll_uart_no_data); + RUN_TEST(test_poll_null_args); + RUN_TEST(test_map_to_zcl_modes); + RUN_TEST(test_get_last_status_and_zcl); + RUN_TEST(test_successful_poll_resets_retry); + return UNITY_END(); +} diff --git a/test/uart_driver_test b/test/uart_driver_test index 56dc5e5..2e90a53 100755 Binary files a/test/uart_driver_test and b/test/uart_driver_test differ diff --git a/test/zigbee_zcl_test b/test/zigbee_zcl_test index 3906661..811fe61 100755 Binary files a/test/zigbee_zcl_test and b/test/zigbee_zcl_test differ diff --git a/unity b/unity new file mode 160000 index 0000000..b706271 --- /dev/null +++ b/unity @@ -0,0 +1 @@ +Subproject commit b706271f3255e33a0e5ec068844462c5fdb5c527