├── components └── sharp_ac │ ├── __init__.py │ ├── comp_climate.h │ ├── comp_ion_switch.h │ ├── comp_reconnect_button.h │ ├── comp_connection_status.h │ ├── core_messages.h │ ├── core_types.h │ ├── core_state.h │ ├── core_frame.h │ ├── comp_vane_horizontal.h │ ├── comp_vane_vertical.h │ ├── core_logic.h │ ├── climate.py │ ├── comp_hardware.h │ ├── core_frame.cpp │ ├── comp_hardware.cpp │ └── core_logic.cpp ├── docs └── cn13.png ├── .vscode └── settings.json ├── .gitignore ├── tests ├── test_mocks.cpp ├── Makefile.test ├── run_tests.sh ├── test_frame_parsing.cpp ├── test_core_logic.cpp └── test_integration.cpp ├── klima.yaml └── README.md /components/sharp_ac/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/sharp_ac/comp_climate.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "comp_hardware.h" -------------------------------------------------------------------------------- /docs/cn13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sven819/SharpClimateUART-ESPHome/HEAD/docs/cn13.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "array": "cpp", 4 | "deque": "cpp", 5 | "initializer_list": "cpp", 6 | "list": "cpp", 7 | "queue": "cpp", 8 | "random": "cpp", 9 | "regex": "cpp", 10 | "type_traits": "cpp", 11 | "vector": "cpp", 12 | "xhash": "cpp", 13 | "xstring": "cpp", 14 | "xtree": "cpp", 15 | "xutility": "cpp" 16 | } 17 | } -------------------------------------------------------------------------------- /components/sharp_ac/comp_ion_switch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/components/switch/switch.h" 4 | #include "comp_climate.h" 5 | 6 | namespace esphome 7 | { 8 | namespace sharp_ac 9 | { 10 | 11 | class IonSwitch : public switch_::Switch, public Parented 12 | { 13 | 14 | protected: 15 | void write_state(bool state) override 16 | { 17 | parent_->setIon(state); 18 | }; 19 | }; 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /components/sharp_ac/comp_reconnect_button.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/components/button/button.h" 4 | #include "esphome/core/component.h" 5 | 6 | namespace esphome 7 | { 8 | namespace sharp_ac 9 | { 10 | class SharpAc; 11 | 12 | class ReconnectButton : public button::Button, public Component 13 | { 14 | public: 15 | void set_parent(SharpAc *parent) { this->parent_ = parent; } 16 | 17 | protected: 18 | void press_action() override; 19 | SharpAc *parent_{nullptr}; 20 | }; 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore settings for ESPHome 2 | # Gitignore settings for ESPHome 3 | /.esphome/ 4 | /secrets.yaml 5 | /logs/ 6 | **/__pycache__/ 7 | **/.DS_Store 8 | testdev.yaml 9 | schlafzimmer.yaml 10 | wohnzimmer.yaml 11 | ota.yaml 12 | 13 | # C++ Unit Test artifacts 14 | tests/*.o 15 | tests/test_frame_parsing 16 | tests/test_core_logic 17 | tests/test_integration 18 | wohnzimmer.yaml 19 | ota.yaml 20 | **/node_modules/ 21 | 22 | # C++ Unit Test artifacts 23 | tests/*.o 24 | tests/test_frame_parsing 25 | tests/test_core_logic 26 | tests/test_integration 27 | -------------------------------------------------------------------------------- /components/sharp_ac/comp_connection_status.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/components/text_sensor/text_sensor.h" 4 | #include "esphome/core/component.h" 5 | 6 | namespace esphome 7 | { 8 | namespace sharp_ac 9 | { 10 | class SharpAc; 11 | 12 | class ConnectionStatusSensor : public text_sensor::TextSensor, public Component 13 | { 14 | public: 15 | void set_parent(SharpAc *parent) { this->parent_ = parent; } 16 | 17 | void publish_status(const std::string &status) 18 | { 19 | if (this->state != status) 20 | { 21 | this->publish_state(status); 22 | } 23 | } 24 | 25 | protected: 26 | SharpAc *parent_{nullptr}; 27 | }; 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /components/sharp_ac/core_messages.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | const uint8_t ACK[] = {0x06}; 5 | 6 | const uint8_t init_msg[] = {0x02, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00}; 7 | const uint8_t init_msg2[] = {0x02, 0xff, 0xff, 0x01, 0x01, 0x00, 0x01, 0x00}; 8 | const uint8_t connected_msg[] = {0x03, 0x05, 0xB0, 0x00, 0x10, 0x00, 0x00}; 9 | const uint8_t dissconnect[] = {0x03, 0x05, 0xB0, 0x00, 0x01, 0x00, 0x00}; 10 | const uint8_t subscribe_msg[] = {0x03, 0xFF, 0xA0, 0x01, 0x00, 0x00, 0x00}; // Status Subscribe ? 11 | const uint8_t subscribe_msg2[] = {0x03, 0xFE, 0xA0, 0x01, 0x00, 0x00, 0x00}; // Status Subscribe ? 12 | const uint8_t get_state[] = {0xdd, 0x02, 0xfc, 0x62}; 13 | const uint8_t get_status[] = {0xdd, 0x02, 0xfd, 0x62}; 14 | -------------------------------------------------------------------------------- /tests/test_mocks.cpp: -------------------------------------------------------------------------------- 1 | #ifdef TEST_BUILD 2 | 3 | #include 4 | #include 5 | 6 | namespace esphome { 7 | 8 | // Mock Log implementation 9 | class Logger { 10 | public: 11 | static void log(const char* tag, const char* format, ...) { 12 | va_list args; 13 | va_start(args, format); 14 | printf("[%s] ", tag); 15 | vprintf(format, args); 16 | printf("\n"); 17 | va_end(args); 18 | } 19 | }; 20 | 21 | } // namespace esphome 22 | 23 | // Mock log macros 24 | #define ESP_LOGD(tag, ...) esphome::Logger::log(tag, __VA_ARGS__) 25 | #define ESP_LOGI(tag, ...) esphome::Logger::log(tag, __VA_ARGS__) 26 | #define ESP_LOGW(tag, ...) esphome::Logger::log(tag, __VA_ARGS__) 27 | #define ESP_LOGE(tag, ...) esphome::Logger::log(tag, __VA_ARGS__) 28 | 29 | #endif // TEST_BUILD 30 | -------------------------------------------------------------------------------- /components/sharp_ac/core_types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | const int IonMode = 0x80; 4 | 5 | enum class PowerMode 6 | { 7 | heat = 0x1, 8 | cool = 0x2, 9 | dry = 0x3, 10 | fan = 0x4 11 | }; 12 | 13 | enum class Preset 14 | { 15 | NONE = 0x0, 16 | ECO = 0x1, 17 | FULLPOWER = 0x2 18 | }; 19 | 20 | enum class FanMode 21 | { 22 | low = 0x4, 23 | mid = 0x3, 24 | high = 0x5, 25 | highest = 0x7, 26 | auto_fan = 0x2 27 | }; 28 | 29 | enum class SwingVertical 30 | { 31 | swing = 0xF, 32 | auto_position = 0x8, 33 | highest = 0x9, 34 | high = 0xA, 35 | mid = 0xB, 36 | low = 0xC, 37 | lowest = 0xD, 38 | }; 39 | 40 | enum class SwingHorizontal 41 | { 42 | swing = 0xF, 43 | middle = 0x1, 44 | right = 0x2, 45 | left = 0x3, 46 | }; -------------------------------------------------------------------------------- /klima.yaml: -------------------------------------------------------------------------------- 1 | external_components: 2 | - source: components 3 | refresh: 0s 4 | 5 | esphome: 6 | name: klima_wohnzimmer 7 | 8 | esp8266: 9 | board: esp01_1m 10 | 11 | api: 12 | ota: 13 | platform: esphome 14 | web_server: 15 | port: 80 16 | 17 | wifi: 18 | ssid: !secret wifi_ssid 19 | password: !secret wifi_password 20 | 21 | 22 | logger: 23 | baud_rate: 0 # disable serial logging if you're using the standard TX/RX pins for your serial peripheral 24 | level: DEBUG 25 | 26 | uart: 27 | tx_pin: 1 # hardware dependant 28 | rx_pin: 3 # hardware dependant 29 | baud_rate: 9600 30 | parity: EVEN 31 | 32 | climate: 33 | - platform: sharp_ac 34 | id: hvac 35 | name: "Living Room AC" 36 | horizontal_vane_select: 37 | name: "Horizontal Vane" 38 | vertical_vane_select: 39 | name: "Vertikal Vane" 40 | ion_switch: 41 | name: Plasmacluster 42 | connection_status: 43 | name: "AC Connection Status" 44 | reconnect_button: 45 | name: "AC Reconnect" -------------------------------------------------------------------------------- /components/sharp_ac/core_state.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "core_types.h" 3 | #include "core_frame.h" 4 | 5 | class SharpState 6 | { 7 | public: 8 | bool state; 9 | PowerMode mode; 10 | FanMode fan; 11 | SwingHorizontal swingH; 12 | SwingVertical swingV; 13 | int temperature; 14 | bool ion; 15 | Preset preset; 16 | 17 | SharpCommandFrame toFrame() 18 | { 19 | SharpCommandFrame frame; 20 | frame.setData(this); 21 | return frame; 22 | } 23 | 24 | SharpState() : state(false), mode(PowerMode::fan), fan(FanMode::low), swingH(SwingHorizontal::middle), swingV(SwingVertical::mid), temperature(25), ion(false), preset(Preset::NONE) {} 25 | 26 | SharpState(const SharpState &other) 27 | { 28 | state = other.state; 29 | mode = other.mode; 30 | fan = other.fan; 31 | temperature = other.temperature; 32 | swingH = other.swingH; 33 | swingV = other.swingV; 34 | ion = other.ion; 35 | preset = other.preset; 36 | } 37 | }; -------------------------------------------------------------------------------- /components/sharp_ac/core_frame.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "core_types.h" 6 | 7 | class SharpState; 8 | 9 | class SharpFrame 10 | { 11 | protected: 12 | uint8_t *data; 13 | size_t size; 14 | 15 | public: 16 | SharpFrame(); 17 | SharpFrame(char c); 18 | SharpFrame(const uint8_t *arr, size_t sz); 19 | ~SharpFrame(); 20 | SharpFrame(const SharpFrame &other); 21 | uint8_t *getData() const; 22 | size_t getSize() const; 23 | int setSize(size_t sz); 24 | void print(); 25 | virtual void setChecksum(); 26 | bool validateChecksum(); 27 | uint8_t calcChecksum(); 28 | }; 29 | 30 | class SharpStatusFrame : public SharpFrame 31 | { 32 | public: 33 | SharpStatusFrame(const uint8_t *arr); 34 | int getTemperature(); 35 | }; 36 | 37 | class SharpModeFrame : public SharpFrame 38 | { 39 | public: 40 | SharpModeFrame(const uint8_t *arr); 41 | int getTemperature(); 42 | bool getState(); 43 | FanMode getFanMode(); 44 | PowerMode getPowerMode(); 45 | SwingVertical getSwingVertical(); 46 | SwingHorizontal getSwingHorizontal(); 47 | Preset getPreset(); 48 | bool getIon(); 49 | }; 50 | 51 | class SharpCommandFrame : public SharpFrame 52 | { 53 | public: 54 | SharpCommandFrame(); 55 | void setData(SharpState *state); 56 | void setChecksum() override; 57 | 58 | private: 59 | void commandChecksum(); 60 | }; 61 | 62 | class SharpACKFrame : public SharpFrame 63 | { 64 | public: 65 | SharpACKFrame(); 66 | void setChecksum() override; 67 | }; -------------------------------------------------------------------------------- /components/sharp_ac/comp_vane_horizontal.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/components/select/select.h" 4 | #include "comp_climate.h" 5 | 6 | namespace esphome 7 | { 8 | namespace sharp_ac 9 | { 10 | 11 | class VaneSelectHorizontal : public select::Select, public Parented 12 | { 13 | public: 14 | VaneSelectHorizontal() = default; 15 | 16 | void setVal(SwingHorizontal val) 17 | { 18 | switch (val) 19 | { 20 | 21 | case SwingHorizontal::swing: 22 | this->publish_state("swing"); 23 | break; 24 | case SwingHorizontal::left: 25 | this->publish_state("left"); 26 | break; 27 | case SwingHorizontal::middle: 28 | this->publish_state("center"); 29 | break; 30 | case SwingHorizontal::right: 31 | this->publish_state("right"); 32 | break; 33 | } 34 | } 35 | void control(const std::string &value) override 36 | { 37 | SwingHorizontal pos = SwingHorizontal::middle; 38 | if (value == "swing") 39 | { 40 | pos = SwingHorizontal::swing; 41 | } 42 | else if (value == "left") 43 | { 44 | pos = SwingHorizontal::left; 45 | } 46 | else if (value == "center") 47 | { 48 | pos = SwingHorizontal::middle; 49 | } 50 | else if (value == "right") 51 | { 52 | pos = SwingHorizontal::right; 53 | } 54 | this->parent_->setVaneHorizontal(pos); 55 | }; 56 | 57 | protected: 58 | private: 59 | }; 60 | 61 | } 62 | } -------------------------------------------------------------------------------- /components/sharp_ac/comp_vane_vertical.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/components/select/select.h" 4 | #include "comp_climate.h" 5 | 6 | namespace esphome 7 | { 8 | namespace sharp_ac 9 | { 10 | 11 | class VaneSelectVertical : public select::Select, public Parented 12 | { 13 | public: 14 | VaneSelectVertical() = default; 15 | void control(const std::string &value) override 16 | { 17 | SwingVertical pos = SwingVertical::auto_position; 18 | if (value == "auto") 19 | { 20 | pos = SwingVertical::auto_position; 21 | } 22 | else if (value == "swing") 23 | { 24 | pos = SwingVertical::swing; 25 | } 26 | else if (value == "up") 27 | { 28 | pos = SwingVertical::highest; 29 | } 30 | else if (value == "up_center") 31 | { 32 | pos = SwingVertical::high; 33 | } 34 | else if (value == "center") 35 | { 36 | pos = SwingVertical::mid; 37 | } 38 | else if (value == "down_center") 39 | { 40 | pos = SwingVertical::low; 41 | } 42 | else if (value == "down") 43 | { 44 | pos = SwingVertical::lowest; 45 | } 46 | this->parent_->setVaneVertical(pos); 47 | }; 48 | void setVal(SwingVertical val) 49 | { 50 | switch (val) 51 | { 52 | case SwingVertical::auto_position: 53 | this->publish_state("auto"); 54 | break; 55 | case SwingVertical::swing: 56 | this->publish_state("swing"); 57 | break; 58 | case SwingVertical::highest: 59 | this->publish_state("up"); 60 | break; 61 | case SwingVertical::high: 62 | this->publish_state("up_center"); 63 | break; 64 | case SwingVertical::mid: 65 | this->publish_state("center"); 66 | break; 67 | case SwingVertical::low: 68 | this->publish_state("down_center"); 69 | break; 70 | case SwingVertical::lowest: 71 | this->publish_state("down"); 72 | break; 73 | } 74 | } 75 | 76 | protected: 77 | private: 78 | }; 79 | 80 | } 81 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SharpClimateUART-ESPHome 2 | ESPHome Component for Sharp HVAC UART Protocol 3 | 4 | ## Supported Devices 5 | * Sharp (unknown) 6 | * Bosch (Climate 6000i (tested), 6100i, 8100i, 9100i) 7 | * Buderus (Logacool AC166i (tested), AC186i, AC196i, AC176i.2, AC186.2) 8 | * IVT (Aero 600, 800, 900) 9 | 10 | If you use an untested device, please open an issue with the initial log (contains "Sharp INIT Data") and the model name. 11 | 12 | ## Usage 13 | 14 | ### Hardware 15 | Tested with ESP12S Modul with 5v Levelconverter 16 | 17 | #### Connect HVAC 18 | Parts: 19 | - Crimp contacts: SPHD-001T-P 20 | - Connector: PAP-08V-S 21 | 22 | 23 | ![CN13a](https://github.com/sven819/SharpClimateUART-ESPHome/blob/main/docs/cn13.png?raw=true) 24 | 25 | Pinout HVAC <-> ESP: 26 | * Black: GND <-> GND 27 | * White: RX <-> TX (5v Logic) 28 | * Green: TX <-> RX (5v Logic) 29 | * Red: 5V <-> VCC 30 | 31 | 32 | ### Software 33 | 34 | To use this component in your ESPHome configuration, follow the example below: 35 | 36 | #### Example configuration 37 | 38 | ```yaml 39 | external_components: 40 | - source: component 41 | refresh: 0s 42 | 43 | esphome: 44 | name: klima_wohnzimmer 45 | 46 | esp8266: 47 | board: esp01_1m 48 | 49 | api: 50 | ota: 51 | web_server: 52 | port: 80 53 | 54 | wifi: 55 | ssid: !secret wifi_ssid 56 | password: !secret wifi_password 57 | 58 | logger: 59 | baud_rate: 0 # disable serial logging if you're using the standard TX/RX pins for your serial peripheral 60 | level: DEBUG 61 | 62 | uart: 63 | tx_pin: 1 # hardware dependent 64 | rx_pin: 3 # hardware dependent 65 | baud_rate: 9600 66 | parity: EVEN 67 | 68 | button: 69 | - platform: restart 70 | name: "Living Room Restart" 71 | 72 | climate: 73 | - platform: sharp_ac 74 | id: hvac 75 | name: "Living Room AC" 76 | horizontal_vane_select: 77 | name: "Horizontal Vane" 78 | vertical_vane_select: 79 | name: "Vertikal Vane" 80 | ion_switch: 81 | name: Plasmacluster 82 | connection_status: 83 | name: "AC Connection Status" 84 | reconnect_button: 85 | name: "AC Reconnect" 86 | ``` 87 | 88 | ### Adding this Component 89 | Add the external_components entry to your ESPHome configuration file, pointing to the repository of this component. 90 | Configure the uart section with the correct tx_pin and rx_pin for your hardware. 91 | Set up the climate platform to sharp_ac and name it appropriately. 92 | 93 | ## Disclaimer 94 | This project is provided "as is" without any warranty of any kind, express or implied. By using this project, you acknowledge that you do so at your own risk. The authors are not responsible for any damages or issues that may arise from using this software. Use it at your own discretion. 95 | 96 | This repository is not affiliated with, endorsed by, or in any way connected to Sharp, Bosch, Buderus, or IVT. All product and company names are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. 97 | 98 | -------------------------------------------------------------------------------- /tests/Makefile.test: -------------------------------------------------------------------------------- 1 | # Makefile for Sharp AC Unit Tests 2 | 3 | CXX = g++ 4 | CXXFLAGS = -std=c++11 -Wall -Wextra -I../components/sharp_ac -I. -DTEST_BUILD 5 | LDFLAGS = 6 | 7 | # Component directory source files 8 | COMPONENT_DIR = ../components/sharp_ac 9 | CORE_FRAME_CPP = $(COMPONENT_DIR)/core_frame.cpp 10 | CORE_LOGIC_CPP = $(COMPONENT_DIR)/core_logic.cpp 11 | 12 | # Source files 13 | SOURCES_FRAME = test_frame_parsing.cpp $(CORE_FRAME_CPP) 14 | SOURCES_CORE = test_core_logic.cpp $(CORE_FRAME_CPP) $(CORE_LOGIC_CPP) 15 | SOURCES_INTEGRATION = test_integration.cpp $(CORE_FRAME_CPP) $(CORE_LOGIC_CPP) 16 | 17 | OBJECTS_FRAME = test_frame_parsing.o core_frame.o 18 | OBJECTS_CORE = test_core_logic.o core_frame.o core_logic.o 19 | OBJECTS_INTEGRATION = test_integration.o core_frame.o core_logic.o 20 | 21 | TARGET_FRAME = test_frame_parsing 22 | TARGET_CORE = test_core_logic 23 | TARGET_INTEGRATION = test_integration 24 | 25 | # Mock ESPHome dependencies for testing 26 | MOCK_SOURCES = test_mocks.cpp 27 | MOCK_OBJECTS = $(MOCK_SOURCES:.cpp=.o) 28 | 29 | all: $(TARGET_FRAME) $(TARGET_CORE) $(TARGET_INTEGRATION) 30 | 31 | $(TARGET_FRAME): test_frame_parsing.o core_frame.o $(MOCK_OBJECTS) 32 | $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) 33 | 34 | $(TARGET_CORE): test_core_logic.o core_frame.o core_logic.o $(MOCK_OBJECTS) 35 | $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) 36 | 37 | $(TARGET_INTEGRATION): test_integration.o core_frame.o core_logic.o $(MOCK_OBJECTS) 38 | $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) 39 | 40 | # Compilation rules 41 | test_frame_parsing.o: test_frame_parsing.cpp 42 | $(CXX) $(CXXFLAGS) -c $< -o $@ 43 | 44 | test_core_logic.o: test_core_logic.cpp 45 | $(CXX) $(CXXFLAGS) -c $< -o $@ 46 | 47 | test_integration.o: test_integration.cpp 48 | $(CXX) $(CXXFLAGS) -c $< -o $@ 49 | 50 | core_frame.o: $(CORE_FRAME_CPP) 51 | $(CXX) $(CXXFLAGS) -c $< -o $@ 52 | 53 | core_logic.o: $(CORE_LOGIC_CPP) 54 | $(CXX) $(CXXFLAGS) -c $< -o $@ 55 | 56 | test_mocks.o: test_mocks.cpp 57 | $(CXX) $(CXXFLAGS) -c $< -o $@ 58 | 59 | clean: 60 | rm -f *.o $(TARGET_FRAME) $(TARGET_CORE) $(TARGET_INTEGRATION) 61 | 62 | # Run targets 63 | run: $(TARGET_FRAME) 64 | @echo "\n=== Running Frame Parsing Tests ===" 65 | ./$(TARGET_FRAME) 66 | 67 | run_core: $(TARGET_CORE) 68 | @echo "\n=== Running Core Logic Tests ===" 69 | ./$(TARGET_CORE) 70 | 71 | run_integration: $(TARGET_INTEGRATION) 72 | @echo "\n=== Running Integration Tests ===" 73 | ./$(TARGET_INTEGRATION) 74 | 75 | run_all: $(TARGET_FRAME) $(TARGET_CORE) $(TARGET_INTEGRATION) 76 | @echo "\n╔════════════════════════════════════════════════════════════════╗" 77 | @echo "║ Running All Sharp AC Component Tests ║" 78 | @echo "╚════════════════════════════════════════════════════════════════╝" 79 | @echo "\n=== 1. Frame Parsing Tests ===" 80 | ./$(TARGET_FRAME) 81 | @echo "\n=== 2. Core Logic Tests ===" 82 | ./$(TARGET_CORE) 83 | @echo "\n=== 3. Integration Tests ===" 84 | ./$(TARGET_INTEGRATION) 85 | @echo "\n╔════════════════════════════════════════════════════════════════╗" 86 | @echo "║ All Test Suites Complete ║" 87 | @echo "╚════════════════════════════════════════════════════════════════╝" 88 | 89 | .PHONY: all clean run run_core run_integration run_all 90 | -------------------------------------------------------------------------------- /components/sharp_ac/core_logic.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "core_types.h" 8 | #include "core_state.h" 9 | #include "core_frame.h" 10 | #include "core_messages.h" 11 | 12 | namespace esphome 13 | { 14 | namespace sharp_ac 15 | { 16 | static const char *const TAG = "sharp_ac.climate"; 17 | 18 | class SharpAcHardwareInterface { 19 | public: 20 | virtual ~SharpAcHardwareInterface() = default; 21 | virtual size_t read_array(uint8_t *data, size_t len) = 0; 22 | virtual size_t available() = 0; 23 | virtual void write_array(const uint8_t *data, size_t len) = 0; 24 | virtual uint8_t peek() = 0; 25 | virtual uint8_t read() = 0; 26 | virtual unsigned long get_millis() = 0; 27 | virtual void log_debug(const char* tag, const char* format, ...) = 0; 28 | virtual std::string format_hex_pretty(const uint8_t *data, size_t len) = 0; 29 | }; 30 | 31 | class SharpAcStateCallback { 32 | public: 33 | virtual ~SharpAcStateCallback() = default; 34 | virtual void on_state_update() = 0; 35 | virtual void on_ion_state_update(bool state) = 0; 36 | virtual void on_vane_horizontal_update(SwingHorizontal val) = 0; 37 | virtual void on_vane_vertical_update(SwingVertical val) = 0; 38 | virtual void on_connection_status_update(int status) = 0; 39 | }; 40 | 41 | class SharpAcCore 42 | { 43 | public: 44 | SharpAcCore(SharpAcHardwareInterface* hardware, SharpAcStateCallback* callback); 45 | virtual ~SharpAcCore() = default; 46 | 47 | void loop(); 48 | void setup(); 49 | 50 | void setIon(bool state); 51 | void setVaneHorizontal(SwingHorizontal val); 52 | void setVaneVertical(SwingVertical val); 53 | 54 | void publishUpdate(); 55 | const SharpState& getState() const { return state; } 56 | float getCurrentTemperature() const { return currentTemperature; } 57 | 58 | void controlMode(PowerMode mode, bool state); 59 | void controlFan(FanMode fan); 60 | void controlSwing(SwingHorizontal h, SwingVertical v); 61 | void controlTemperature(int temperature); 62 | void controlPreset(Preset preset); 63 | void resetConnection(); 64 | 65 | protected: 66 | std::string analyzeByte(uint8_t byte, size_t position, bool isStatusFrame); 67 | 68 | void write_frame(SharpFrame &frame) 69 | { 70 | frame.setChecksum(); 71 | frame.print(); 72 | 73 | if (frame.getSize() == 1 && frame.getData()[0] == 0x06) { 74 | hardware->log_debug(TAG, "TX: ACK"); 75 | } else { 76 | hardware->log_debug(TAG, "TX: %s", hardware->format_hex_pretty(frame.getData(), frame.getSize()).c_str()); 77 | awaitingResponse = true; 78 | lastRequestTime = hardware->get_millis(); 79 | } 80 | 81 | hardware->write_array(frame.getData(), frame.getSize()); 82 | } 83 | 84 | void write_ack(); 85 | 86 | private: 87 | SharpAcHardwareInterface* hardware; 88 | SharpAcStateCallback* callback; 89 | 90 | void sendInitMsg(const uint8_t *arr, size_t size); 91 | int errCounter = 0; 92 | 93 | protected: 94 | SharpState state; 95 | void init(SharpFrame &frame); 96 | SharpFrame readMsg(); 97 | void processUpdate(SharpFrame &frame); 98 | void startInit(); 99 | void checkTimeout(); 100 | int status = 0; 101 | unsigned long connectionStart = 0; 102 | unsigned long previousMillis = 0; 103 | unsigned long lastRequestTime = 0; 104 | bool awaitingResponse = false; 105 | const long interval = 60000; 106 | const long responseTimeout = 10000; // 10 seconds 107 | float currentTemperature = 0.0f; 108 | }; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit on error 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | cd "$SCRIPT_DIR" 7 | 8 | # Colors for output 9 | RED='\033[0;31m' 10 | GREEN='\033[0;32m' 11 | YELLOW='\033[1;33m' 12 | BLUE='\033[0;34m' 13 | NC='\033[0m' # No Color 14 | 15 | echo -e "${BLUE}" 16 | echo "╔══════════════════════════════════════════════════════════════╗" 17 | echo "║ Sharp AC Component - Test Suite Runner ║" 18 | echo "╚══════════════════════════════════════════════════════════════╝" 19 | echo -e "${NC}" 20 | 21 | # Function to display step headers 22 | step() { 23 | echo -e "\n${YELLOW}>>> $1${NC}" 24 | } 25 | 26 | # Function to display success 27 | success() { 28 | echo -e "${GREEN}✓ $1${NC}" 29 | } 30 | 31 | # Function to display error 32 | error() { 33 | echo -e "${RED}✗ $1${NC}" 34 | } 35 | 36 | # Check if g++ is installed 37 | if ! command -v g++ &> /dev/null; then 38 | error "g++ is not installed. Please install it first." 39 | exit 1 40 | fi 41 | 42 | success "g++ found: $(g++ --version | head -n1)" 43 | 44 | # Check if make is installed 45 | if ! command -v make &> /dev/null; then 46 | error "make is not installed. Please install it first." 47 | exit 1 48 | fi 49 | 50 | success "make found: $(make --version | head -n1)" 51 | 52 | # Clean 53 | step "Cleaning old build artifacts..." 54 | make -f Makefile.test clean > /dev/null 2>&1 55 | success "Cleanup complete" 56 | 57 | # Build 58 | step "Compiling test suites..." 59 | if make -f Makefile.test; then 60 | success "Compilation successful" 61 | else 62 | error "Compilation failed" 63 | exit 1 64 | fi 65 | 66 | # Counter for test results 67 | TOTAL_TESTS=0 68 | PASSED_TESTS=0 69 | 70 | # Run Frame Parsing Tests 71 | step "Running frame parsing tests..." 72 | echo "" 73 | if ./test_frame_parsing; then 74 | success "Frame parsing tests passed" 75 | ((PASSED_TESTS++)) 76 | else 77 | error "Frame parsing tests failed" 78 | fi 79 | ((TOTAL_TESTS++)) 80 | 81 | # Run Core Logic Tests 82 | step "Running core logic tests..." 83 | echo "" 84 | if ./test_core_logic; then 85 | success "Core logic tests passed" 86 | ((PASSED_TESTS++)) 87 | else 88 | error "Core logic tests failed" 89 | fi 90 | ((TOTAL_TESTS++)) 91 | 92 | # Run Integration Tests 93 | step "Running integration tests..." 94 | echo "" 95 | if ./test_integration; then 96 | success "Integration tests passed" 97 | ((PASSED_TESTS++)) 98 | else 99 | error "Integration tests failed" 100 | fi 101 | ((TOTAL_TESTS++)) 102 | 103 | # Summary 104 | echo -e "\n${BLUE}" 105 | echo "╔══════════════════════════════════════════════════════════════╗" 106 | echo "║ Test Summary ║" 107 | echo "╠══════════════════════════════════════════════════════════════╣" 108 | # Dynamically pad the summary line to fit the box 109 | summary="Test suites passed: $PASSED_TESTS / $TOTAL_TESTS" 110 | # Box width is 62, left border (1), 2 spaces, summary, padding, 1 space, right border (1) = 62 111 | # So, content width = 62 - 2 (borders) = 60 112 | # " " (2 spaces) + summary + padding + " " (1 space) = 60 113 | content=" $summary" 114 | content_len=${#content} 115 | total_content_width=59 # 2 spaces + summary + padding + 1 space = 60, but we want 1 space before right border 116 | padding_len=$((total_content_width - content_len)) 117 | padding="" 118 | if [ $padding_len -gt 0 ]; then 119 | padding=$(printf '%*s' "$padding_len" "") 120 | fi 121 | printf "║%s%s ║\n" "$content" "$padding" 122 | echo "╚══════════════════════════════════════════════════════════════╝" 123 | echo -e "${NC}" 124 | 125 | if [ $PASSED_TESTS -eq $TOTAL_TESTS ]; then 126 | echo -e "${GREEN}" 127 | echo "✓✓✓ All test suites passed! ✓✓✓" 128 | echo -e "${NC}" 129 | exit 0 130 | else 131 | echo -e "${RED}" 132 | echo "✗✗✗ Some test suites failed! ✗✗✗" 133 | echo "" 134 | echo "Please check the detailed output above." 135 | echo -e "${NC}" 136 | exit 1 137 | fi 138 | -------------------------------------------------------------------------------- /components/sharp_ac/climate.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import climate, uart, select, switch, text_sensor, button 4 | from esphome.const import CONF_ID 5 | 6 | AUTO_LOAD = ["climate", "uart", "select", "switch", "text_sensor", "button"] 7 | CODEOWNERS = ["@sven819"] 8 | 9 | CONF_SHARP_ID = "sharp_id" 10 | 11 | sharp_ac_ns = cg.esphome_ns.namespace("sharp_ac") 12 | SharpAc = sharp_ac_ns.class_("SharpAc", climate.Climate,uart.UARTDevice, cg.Component) 13 | 14 | VaneSelectVertical = sharp_ac_ns.class_("VaneSelectVertical", select.Select, cg.Component) 15 | VaneSelectHorizontal = sharp_ac_ns.class_("VaneSelectHorizontal", select.Select, cg.Component) 16 | 17 | IonSwitch = sharp_ac_ns.class_("IonSwitch", switch.Switch) 18 | ConnectionStatusSensor = sharp_ac_ns.class_("ConnectionStatusSensor", text_sensor.TextSensor, cg.Component) 19 | ReconnectButton = sharp_ac_ns.class_("ReconnectButton", button.Button, cg.Component) 20 | 21 | CONF_HORIZONTAL_SWING_SELECT = "horizontal_vane_select" 22 | CONF_VERTICAL_SWING_SELECT = "vertical_vane_select" 23 | CONF_ION_SWITCH = "ion_switch" 24 | CONF_CONNECTION_STATUS = "connection_status" 25 | CONF_RECONNECT_BUTTON = "reconnect_button" 26 | 27 | HORIZONTAL_SWING_OPTIONS = ["swing","left","center","right"] 28 | VERTICAL_SWING_OPTIONS = ["auto", "swing" , "up" , "up_center", "center", "down_center", "down"] 29 | 30 | SELECT_SCHEMA_VERTICAL = select.select_schema(VaneSelectVertical).extend( 31 | { 32 | cv.GenerateID(CONF_ID): cv.declare_id(VaneSelectVertical), 33 | } 34 | ) 35 | 36 | SELECT_SCHEMA_HORIZONTAL = select.select_schema(VaneSelectHorizontal).extend( 37 | { 38 | cv.GenerateID(CONF_ID): cv.declare_id(VaneSelectHorizontal), 39 | } 40 | ) 41 | 42 | ION_SCHEMA = switch.switch_schema(IonSwitch).extend( 43 | {cv.GenerateID(CONF_ID): cv.declare_id(IonSwitch)} 44 | ) 45 | 46 | CONNECTION_STATUS_SCHEMA = text_sensor.text_sensor_schema(ConnectionStatusSensor).extend( 47 | {cv.GenerateID(CONF_ID): cv.declare_id(ConnectionStatusSensor)} 48 | ) 49 | 50 | RECONNECT_BUTTON_SCHEMA = button.button_schema(ReconnectButton).extend( 51 | {cv.GenerateID(CONF_ID): cv.declare_id(ReconnectButton)} 52 | ) 53 | 54 | CONFIG_SCHEMA = climate.climate_schema(SharpAc).extend( 55 | { 56 | cv.GenerateID(): cv.declare_id(SharpAc), 57 | cv.Optional(CONF_HORIZONTAL_SWING_SELECT): SELECT_SCHEMA_HORIZONTAL, 58 | cv.Optional(CONF_VERTICAL_SWING_SELECT): SELECT_SCHEMA_VERTICAL, 59 | cv.Optional(CONF_ION_SWITCH): ION_SCHEMA, 60 | cv.Optional(CONF_CONNECTION_STATUS): CONNECTION_STATUS_SCHEMA, 61 | cv.Optional(CONF_RECONNECT_BUTTON): RECONNECT_BUTTON_SCHEMA 62 | } 63 | ).extend(uart.UART_DEVICE_SCHEMA) 64 | 65 | async def to_code(config): 66 | var = cg.new_Pvariable(config[CONF_ID]) 67 | 68 | if CONF_HORIZONTAL_SWING_SELECT in config: 69 | conf = config[CONF_HORIZONTAL_SWING_SELECT] 70 | swing_select = await select.new_select(conf, options=HORIZONTAL_SWING_OPTIONS) 71 | await cg.register_parented(swing_select, var) 72 | cg.add(var.setVaneHorizontalSelect(swing_select)) 73 | 74 | if CONF_VERTICAL_SWING_SELECT in config: 75 | conf = config[CONF_VERTICAL_SWING_SELECT] 76 | swing_select = await select.new_select(conf, options=VERTICAL_SWING_OPTIONS) 77 | await cg.register_parented(swing_select, var) 78 | cg.add(var.setVaneVerticalSelect(swing_select)) 79 | 80 | if CONF_ION_SWITCH in config: 81 | conf = config[CONF_ION_SWITCH] 82 | swt = await switch.new_switch(conf) 83 | cg.add(var.setIonSwitch(swt)) 84 | await cg.register_parented(swt, var) 85 | 86 | if CONF_CONNECTION_STATUS in config: 87 | conf = config[CONF_CONNECTION_STATUS] 88 | sens = await text_sensor.new_text_sensor(conf) 89 | cg.add(var.setConnectionStatusSensor(sens)) 90 | await cg.register_parented(sens, var) 91 | 92 | if CONF_RECONNECT_BUTTON in config: 93 | conf = config[CONF_RECONNECT_BUTTON] 94 | btn = await button.new_button(conf) 95 | cg.add(btn.set_parent(var)) 96 | cg.add(var.setReconnectButton(btn)) 97 | await cg.register_parented(btn, var) 98 | 99 | await uart.register_uart_device(var, config) 100 | await climate.register_climate(var, config) 101 | await cg.register_component(var, config) 102 | -------------------------------------------------------------------------------- /components/sharp_ac/comp_hardware.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/components/climate/climate.h" 4 | #include "esphome/components/uart/uart.h" 5 | #include "esphome/components/switch/switch.h" 6 | #include "esphome/components/select/select.h" 7 | #include "esphome/components/text_sensor/text_sensor.h" 8 | #include "esphome/components/button/button.h" 9 | #include "esphome/core/log.h" 10 | #include "esphome/core/helpers.h" 11 | 12 | #include "core_logic.h" 13 | #include "core_types.h" 14 | #include "core_state.h" 15 | #include "core_frame.h" 16 | #include "core_messages.h" 17 | 18 | #include 19 | #include 20 | 21 | namespace esphome 22 | { 23 | namespace sharp_ac 24 | { 25 | 26 | using climate::ClimateCall; 27 | using climate::ClimateFanMode; 28 | using climate::ClimateMode; 29 | using climate::ClimatePreset; 30 | using climate::ClimateSwingMode; 31 | using climate::ClimateTraits; 32 | 33 | class VaneSelectVertical; 34 | class VaneSelectHorizontal; 35 | class ConnectionStatusSensor; 36 | class ReconnectButton; 37 | class SharpAc; 38 | 39 | class ESPHomeHardwareInterface : public SharpAcHardwareInterface { 40 | public: 41 | ESPHomeHardwareInterface(uart::UARTDevice* uart_device) : uart_device_(uart_device) {} 42 | 43 | size_t read_array(uint8_t *data, size_t len) override { 44 | return uart_device_->read_array(data, len) ? len : 0; 45 | } 46 | 47 | size_t available() override { 48 | return uart_device_->available(); 49 | } 50 | 51 | void write_array(const uint8_t *data, size_t len) override { 52 | uart_device_->write_array(data, len); 53 | } 54 | 55 | uint8_t peek() override { 56 | return uart_device_->peek(); 57 | } 58 | 59 | uint8_t read() override { 60 | return uart_device_->read(); 61 | } 62 | 63 | unsigned long get_millis() override { 64 | return millis(); 65 | } 66 | 67 | void log_debug(const char* tag, const char* format, ...) override { 68 | va_list args; 69 | va_start(args, format); 70 | esp_log_vprintf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, format, args); 71 | va_end(args); 72 | } 73 | 74 | std::string format_hex_pretty(const uint8_t *data, size_t len) override { 75 | return esphome::format_hex_pretty(data, len); 76 | } 77 | 78 | private: 79 | uart::UARTDevice* uart_device_; 80 | }; 81 | 82 | class ESPHomeStateCallback : public SharpAcStateCallback { 83 | public: 84 | ESPHomeStateCallback(SharpAc* sharp_ac) : sharp_ac_(sharp_ac) {} 85 | 86 | void on_state_update() override; 87 | void on_ion_state_update(bool state) override; 88 | void on_vane_horizontal_update(SwingHorizontal val) override; 89 | void on_vane_vertical_update(SwingVertical val) override; 90 | void on_connection_status_update(int status) override; 91 | 92 | private: 93 | SharpAc* sharp_ac_; 94 | }; 95 | 96 | class SharpAc : public climate::Climate, public uart::UARTDevice, public Component 97 | { 98 | public: 99 | SharpAc(); 100 | 101 | void control(const climate::ClimateCall &call) override; 102 | void loop() override; 103 | void setup() override; 104 | esphome::climate::ClimateTraits traits() override; 105 | 106 | void setIon(bool state); 107 | void setVaneHorizontal(SwingHorizontal val); 108 | void setVaneVertical(SwingVertical val); 109 | 110 | void publishUpdate(); 111 | 112 | void setIonSwitch(switch_::Switch *ionSwitch) 113 | { 114 | this->ionSwitch = ionSwitch; 115 | }; 116 | void setVaneVerticalSelect(VaneSelectVertical *vane) 117 | { 118 | this->vaneVertical = vane; 119 | }; 120 | void setVaneHorizontalSelect(VaneSelectHorizontal *vane) 121 | { 122 | this->vaneHorizontal = vane; 123 | }; 124 | void setConnectionStatusSensor(text_sensor::TextSensor *sensor) 125 | { 126 | this->connectionStatusSensor = sensor; 127 | }; 128 | 129 | void setReconnectButton(button::Button *button) 130 | { 131 | this->reconnectButton = button; 132 | }; 133 | 134 | void updateConnectionStatus(int status); 135 | void triggerReconnect(); 136 | 137 | private: 138 | std::unique_ptr hardware_interface_; 139 | std::unique_ptr state_callback_; 140 | std::unique_ptr core_; 141 | 142 | switch_::Switch *ionSwitch; 143 | VaneSelectVertical *vaneVertical; 144 | VaneSelectHorizontal *vaneHorizontal; 145 | text_sensor::TextSensor *connectionStatusSensor{nullptr}; 146 | button::Button *reconnectButton{nullptr}; 147 | }; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /components/sharp_ac/core_frame.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "core_types.h" 3 | #include "core_frame.h" 4 | #include "core_state.h" 5 | 6 | SharpFrame::SharpFrame(char c) : size(1) 7 | { 8 | data = new uint8_t[size]; 9 | data[0] = static_cast(c); 10 | } 11 | SharpFrame::SharpFrame() : size(0) 12 | { 13 | } 14 | 15 | SharpFrame::SharpFrame(const uint8_t *arr, size_t sz) : size(sz) 16 | { 17 | data = new uint8_t[sz]; 18 | memcpy(data, arr, sz); 19 | } 20 | 21 | SharpFrame::~SharpFrame() 22 | { 23 | delete[] data; 24 | } 25 | 26 | SharpFrame::SharpFrame(const SharpFrame &other) : size(other.size) 27 | { 28 | data = new uint8_t[size]; 29 | memcpy(data, other.data, size); 30 | } 31 | 32 | uint8_t *SharpFrame::getData() const 33 | { 34 | return data; 35 | } 36 | 37 | size_t SharpFrame::getSize() const 38 | { 39 | return size; 40 | } 41 | 42 | int SharpFrame::setSize(size_t sz) 43 | { 44 | if (this->size == 0) 45 | { 46 | this->size = sz; 47 | this->data = new uint8_t[this->size]; 48 | return 1; 49 | } 50 | return 0; 51 | } 52 | 53 | void SharpFrame::print() 54 | { 55 | } 56 | 57 | void SharpFrame::setChecksum() 58 | { 59 | this->data[size - 1] = calcChecksum(); 60 | } 61 | 62 | bool SharpFrame::validateChecksum() 63 | { 64 | return this->data[size - 1] == calcChecksum(); 65 | } 66 | 67 | uint8_t SharpFrame::calcChecksum() 68 | { 69 | uint16_t sum = 0; 70 | for (int i = 1; i < this->size - 1; i++) 71 | { 72 | sum += this->data[i]; 73 | sum &= 0xFF; 74 | } 75 | uint8_t checksum = (uint8_t)((256 - sum) & 0xFF); 76 | return checksum; 77 | } 78 | 79 | SharpStatusFrame::SharpStatusFrame(const uint8_t *arr) : SharpFrame(arr, 18) 80 | { 81 | } 82 | 83 | int SharpStatusFrame::getTemperature() 84 | { 85 | return this->data[7]; 86 | } 87 | 88 | SharpModeFrame::SharpModeFrame(const uint8_t *arr) : SharpFrame(arr, 14) 89 | { 90 | } 91 | 92 | int SharpModeFrame::getTemperature() 93 | { 94 | // Temperature is encoded in lower nibble + 16 offset 95 | return (this->data[4] & 0x0F) + 16; 96 | } 97 | 98 | bool SharpModeFrame::getState() 99 | { 100 | // Only check Bit 7 (0x80) for actual power state 101 | return (this->data[8] & 0x80) != 0; 102 | } 103 | 104 | Preset SharpModeFrame::getPreset(){ 105 | // Response frames (0xFC): byte[7] bit-based (0x40=ECO, 0x80=FULLPOWER) 106 | // Command frames (0xFB): byte[7]=0x10 for ECO, byte[10]=0x01 for FULLPOWER 107 | if (this->data[2] == 0xFC) { 108 | // Response frame format 109 | if((this->data[7] & 0x40) == 0x40) 110 | return Preset::ECO; 111 | else if((this->data[7] & 0x80) == 0x80) 112 | return Preset::FULLPOWER; 113 | } else { 114 | // Command frame format (0xFB) 115 | if(this->data[10] == 0x01) 116 | return Preset::FULLPOWER; 117 | else if(this->data[7] == 0x10) 118 | return Preset::ECO; 119 | } 120 | 121 | return Preset::NONE; 122 | } 123 | 124 | 125 | SwingVertical SharpModeFrame::getSwingVertical() 126 | { 127 | // Response frames (0xFC) 128 | // Command frames (0xFB) 129 | if (this->data[2] == 0xFC) 130 | return static_cast(this->data[6] & 0x0F); 131 | else // 0xFB command frames 132 | return static_cast(this->data[8] & 0x0F); 133 | } 134 | 135 | SwingHorizontal SharpModeFrame::getSwingHorizontal() 136 | { 137 | // Response frames (0xFC) 138 | // Command frames (0xFB) 139 | if (this->data[2] == 0xFC) 140 | return static_cast((this->data[6] & 0xF0) >> 4); 141 | else // 0xFB command frames 142 | return static_cast((this->data[8] & 0xF0) >> 4); 143 | } 144 | 145 | FanMode SharpModeFrame::getFanMode() 146 | { 147 | // Response frames (0xFC) 148 | // Command frames (0xFB) 149 | if (this->data[2] == 0xFC) 150 | return static_cast((this->data[5] & 0xF0) >> 4); 151 | else // 0xFB command frames 152 | return static_cast((this->data[6] & 0xF0) >> 4); 153 | } 154 | 155 | PowerMode SharpModeFrame::getPowerMode() 156 | { 157 | // Response frames (0xFC) 158 | // Command frames (0xFB) 159 | if (this->data[2] == 0xFC) 160 | return static_cast(this->data[5] & 0x0F); 161 | else // 0xFB command frames 162 | return static_cast(this->data[6] & 0x0F); 163 | } 164 | 165 | bool SharpModeFrame::getIon() 166 | { 167 | // Check Bit 2 (0x04) for Ion/Plasmacluster state 168 | // 0x84, 0x94, 0x04 all have Ion ON 169 | return (this->data[8] & 0x04) != 0; 170 | } 171 | 172 | SharpCommandFrame::SharpCommandFrame() : SharpFrame() 173 | { 174 | this->setSize(14); 175 | 176 | this->data[0] = 0xdd; 177 | this->data[1] = 0x0b; 178 | this->data[2] = 0xfb; 179 | this->data[3] = 0x60; 180 | this->data[7] = 0x00; 181 | this->data[9] = 0x00; 182 | this->data[10] = 0x00; 183 | this->data[11] = 0xe4; 184 | } 185 | 186 | void SharpCommandFrame::setData(SharpState *state) 187 | { 188 | switch (state->mode) 189 | { 190 | case PowerMode::fan: 191 | { 192 | this->data[4] = 0x01; 193 | break; 194 | } 195 | case PowerMode::dry: 196 | { 197 | this->data[4] = 0x00; 198 | break; 199 | } 200 | case PowerMode::cool: 201 | { 202 | this->data[4] = 0xC0 | (state->temperature - 15); 203 | break; 204 | } 205 | case PowerMode::heat: 206 | { 207 | this->data[4] = 0xC0 | (state->temperature - 15); 208 | break; 209 | } 210 | } 211 | 212 | // Byte 6: Mode (lower nibble) + Fan (upper nibble) 213 | this->data[6] = (uint8_t)state->mode; 214 | if (state->mode == PowerMode::fan && state->fan == FanMode::auto_fan) 215 | this->data[6] |= (uint8_t)FanMode::low << 4; 216 | else if (state->preset == Preset::FULLPOWER) 217 | this->data[6] |= (uint8_t)FanMode::auto_fan << 4; 218 | else 219 | this->data[6] |= (uint8_t)state->fan << 4; 220 | 221 | // Byte 5: State indicator 222 | if (state->state){ 223 | if(state->preset == Preset::NONE) 224 | this->data[5] = 0x31; 225 | else 226 | this->data[5] = 0x61; 227 | } 228 | else 229 | this->data[5] = 0x21; 230 | 231 | // Ion mode 232 | if (state->ion) 233 | { 234 | this->data[11] = 0xE4; 235 | } 236 | else 237 | { 238 | this->data[11] = 0x10; 239 | } 240 | 241 | // Preset handling 242 | // From working example 243 | if(state->preset == Preset::FULLPOWER){ 244 | this->data[10] = 0x01; 245 | }else if (state->preset == Preset::ECO){ 246 | this->data[7] = 0x10; 247 | } 248 | 249 | // Swing data in byte 8 250 | // From working example 251 | this->data[8] = ((uint8_t)state->swingH << 4) | (uint8_t)state->swingV; 252 | } 253 | 254 | void SharpCommandFrame::setChecksum() 255 | { 256 | commandChecksum(); 257 | SharpFrame::setChecksum(); 258 | } 259 | 260 | void SharpCommandFrame::commandChecksum() 261 | { 262 | uint8_t checksum = 0x3; 263 | 264 | for (int i = 4; i < 12; i++) 265 | { 266 | checksum ^= this->data[i] & 0x0F; 267 | checksum ^= (data[i] >> 4) & 0x0F; 268 | } 269 | checksum = 0xF - (checksum & 0x0F); 270 | this->data[12] = (checksum << 4) | 0x01; 271 | } 272 | 273 | SharpACKFrame::SharpACKFrame() : SharpFrame(0x06) {} 274 | 275 | void SharpACKFrame::setChecksum() 276 | { 277 | } 278 | -------------------------------------------------------------------------------- /tests/test_frame_parsing.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "core_frame.h" 5 | #include "core_state.h" 6 | #include "core_types.h" 7 | 8 | void printBytes(const char* label, const uint8_t* arr, size_t size) { 9 | printf("%s: ", label); 10 | for (size_t i = 0; i < size; i++) { 11 | printf("0x%02x ", arr[i]); 12 | } 13 | printf("\n"); 14 | } 15 | 16 | // Test 1: Cool mode (31°C) 17 | void test_cool_31c() { 18 | printf("\n=== Test: Cool 31°C ===\n"); 19 | const uint8_t frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xcf, 0x31, 0x32, 0x00, 0xf9, 0x80, 0x00, 0xe4, 0x81, 0x8a}; 20 | printBytes("Frame", frame, 14); 21 | 22 | SharpModeFrame f(frame); 23 | 24 | assert(f.validateChecksum() == true); 25 | assert(f.getTemperature() == 31); 26 | assert(f.getState() == true); 27 | assert(f.getPowerMode() == PowerMode::cool); 28 | assert(f.getFanMode() == FanMode::mid); 29 | assert(f.getPreset() == Preset::NONE); 30 | // Byte 8 = 0xf9: lower=0x9 (highest), upper=0xF (swing) 31 | assert(f.getSwingVertical() == SwingVertical::highest); 32 | assert(f.getSwingHorizontal() == SwingHorizontal::swing); 33 | 34 | printf("✓ Temperature: %d°C\n", f.getTemperature()); 35 | printf("✓ State: %s\n", f.getState() ? "ON" : "OFF"); 36 | printf("✓ Mode: Cool (0x%02x)\n", (int)f.getPowerMode()); 37 | printf("✓ Fan: Mid (0x%02x)\n", (int)f.getFanMode()); 38 | printf("✓ Preset: NONE\n"); 39 | printf("✓ SwingV: Highest (0x9), SwingH: Swing (0xF)\n"); 40 | printf("✓ Checksum: VALID\n"); 41 | printf("✓ PASSED\n"); 42 | } 43 | 44 | // Test 2: Cool Eco mode 45 | void test_cool_eco() { 46 | printf("\n=== Test: Cool Eco 31°C ===\n"); 47 | const uint8_t frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xcf, 0x61, 0x32, 0x10, 0xf9, 0x80, 0x00, 0xf4, 0xd1, 0xea}; 48 | printBytes("Frame", frame, 14); 49 | 50 | SharpModeFrame f(frame); 51 | 52 | assert(f.validateChecksum() == true); 53 | assert(f.getTemperature() == 31); 54 | assert(f.getState() == true); 55 | assert(f.getPowerMode() == PowerMode::cool); 56 | assert(f.getFanMode() == FanMode::mid); 57 | assert(f.getPreset() == Preset::ECO); 58 | 59 | printf("✓ Temperature: %d°C\n", f.getTemperature()); 60 | printf("✓ Preset: ECO (byte[7]=0x%02x)\n", frame[7]); 61 | printf("✓ Checksum: VALID\n"); 62 | printf("✓ PASSED\n"); 63 | } 64 | 65 | // Test 3: Cool Eco 25°C 66 | void test_cool_eco_25c() { 67 | printf("\n=== Test: Cool Eco 25°C ===\n"); 68 | const uint8_t frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xc9, 0x61, 0x22, 0x0c, 0xf8, 0x00, 0x20, 0x1c, 0xa1, 0x6d}; 69 | printBytes("Frame", frame, 14); 70 | 71 | SharpModeFrame f(frame); 72 | 73 | assert(f.validateChecksum() == true); 74 | assert(f.getTemperature() == 25); 75 | assert(f.getState() == true); 76 | assert(f.getPowerMode() == PowerMode::cool); 77 | assert(f.getFanMode() == FanMode::auto_fan); 78 | 79 | printf("✓ Temperature: %d°C (byte[4]=0x%02x -> %d+16)\n", 80 | f.getTemperature(), frame[4], frame[4] & 0x0F); 81 | printf("✓ Fan: Auto (0x%02x)\n", (int)f.getFanMode()); 82 | printf("✓ PASSED\n"); 83 | } 84 | 85 | // Test 4: Full Power 31°C 86 | void test_full_power_31c() { 87 | printf("\n=== Test: Full Power 31°C ===\n"); 88 | const uint8_t frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xcf, 0x61, 0x22, 0x00, 0xf8, 0x80, 0x01, 0xe4, 0xc1, 0x2a}; 89 | printBytes("Frame", frame, 14); 90 | 91 | SharpModeFrame f(frame); 92 | 93 | assert(f.validateChecksum() == true); 94 | assert(f.getTemperature() == 31); 95 | assert(f.getState() == true); 96 | assert(f.getPowerMode() == PowerMode::cool); 97 | assert(f.getFanMode() == FanMode::auto_fan); 98 | assert(f.getPreset() == Preset::FULLPOWER); 99 | assert(f.getSwingVertical() == SwingVertical::auto_position); 100 | 101 | printf("✓ Temperature: %d°C\n", f.getTemperature()); 102 | printf("✓ Preset: FULLPOWER (byte[10]=0x%02x)\n", frame[10]); 103 | printf("✓ SwingV: Auto Position (0x%02x)\n", (int)f.getSwingVertical()); 104 | printf("✓ PASSED\n"); 105 | } 106 | 107 | // Test 5: Full Power 25°C 108 | void test_full_power_25c() { 109 | printf("\n=== Test: Full Power 25°C ===\n"); 110 | const uint8_t frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xc9, 0x61, 0x22, 0x00, 0xf8, 0x80, 0x01, 0xe4, 0xa1, 0x50}; 111 | printBytes("Frame", frame, 14); 112 | 113 | SharpModeFrame f(frame); 114 | 115 | assert(f.validateChecksum() == true); 116 | assert(f.getTemperature() == 25); 117 | assert(f.getPreset() == Preset::FULLPOWER); 118 | 119 | printf("✓ Temperature: %d°C\n", f.getTemperature()); 120 | printf("✓ Preset: FULLPOWER\n"); 121 | printf("✓ PASSED\n"); 122 | } 123 | 124 | // Test 6: Full Power without Cluster 17°C 125 | void test_full_power_no_cluster() { 126 | printf("\n=== Test: Full Power without Cluster 17°C ===\n"); 127 | const uint8_t frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xc1, 0x71, 0x22, 0x0c, 0xf8, 0x00, 0x21, 0x10, 0xe1, 0x30}; 128 | printBytes("Frame", frame, 14); 129 | 130 | SharpModeFrame f(frame); 131 | 132 | assert(f.validateChecksum() == true); 133 | assert(f.getTemperature() == 17); 134 | 135 | printf("✓ Temperature: %d°C (byte[4]=0x%02x -> %d+16)\n", 136 | f.getTemperature(), frame[4], frame[4] & 0x0F); 137 | printf("✓ PASSED\n"); 138 | } 139 | 140 | // Test 7: Eco without Cluster 25°C 141 | void test_eco_no_cluster() { 142 | printf("\n=== Test: Eco without Cluster 25°C ===\n"); 143 | const uint8_t frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xc9, 0x61, 0x22, 0x10, 0xf8, 0x80, 0x00, 0xf0, 0xf1, 0xe5}; 144 | printBytes("Frame", frame, 14); 145 | 146 | SharpModeFrame f(frame); 147 | 148 | assert(f.validateChecksum() == true); 149 | assert(f.getTemperature() == 25); 150 | assert(f.getPreset() == Preset::ECO); 151 | 152 | printf("✓ Temperature: %d°C\n", f.getTemperature()); 153 | printf("✓ Preset: ECO (byte[7]=0x%02x)\n", frame[7]); 154 | printf("✓ PASSED\n"); 155 | } 156 | 157 | // Test 8: Command Generation 158 | void test_command_generation() { 159 | printf("\n=== Test: Command Generation ===\n"); 160 | 161 | // Cool mode, 25°C 162 | SharpState state; 163 | state.state = true; 164 | state.mode = PowerMode::cool; 165 | state.fan = FanMode::mid; 166 | state.temperature = 25; 167 | state.swingV = SwingVertical::swing; 168 | state.swingH = SwingHorizontal::swing; 169 | state.ion = false; 170 | state.preset = Preset::NONE; 171 | 172 | SharpCommandFrame cmd; 173 | cmd.setData(&state); 174 | cmd.setChecksum(); 175 | 176 | printBytes("Generated", cmd.getData(), cmd.getSize()); 177 | 178 | printf(" Byte[4] (Temp): 0x%02x (should be 0xC0+(25-15)=0xCA)\n", cmd.getData()[4]); 179 | printf(" Byte[5] (State): 0x%02x (should be 0x31 for ON)\n", cmd.getData()[5]); 180 | printf(" Byte[6] (Mode): 0x%02x (should have cool+mid)\n", cmd.getData()[6]); 181 | printf(" Byte[8] (Swing): 0x%02x\n", cmd.getData()[8]); 182 | 183 | assert(cmd.validateChecksum() == true); 184 | printf("✓ Checksum: VALID\n"); 185 | printf("✓ PASSED\n"); 186 | } 187 | 188 | // Test 9: All temperatures 16-31°C 189 | void test_all_temperatures() { 190 | printf("\n=== Test: Temperature Range 16-31°C ===\n"); 191 | 192 | for (int temp = 16; temp <= 31; temp++) { 193 | uint8_t frame[14] = {0xdd, 0x0b, 0xfb, 0x60, 0xc0, 0x31, 0x32, 0x00, 0xf9, 0x80, 0x00, 0xe4, 0x81, 0x00}; 194 | 195 | // Set temperature in byte 4 196 | frame[4] = 0xC0 | (temp - 16); 197 | 198 | // Recalculate checksum 199 | uint16_t sum = 0; 200 | for (int i = 1; i < 13; i++) { 201 | sum += frame[i]; 202 | sum &= 0xFF; 203 | } 204 | frame[13] = (uint8_t)((256 - sum) & 0xFF); 205 | 206 | SharpModeFrame f(frame); 207 | int parsed = f.getTemperature(); 208 | 209 | if (parsed != temp) { 210 | printf("✗ Temperature %d°C failed: parsed as %d\n", temp, parsed); 211 | assert(false); 212 | } 213 | } 214 | 215 | printf("✓ All temperatures 16-31°C parsed correctly\n"); 216 | printf("✓ PASSED\n"); 217 | } 218 | 219 | int main() { 220 | printf("\n"); 221 | printf("╔════════════════════════════════════════════════════╗\n"); 222 | printf("║ Sharp AC Unit Tests - Based on Real Frames ║\n"); 223 | printf("║ All test data from logs/Modes.txt ║\n"); 224 | printf("╚════════════════════════════════════════════════════╝\n"); 225 | 226 | test_cool_31c(); 227 | test_cool_eco(); 228 | test_cool_eco_25c(); 229 | test_full_power_31c(); 230 | test_full_power_25c(); 231 | test_full_power_no_cluster(); 232 | test_eco_no_cluster(); 233 | test_all_temperatures(); 234 | test_command_generation(); 235 | 236 | printf("\n"); 237 | printf("╔════════════════════════════════════════════════════╗\n"); 238 | printf("║ ✓ ALL TESTS PASSED ✓ ║\n"); 239 | printf("╚════════════════════════════════════════════════════╝\n"); 240 | printf("\n"); 241 | 242 | return 0; 243 | } 244 | -------------------------------------------------------------------------------- /components/sharp_ac/comp_hardware.cpp: -------------------------------------------------------------------------------- 1 | #include "comp_hardware.h" 2 | #include "comp_vane_horizontal.h" 3 | #include "comp_vane_vertical.h" 4 | #include "comp_reconnect_button.h" 5 | 6 | namespace esphome 7 | { 8 | namespace sharp_ac 9 | { 10 | void ESPHomeStateCallback::on_state_update() { 11 | if (sharp_ac_) { 12 | sharp_ac_->publishUpdate(); 13 | } 14 | } 15 | 16 | void ESPHomeStateCallback::on_ion_state_update(bool state) { 17 | } 18 | 19 | void ESPHomeStateCallback::on_vane_horizontal_update(SwingHorizontal val) { 20 | } 21 | 22 | void ESPHomeStateCallback::on_vane_vertical_update(SwingVertical val) { 23 | } 24 | 25 | void ESPHomeStateCallback::on_connection_status_update(int status) { 26 | if (sharp_ac_) { 27 | sharp_ac_->updateConnectionStatus(status); 28 | } 29 | } 30 | 31 | SharpAc::SharpAc() { 32 | hardware_interface_ = std::make_unique(this); 33 | state_callback_ = std::make_unique(this); 34 | core_ = std::make_unique(hardware_interface_.get(), state_callback_.get()); 35 | } 36 | 37 | ClimateTraits SharpAc::traits() 38 | { 39 | auto traits = esphome::climate::ClimateTraits(); 40 | traits.set_supports_current_temperature(true); 41 | traits.set_visual_min_temperature(16); 42 | traits.set_visual_max_temperature(30); 43 | traits.set_visual_temperature_step(1.0); 44 | 45 | traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); 46 | traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); 47 | traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM); 48 | traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH); 49 | 50 | traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF); 51 | traits.add_supported_mode(ClimateMode::CLIMATE_MODE_COOL); 52 | traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT); 53 | traits.add_supported_mode(ClimateMode::CLIMATE_MODE_DRY); 54 | traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY); 55 | 56 | traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); 57 | traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST); 58 | traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE); 59 | 60 | traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF); 61 | traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_BOTH); 62 | traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_HORIZONTAL); 63 | traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL); 64 | 65 | return traits; 66 | } 67 | 68 | void SharpAc::publishUpdate() 69 | { 70 | const auto& state = core_->getState(); 71 | 72 | this->target_temperature = state.temperature; 73 | this->current_temperature = core_->getCurrentTemperature(); 74 | 75 | switch (state.fan) 76 | { 77 | case FanMode::auto_fan: 78 | this->fan_mode = ClimateFanMode::CLIMATE_FAN_AUTO; 79 | break; 80 | case FanMode::low: 81 | this->fan_mode = ClimateFanMode::CLIMATE_FAN_LOW; 82 | break; 83 | case FanMode::mid: 84 | this->fan_mode = ClimateFanMode::CLIMATE_FAN_MEDIUM; 85 | break; 86 | case FanMode::high: 87 | this->fan_mode = ClimateFanMode::CLIMATE_FAN_MEDIUM; 88 | break; 89 | case FanMode::highest: 90 | this->fan_mode = ClimateFanMode::CLIMATE_FAN_HIGH; 91 | break; 92 | default: 93 | ESP_LOGD("sharp_ac", "UNKNOWN FAN MODE"); 94 | } 95 | 96 | switch (state.mode) 97 | { 98 | case PowerMode::fan: 99 | this->mode = ClimateMode::CLIMATE_MODE_FAN_ONLY; 100 | break; 101 | case PowerMode::cool: 102 | this->mode = ClimateMode::CLIMATE_MODE_COOL; 103 | break; 104 | case PowerMode::heat: 105 | this->mode = ClimateMode::CLIMATE_MODE_HEAT; 106 | break; 107 | case PowerMode::dry: 108 | this->mode = ClimateMode::CLIMATE_MODE_DRY; 109 | break; 110 | default: 111 | ESP_LOGD("sharp_ac", "UNKNOWN MODE"); 112 | } 113 | 114 | if (!state.state) 115 | { 116 | this->mode = ClimateMode::CLIMATE_MODE_OFF; 117 | } 118 | 119 | switch (state.preset) 120 | { 121 | case Preset::ECO: 122 | this->preset = ClimatePreset::CLIMATE_PRESET_ECO; 123 | break; 124 | case Preset::FULLPOWER: 125 | this->preset = ClimatePreset::CLIMATE_PRESET_BOOST; 126 | break; 127 | default: 128 | this->preset = ClimatePreset::CLIMATE_PRESET_NONE; 129 | break; 130 | } 131 | 132 | if (state.swingH == SwingHorizontal::swing && state.swingV == SwingVertical::swing) 133 | this->swing_mode = ClimateSwingMode::CLIMATE_SWING_BOTH; 134 | else if (state.swingH == SwingHorizontal::swing) 135 | this->swing_mode = ClimateSwingMode::CLIMATE_SWING_HORIZONTAL; 136 | else if (state.swingV == SwingVertical::swing) 137 | this->swing_mode = ClimateSwingMode::CLIMATE_SWING_VERTICAL; 138 | else 139 | this->swing_mode = ClimateSwingMode::CLIMATE_SWING_OFF; 140 | 141 | if (this->ionSwitch != nullptr) 142 | this->ionSwitch->publish_state(state.ion); 143 | 144 | if (this->vaneHorizontal != nullptr) 145 | this->vaneHorizontal->setVal(state.swingH); 146 | 147 | if (this->vaneVertical != nullptr) 148 | this->vaneVertical->setVal(state.swingV); 149 | 150 | this->publish_state(); 151 | } 152 | 153 | void SharpAc::control(const ClimateCall &call) 154 | { 155 | ESP_LOGD("sharp_ac", "=== Climate Control Called ==="); 156 | 157 | if (call.get_mode().has_value()) 158 | { 159 | ClimateMode newMode = call.get_mode().value(); 160 | ESP_LOGD("sharp_ac", "Setting mode: %d (%s)", (int)newMode, 161 | newMode == ClimateMode::CLIMATE_MODE_OFF ? "OFF" : 162 | newMode == ClimateMode::CLIMATE_MODE_COOL ? "COOL" : 163 | newMode == ClimateMode::CLIMATE_MODE_HEAT ? "HEAT" : 164 | newMode == ClimateMode::CLIMATE_MODE_DRY ? "DRY" : 165 | newMode == ClimateMode::CLIMATE_MODE_FAN_ONLY ? "FAN" : "UNKNOWN"); 166 | 167 | switch (newMode) 168 | { 169 | case ClimateMode::CLIMATE_MODE_OFF: 170 | ESP_LOGD("sharp_ac", "Sending OFF command to core"); 171 | core_->controlMode(PowerMode::cool, false); 172 | break; 173 | case ClimateMode::CLIMATE_MODE_COOL: 174 | ESP_LOGD("sharp_ac", "Sending COOL ON command to core"); 175 | core_->controlMode(PowerMode::cool, true); 176 | break; 177 | case ClimateMode::CLIMATE_MODE_HEAT: 178 | ESP_LOGD("sharp_ac", "Sending HEAT ON command to core"); 179 | core_->controlMode(PowerMode::heat, true); 180 | break; 181 | case ClimateMode::CLIMATE_MODE_DRY: 182 | ESP_LOGD("sharp_ac", "Sending DRY ON command to core"); 183 | core_->controlMode(PowerMode::dry, true); 184 | break; 185 | case ClimateMode::CLIMATE_MODE_FAN_ONLY: 186 | ESP_LOGD("sharp_ac", "Sending FAN ONLY ON command to core"); 187 | core_->controlMode(PowerMode::fan, true); 188 | break; 189 | default: 190 | ESP_LOGE("sharp_ac", "Unsupported mode: %d", (int)newMode); 191 | } 192 | } 193 | 194 | if (call.get_target_temperature().has_value()) 195 | { 196 | float temp = call.get_target_temperature().value(); 197 | ESP_LOGD("sharp_ac", "Setting target temperature: %.1f°C", temp); 198 | ESP_LOGD("sharp_ac", "Sending temperature command to core"); 199 | core_->controlTemperature((int)temp); 200 | } 201 | 202 | if (call.get_fan_mode().has_value()) 203 | { 204 | ClimateFanMode fanMode = call.get_fan_mode().value(); 205 | ESP_LOGD("sharp_ac", "Setting fan mode: %d (%s)", (int)fanMode, 206 | fanMode == ClimateFanMode::CLIMATE_FAN_AUTO ? "AUTO" : 207 | fanMode == ClimateFanMode::CLIMATE_FAN_LOW ? "LOW" : 208 | fanMode == ClimateFanMode::CLIMATE_FAN_MEDIUM ? "MEDIUM" : 209 | fanMode == ClimateFanMode::CLIMATE_FAN_HIGH ? "HIGH" : "UNKNOWN"); 210 | 211 | switch (fanMode) 212 | { 213 | case ClimateFanMode::CLIMATE_FAN_AUTO: 214 | ESP_LOGD("sharp_ac", "Sending AUTO FAN command to core"); 215 | core_->controlFan(FanMode::auto_fan); 216 | break; 217 | case ClimateFanMode::CLIMATE_FAN_LOW: 218 | ESP_LOGD("sharp_ac", "Sending LOW FAN command to core"); 219 | core_->controlFan(FanMode::low); 220 | break; 221 | case ClimateFanMode::CLIMATE_FAN_MEDIUM: 222 | ESP_LOGD("sharp_ac", "Sending MEDIUM FAN command to core"); 223 | core_->controlFan(FanMode::mid); 224 | break; 225 | case ClimateFanMode::CLIMATE_FAN_HIGH: 226 | ESP_LOGD("sharp_ac", "Sending HIGH FAN command to core"); 227 | core_->controlFan(FanMode::highest); 228 | break; 229 | default: 230 | ESP_LOGE("sharp_ac", "Unsupported fan mode: %d", (int)fanMode); 231 | } 232 | } 233 | 234 | if (call.get_preset().has_value()) 235 | { 236 | ClimatePreset preset = call.get_preset().value(); 237 | ESP_LOGD("sharp_ac", "Setting preset: %d (%s)", (int)preset, 238 | preset == ClimatePreset::CLIMATE_PRESET_ECO ? "ECO" : 239 | preset == ClimatePreset::CLIMATE_PRESET_BOOST ? "BOOST" : 240 | preset == ClimatePreset::CLIMATE_PRESET_NONE ? "NONE" : "UNKNOWN"); 241 | 242 | switch (preset) 243 | { 244 | case ClimatePreset::CLIMATE_PRESET_ECO: 245 | ESP_LOGD("sharp_ac", "Sending ECO preset command to core"); 246 | core_->controlPreset(Preset::ECO); 247 | break; 248 | case ClimatePreset::CLIMATE_PRESET_BOOST: 249 | ESP_LOGD("sharp_ac", "Sending BOOST preset command to core"); 250 | core_->controlPreset(Preset::FULLPOWER); 251 | break; 252 | default: 253 | ESP_LOGD("sharp_ac", "Sending NONE preset command to core"); 254 | core_->controlPreset(Preset::NONE); 255 | break; 256 | } 257 | } 258 | 259 | if (call.get_swing_mode().has_value()) 260 | { 261 | ClimateSwingMode swingMode = call.get_swing_mode().value(); 262 | ESP_LOGD("sharp_ac", "Setting swing mode: %d (%s)", (int)swingMode, 263 | swingMode == ClimateSwingMode::CLIMATE_SWING_OFF ? "OFF" : 264 | swingMode == ClimateSwingMode::CLIMATE_SWING_BOTH ? "BOTH" : 265 | swingMode == ClimateSwingMode::CLIMATE_SWING_HORIZONTAL ? "HORIZONTAL" : 266 | swingMode == ClimateSwingMode::CLIMATE_SWING_VERTICAL ? "VERTICAL" : "UNKNOWN"); 267 | 268 | switch (swingMode) 269 | { 270 | case ClimateSwingMode::CLIMATE_SWING_OFF: 271 | ESP_LOGD("sharp_ac", "Sending SWING OFF command to core (middle/mid position)"); 272 | core_->controlSwing(SwingHorizontal::middle, SwingVertical::mid); 273 | break; 274 | case ClimateSwingMode::CLIMATE_SWING_BOTH: 275 | ESP_LOGD("sharp_ac", "Sending BOTH SWING command to core"); 276 | core_->controlSwing(SwingHorizontal::swing, SwingVertical::swing); 277 | break; 278 | case ClimateSwingMode::CLIMATE_SWING_HORIZONTAL: 279 | ESP_LOGD("sharp_ac", "Sending HORIZONTAL SWING command to core"); 280 | core_->controlSwing(SwingHorizontal::swing, SwingVertical::mid); 281 | break; 282 | case ClimateSwingMode::CLIMATE_SWING_VERTICAL: 283 | ESP_LOGD("sharp_ac", "Sending VERTICAL SWING command to core"); 284 | core_->controlSwing(SwingHorizontal::middle, SwingVertical::swing); 285 | break; 286 | default: 287 | ESP_LOGE("sharp_ac", "Unsupported swing mode: %d", (int)swingMode); 288 | } 289 | } 290 | 291 | // Publish optimistic state immediately after sending command 292 | // This prevents the UI from showing the old state briefly 293 | ESP_LOGD("sharp_ac", "Publishing optimistic state update"); 294 | this->publishUpdate(); 295 | 296 | ESP_LOGD("sharp_ac", "=== Control Processing Complete ==="); 297 | } 298 | 299 | void SharpAc::setIon(bool state) 300 | { 301 | core_->setIon(state); 302 | } 303 | 304 | void SharpAc::setVaneHorizontal(SwingHorizontal val) 305 | { 306 | core_->setVaneHorizontal(val); 307 | } 308 | 309 | void SharpAc::setVaneVertical(SwingVertical val) 310 | { 311 | core_->setVaneVertical(val); 312 | } 313 | 314 | void SharpAc::setup() 315 | { 316 | core_->setup(); 317 | if (connectionStatusSensor != nullptr) { 318 | connectionStatusSensor->publish_state("Disconnected"); 319 | } 320 | } 321 | 322 | void SharpAc::loop() 323 | { 324 | core_->loop(); 325 | } 326 | 327 | void SharpAc::updateConnectionStatus(int status) 328 | { 329 | if (connectionStatusSensor == nullptr) { 330 | return; 331 | } 332 | 333 | std::string status_text; 334 | if (status < 8) { 335 | char buffer[32]; 336 | snprintf(buffer, sizeof(buffer), "Connecting (%d/8)", status); 337 | status_text = buffer; 338 | } else if (status == 8) { 339 | status_text = "Connected"; 340 | } else { 341 | status_text = "Unknown"; 342 | } 343 | 344 | connectionStatusSensor->publish_state(status_text); 345 | } 346 | 347 | void SharpAc::triggerReconnect() 348 | { 349 | ESP_LOGI("sharp_ac", "Triggering connection reset..."); 350 | if (core_) 351 | { 352 | core_->resetConnection(); 353 | } 354 | } 355 | 356 | void ReconnectButton::press_action() 357 | { 358 | ESP_LOGI("sharp_ac", "Reconnect button pressed - resetting connection"); 359 | if (this->parent_ != nullptr) 360 | { 361 | this->parent_->triggerReconnect(); 362 | } 363 | } 364 | 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /components/sharp_ac/core_logic.cpp: -------------------------------------------------------------------------------- 1 | #include "core_logic.h" 2 | #include 3 | #include 4 | 5 | namespace esphome 6 | { 7 | namespace sharp_ac 8 | { 9 | 10 | SharpAcCore::SharpAcCore(SharpAcHardwareInterface* hardware, SharpAcStateCallback* callback) 11 | : hardware(hardware), callback(callback) 12 | { 13 | hardware->log_debug(TAG, "SharpAcCore initialized successfully"); 14 | } 15 | 16 | void SharpAcCore::write_ack() 17 | { 18 | SharpACKFrame frame; 19 | this->write_frame(frame); 20 | } 21 | 22 | void SharpAcCore::setup() 23 | { 24 | } 25 | 26 | void SharpAcCore::startInit() 27 | { 28 | // Don't send if we're already waiting for a response 29 | if (awaitingResponse) { 30 | return; 31 | } 32 | 33 | if (this->connectionStart == 0) { 34 | hardware->log_debug(TAG, "Initializing connection..."); 35 | this->connectionStart = hardware->get_millis(); 36 | } 37 | 38 | SharpFrame frame(init_msg, sizeof(init_msg) + 1); 39 | this->write_frame(frame); 40 | } 41 | 42 | void SharpAcCore::sendInitMsg(const uint8_t *arr, size_t size) 43 | { 44 | SharpFrame frame(arr, size); 45 | this->write_frame(frame); 46 | this->status++; 47 | 48 | if (callback) { 49 | callback->on_connection_status_update(this->status); 50 | } 51 | 52 | if (this->status < 8) { 53 | hardware->log_debug(TAG, "Connecting (%d/8)...", this->status); 54 | } else { 55 | hardware->log_debug(TAG, "Connected"); 56 | } 57 | } 58 | 59 | void SharpAcCore::init(SharpFrame &frame) 60 | { 61 | if (frame.getSize() == 0) 62 | return; 63 | 64 | switch (frame.getData()[0]) 65 | { 66 | case 0x06: 67 | if (this->status == 2 || this->status == 7) 68 | { 69 | this->status++; 70 | 71 | // Notify status change 72 | if (callback) { 73 | callback->on_connection_status_update(this->status); 74 | } 75 | 76 | // Log progress for ACK steps (status 2->3 and 7->8) 77 | if (this->status < 8) { 78 | hardware->log_debug(TAG, "Connecting (%d/8)...", this->status); 79 | } else { 80 | hardware->log_debug(TAG, "Connected"); 81 | } 82 | } 83 | break; 84 | case 0x02: 85 | if (this->status == 0) 86 | sendInitMsg(init_msg2, sizeof(init_msg2) + 1); 87 | else if (this->status == 1) 88 | sendInitMsg(subscribe_msg, sizeof(subscribe_msg) + 1); 89 | break; 90 | case 0x03: 91 | if (this->status == 3) 92 | sendInitMsg(subscribe_msg2, sizeof(subscribe_msg2) + 1); 93 | else if (this->status == 4) 94 | sendInitMsg(get_state, sizeof(get_state) + 1); 95 | break; 96 | case 0xdc: 97 | if (this->status == 5) 98 | sendInitMsg(get_status, sizeof(get_status) + 1); 99 | else if (this->status == 6) 100 | sendInitMsg(connected_msg, sizeof(connected_msg) + 1); 101 | 102 | this->processUpdate(frame); 103 | break; 104 | } 105 | } 106 | 107 | SharpFrame SharpAcCore::readMsg() 108 | { 109 | uint8_t msg[18]; 110 | uint8_t msg_id = hardware->peek(); 111 | 112 | if (msg_id == 0x06 || msg_id == 0x00) 113 | { 114 | uint8_t singleByte = hardware->read(); 115 | SharpFrame frame(singleByte); 116 | 117 | hardware->log_debug(TAG, "RX: ACK"); 118 | awaitingResponse = false; 119 | 120 | return frame; 121 | } 122 | 123 | int size = 8; 124 | 125 | if (hardware->available() < 8) 126 | { 127 | if (this->errCounter < 5) 128 | { 129 | this->errCounter++; 130 | return SharpFrame(msg, 0); 131 | } 132 | else 133 | { 134 | this->errCounter = 0; 135 | uint8_t singleByte = hardware->read(); 136 | SharpFrame frame(singleByte); 137 | 138 | hardware->log_debug(TAG, "RX: %s (error recovery)", hardware->format_hex_pretty(&singleByte, 1).c_str()); 139 | 140 | return frame; 141 | } 142 | } 143 | this->errCounter = 0; 144 | 145 | hardware->read_array(msg, 8); 146 | 147 | if (msg_id == 0x02) 148 | size += msg[6]; 149 | else if (msg_id == 0x03 && msg[1] == 0xfe && msg[2] == 0x00) 150 | size = 17; 151 | else if (msg_id == 0xdc) 152 | { 153 | if (msg[1] == 0x0b) 154 | size = 14; 155 | else if (msg[1] == 0x0F) 156 | size = 18; 157 | } 158 | if (size > 8) 159 | { 160 | hardware->read_array(msg + 8, size - 8); 161 | SharpFrame frame(msg, size); 162 | 163 | hardware->log_debug(TAG, "RX: %s", hardware->format_hex_pretty(frame.getData(), frame.getSize()).c_str()); 164 | 165 | awaitingResponse = false; 166 | 167 | return frame; 168 | } 169 | 170 | SharpFrame frame(msg, size); 171 | 172 | hardware->log_debug(TAG, "RX: %s", hardware->format_hex_pretty(frame.getData(), frame.getSize()).c_str()); 173 | 174 | // Mark that we received a valid response 175 | awaitingResponse = false; 176 | 177 | return frame; 178 | } 179 | 180 | std::string SharpAcCore::analyzeByte(uint8_t byte, size_t position, bool isStatusFrame) { 181 | std::string result; 182 | char hex[8]; 183 | 184 | if (isStatusFrame) { 185 | switch(position) { 186 | case 7: 187 | snprintf(hex, sizeof(hex), "0x%02X", byte & 0x0F); 188 | result = std::string("Temperature LSB: ") + hex + " (" + std::to_string((byte & 0x0F) + 16) + "°C)"; 189 | break; 190 | case 8: 191 | snprintf(hex, sizeof(hex), "0x%02X", byte); 192 | result = std::string("Temperature MSB: ") + hex; 193 | break; 194 | default: 195 | snprintf(hex, sizeof(hex), "0x%02X", byte); 196 | result = std::string("Unknown: ") + hex; 197 | } 198 | } else { 199 | switch(position) { 200 | case 4: 201 | { 202 | std::string mode; 203 | switch(static_cast(byte & 0x0F)) { 204 | case PowerMode::heat: mode = "Heat"; break; 205 | case PowerMode::cool: mode = "Cool"; break; 206 | case PowerMode::dry: mode = "Dry"; break; 207 | case PowerMode::fan: mode = "Fan"; break; 208 | default: mode = "Unknown"; break; 209 | } 210 | result = std::string("Power Mode: ") + mode + " (0x" + hardware->format_hex_pretty(&byte, 1) + ")"; 211 | break; 212 | } 213 | case 5: 214 | { 215 | std::string fan; 216 | switch(static_cast(byte & 0x0F)) { 217 | case FanMode::low: fan = "Low"; break; 218 | case FanMode::mid: fan = "Mid"; break; 219 | case FanMode::high: fan = "High"; break; 220 | case FanMode::highest: fan = "Highest"; break; 221 | case FanMode::auto_fan: fan = "Auto"; break; 222 | default: fan = "Unknown"; break; 223 | } 224 | result = std::string("Fan Mode: ") + fan + " (0x" + hardware->format_hex_pretty(&byte, 1) + ")"; 225 | break; 226 | } 227 | case 6: 228 | { 229 | std::string swing; 230 | // Horizontal swing (upper 4 bits) 231 | switch(static_cast((byte >> 4) & 0x0F)) { 232 | case SwingHorizontal::swing: swing += "Swing"; break; 233 | case SwingHorizontal::left: swing += "Left"; break; 234 | case SwingHorizontal::middle: swing += "Middle"; break; 235 | case SwingHorizontal::right: swing += "Right"; break; 236 | default: swing += "Unknown H"; break; 237 | } 238 | swing += "/"; 239 | // Vertical swing (lower 4 bits) 240 | switch(static_cast(byte & 0x0F)) { 241 | case SwingVertical::swing: swing += "Swing"; break; 242 | case SwingVertical::auto_position: swing += "Auto"; break; 243 | case SwingVertical::highest: swing += "Highest"; break; 244 | case SwingVertical::high: swing += "High"; break; 245 | case SwingVertical::mid: swing += "Middle"; break; 246 | case SwingVertical::low: swing += "Low"; break; 247 | case SwingVertical::lowest: swing += "Lowest"; break; 248 | default: swing += "Unknown V"; break; 249 | } 250 | result = swing + " (0x" + hardware->format_hex_pretty(&byte, 1) + ")"; 251 | break; 252 | } 253 | case 11: 254 | { 255 | std::string features; 256 | Preset preset = static_cast((byte >> 1) & 0x03); 257 | switch(preset) { 258 | case Preset::NONE: features += "None"; break; 259 | case Preset::ECO: features += "Eco"; break; 260 | case Preset::FULLPOWER: features += "Full Power"; break; 261 | default: features += "Unknown"; break; 262 | } 263 | result = features + " (0x" + hardware->format_hex_pretty(&byte, 1) + ")"; 264 | break; 265 | } 266 | default: 267 | snprintf(hex, sizeof(hex), "0x%02X", byte); 268 | result = std::string("Position ") + std::to_string(position) + ": " + hex; 269 | } 270 | } 271 | return result; 272 | } 273 | 274 | void SharpAcCore::processUpdate(SharpFrame &frame) 275 | { 276 | if (frame.getSize() == 0 || frame.getSize() == 1) { 277 | return; 278 | } 279 | 280 | // Status-Frames (18 Byte): Only Temperature (no state update needed) 281 | if (frame.getSize() == 18) 282 | { 283 | SharpStatusFrame *status = static_cast(&frame); 284 | this->currentTemperature = status->getTemperature(); 285 | hardware->log_debug(TAG, "Current temp: %.1f°C", this->currentTemperature); 286 | // Publish only temperature update without changing state 287 | this->publishUpdate(); 288 | } 289 | // Mode-Frames (14 Byte): Full State 290 | else if (frame.getSize() >= 14) 291 | { 292 | SharpModeFrame *status = static_cast(&frame); 293 | this->state.fan = status->getFanMode(); 294 | this->state.mode = status->getPowerMode(); 295 | this->state.state = status->getState(); 296 | this->state.swingH = status->getSwingHorizontal(); 297 | this->state.swingV = status->getSwingVertical(); 298 | this->state.preset = status->getPreset(); 299 | this->state.ion = status->getIon(); 300 | 301 | if (this->state.state) 302 | { 303 | if (this->state.mode == PowerMode::cool || this->state.mode == PowerMode::heat) 304 | this->state.temperature = status->getTemperature(); 305 | } 306 | 307 | // Only publish update if we have received at least one temperature reading 308 | // This prevents showing 0°C before the first status frame 309 | if (this->currentTemperature > 0.0f) { 310 | this->publishUpdate(); 311 | } else { 312 | hardware->log_debug(TAG, "Waiting for temperature reading..."); 313 | } 314 | } 315 | } 316 | 317 | void SharpAcCore::publishUpdate() 318 | { 319 | if (callback) { 320 | callback->on_state_update(); 321 | callback->on_ion_state_update(this->state.ion); 322 | callback->on_vane_horizontal_update(this->state.swingH); 323 | callback->on_vane_vertical_update(this->state.swingV); 324 | } 325 | } 326 | 327 | void SharpAcCore::setIon(bool state) 328 | { 329 | this->state.ion = state; 330 | SharpCommandFrame frame = this->state.toFrame(); 331 | this->write_frame(frame); 332 | } 333 | 334 | void SharpAcCore::setVaneHorizontal(SwingHorizontal state) 335 | { 336 | this->state.swingH = state; 337 | SharpCommandFrame frame = this->state.toFrame(); 338 | this->write_frame(frame); 339 | } 340 | 341 | void SharpAcCore::setVaneVertical(SwingVertical state) 342 | { 343 | this->state.swingV = state; 344 | SharpCommandFrame frame = this->state.toFrame(); 345 | this->write_frame(frame); 346 | } 347 | 348 | void SharpAcCore::controlMode(PowerMode mode, bool state) 349 | { 350 | this->state.state = state; 351 | if (state) 352 | this->state.mode = mode; 353 | SharpCommandFrame frame = this->state.toFrame(); 354 | this->write_frame(frame); 355 | } 356 | 357 | void SharpAcCore::controlFan(FanMode fan) 358 | { 359 | this->state.fan = fan; 360 | SharpCommandFrame frame = this->state.toFrame(); 361 | this->write_frame(frame); 362 | } 363 | 364 | void SharpAcCore::controlSwing(SwingHorizontal h, SwingVertical v) 365 | { 366 | this->state.swingH = h; 367 | this->state.swingV = v; 368 | SharpCommandFrame frame = this->state.toFrame(); 369 | this->write_frame(frame); 370 | } 371 | 372 | void SharpAcCore::controlTemperature(int temperature) 373 | { 374 | this->state.temperature = temperature; 375 | SharpCommandFrame frame = this->state.toFrame(); 376 | this->write_frame(frame); 377 | } 378 | 379 | void SharpAcCore::controlPreset(Preset preset) 380 | { 381 | this->state.preset = preset; 382 | SharpCommandFrame frame = this->state.toFrame(); 383 | this->write_frame(frame); 384 | } 385 | 386 | void SharpAcCore::resetConnection() 387 | { 388 | this->status = 0; 389 | this->awaitingResponse = false; 390 | this->connectionStart = 0; 391 | 392 | if (callback) { 393 | callback->on_connection_status_update(0); 394 | } 395 | } 396 | 397 | void SharpAcCore::checkTimeout() 398 | { 399 | if (!awaitingResponse) { 400 | return; 401 | } 402 | 403 | unsigned long currentMillis = hardware->get_millis(); 404 | if (currentMillis - lastRequestTime >= responseTimeout) { 405 | hardware->log_debug(TAG, "Timeout - no response for 10s, reconnecting..."); 406 | resetConnection(); 407 | } 408 | } 409 | 410 | void SharpAcCore::loop() 411 | { 412 | unsigned long currentMillis = hardware->get_millis(); 413 | 414 | checkTimeout(); 415 | 416 | if (hardware->available() > 0) { 417 | 418 | SharpFrame frame = this->readMsg(); 419 | frame.print(); 420 | 421 | if (status < 8) 422 | { 423 | this->init(frame); 424 | } 425 | else 426 | { 427 | this->processUpdate(frame); 428 | if (frame.getSize() > 1) { 429 | this->write_ack(); 430 | } 431 | } 432 | 433 | 434 | } 435 | if (this->status != 8) 436 | { 437 | this->startInit(); 438 | } 439 | else 440 | { 441 | if (currentMillis - previousMillis >= interval) 442 | { 443 | previousMillis = currentMillis; 444 | 445 | SharpFrame frame(get_status, sizeof(get_status) + 1); 446 | this->write_frame(frame); 447 | } 448 | } 449 | 450 | } 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /tests/test_core_logic.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // Include the core implementation 8 | #include "core_logic.h" 9 | #include "core_frame.h" 10 | #include "core_messages.h" 11 | #include "core_types.h" 12 | #include "core_state.h" 13 | 14 | using namespace esphome::sharp_ac; 15 | 16 | // ============================================================================ 17 | // Mock Hardware Interface for Testing 18 | // ============================================================================ 19 | 20 | class MockHardwareInterface : public SharpAcHardwareInterface { 21 | public: 22 | std::vector uart_buffer; 23 | std::vector> sent_frames; 24 | unsigned long mock_millis = 0; 25 | size_t read_position = 0; 26 | 27 | size_t read_array(uint8_t *data, size_t len) override { 28 | size_t bytes_read = 0; 29 | while (bytes_read < len && read_position < uart_buffer.size()) { 30 | data[bytes_read++] = uart_buffer[read_position++]; 31 | } 32 | return bytes_read; 33 | } 34 | 35 | size_t available() override { 36 | return uart_buffer.size() - read_position; 37 | } 38 | 39 | void write_array(const uint8_t *data, size_t len) override { 40 | std::vector frame(data, data + len); 41 | sent_frames.push_back(frame); 42 | } 43 | 44 | uint8_t peek() override { 45 | if (read_position < uart_buffer.size()) { 46 | return uart_buffer[read_position]; 47 | } 48 | return 0; 49 | } 50 | 51 | uint8_t read() override { 52 | if (read_position < uart_buffer.size()) { 53 | return uart_buffer[read_position++]; 54 | } 55 | return 0; 56 | } 57 | 58 | unsigned long get_millis() override { 59 | return mock_millis; 60 | } 61 | 62 | void log_debug(const char* tag, const char* format, ...) override { 63 | #ifdef VERBOSE_TESTS 64 | va_list args; 65 | va_start(args, format); 66 | printf("[%s] ", tag); 67 | vprintf(format, args); 68 | printf("\n"); 69 | va_end(args); 70 | #else 71 | (void)tag; 72 | (void)format; 73 | #endif 74 | } 75 | 76 | std::string format_hex_pretty(const uint8_t *data, size_t len) override { 77 | std::string result; 78 | for (size_t i = 0; i < len; i++) { 79 | char buf[10]; 80 | snprintf(buf, sizeof(buf), "0x%02X", data[i]); 81 | if (i > 0) result += " "; 82 | result += buf; 83 | } 84 | return result; 85 | } 86 | 87 | // Helper methods for tests 88 | void add_incoming_frame(const uint8_t* data, size_t len) { 89 | uart_buffer.insert(uart_buffer.end(), data, data + len); 90 | } 91 | 92 | void clear_sent_frames() { 93 | sent_frames.clear(); 94 | } 95 | 96 | void reset_uart_buffer() { 97 | uart_buffer.clear(); 98 | read_position = 0; 99 | } 100 | }; 101 | 102 | // ============================================================================ 103 | // Mock State Callback for Tests 104 | // ============================================================================ 105 | 106 | class MockStateCallback : public SharpAcStateCallback { 107 | public: 108 | int state_update_count = 0; 109 | int ion_update_count = 0; 110 | int vane_h_update_count = 0; 111 | int vane_v_update_count = 0; 112 | 113 | void on_state_update() override { 114 | state_update_count++; 115 | } 116 | 117 | void on_ion_state_update(bool state) override { 118 | (void)state; 119 | ion_update_count++; 120 | } 121 | 122 | void on_vane_horizontal_update(SwingHorizontal val) override { 123 | (void)val; 124 | vane_h_update_count++; 125 | } 126 | 127 | void on_vane_vertical_update(SwingVertical val) override { 128 | (void)val; 129 | vane_v_update_count++; 130 | } 131 | 132 | void reset_counters() { 133 | state_update_count = 0; 134 | ion_update_count = 0; 135 | vane_h_update_count = 0; 136 | vane_v_update_count = 0; 137 | } 138 | }; 139 | 140 | // ============================================================================ 141 | // Test Helper Functions 142 | // ============================================================================ 143 | 144 | void print_test_header(const char* test_name) { 145 | std::cout << "\n=== Test: " << test_name << " ===" << std::endl; 146 | } 147 | 148 | void print_test_result(const char* test_name, bool passed) { 149 | if (passed) { 150 | std::cout << "✓ " << test_name << " passed" << std::endl; 151 | } else { 152 | std::cout << "✗ " << test_name << " FAILED" << std::endl; 153 | } 154 | } 155 | 156 | // ============================================================================ 157 | // Test Cases 158 | // ============================================================================ 159 | 160 | /** 161 | * Test 1: Initialization 162 | * Verifies that SharpAcCore initializes correctly 163 | */ 164 | bool test_initialization() { 165 | print_test_header("Initialization"); 166 | 167 | MockHardwareInterface hw; 168 | MockStateCallback callback; 169 | SharpAcCore core(&hw, &callback); 170 | 171 | core.setup(); 172 | 173 | // State should initially be at default values 174 | const SharpState& state = core.getState(); 175 | 176 | bool passed = true; 177 | passed &= (state.state == false); // Initial aus 178 | passed &= (state.temperature == 25); // Default-Temperatur 179 | 180 | print_test_result("Initialization", passed); 181 | return passed; 182 | } 183 | 184 | /** 185 | * Test 2: Init Sequence 186 | * Verifies that the initialization sequence runs correctly 187 | * Based on the sequence from working_example 188 | */ 189 | bool test_init_sequence() { 190 | print_test_header("Init Sequence"); 191 | 192 | MockHardwareInterface hw; 193 | MockStateCallback callback; 194 | SharpAcCore core(&hw, &callback); 195 | 196 | core.setup(); 197 | 198 | // Starte Init 199 | hw.mock_millis = 0; 200 | core.loop(); 201 | 202 | // Should send init_msg 203 | bool passed = (hw.sent_frames.size() >= 1); 204 | 205 | if (passed) { 206 | // Check if init_msg was sent 207 | const auto& first_frame = hw.sent_frames[0]; 208 | passed &= (first_frame.size() > 0); 209 | passed &= (first_frame[0] == init_msg[0]); // Should start with header 210 | } 211 | 212 | print_test_result("Init Sequence", passed); 213 | return passed; 214 | } 215 | 216 | /** 217 | * Test 3: Frame Parsing - Cool Mode 218 | * Verifies that Cool mode frames are parsed correctly 219 | */ 220 | bool test_parse_cool_mode() { 221 | print_test_header("Parse Cool Mode Frame"); 222 | 223 | // Frame from logs/Modes.txt: Cool mode 224 | uint8_t cool_frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xcf, 0x31, 0x32, 0x00, 0xf9, 0x80, 0x00, 0xe4, 0x81, 0x8a}; 225 | 226 | SharpModeFrame frame(cool_frame); 227 | 228 | bool passed = true; 229 | passed &= (frame.getPowerMode() == PowerMode::cool); 230 | passed &= (frame.getState() == true); // 0x31 = On 231 | passed &= (frame.getTemperature() == 31); // 0xcf = 31°C 232 | 233 | print_test_result("Parse Cool Mode Frame", passed); 234 | return passed; 235 | } 236 | 237 | /** 238 | * Test 4: Frame Parsing - Eco Mode 239 | * Verifies that Eco mode frames are parsed correctly 240 | */ 241 | bool test_parse_eco_mode() { 242 | print_test_header("Parse Eco Mode Frame"); 243 | 244 | // Frame from logs/Modes.txt: Cool Eco 245 | uint8_t eco_frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xcf, 0x61, 0x32, 0x10, 0xf9, 0x80, 0x00, 0xf4, 0xd1, 0xea}; 246 | 247 | SharpModeFrame frame(eco_frame); 248 | 249 | bool passed = true; 250 | passed &= (frame.getPowerMode() == PowerMode::cool); 251 | passed &= (frame.getState() == true); // 0x61 = On with Preset 252 | passed &= (frame.getPreset() == Preset::ECO); // 0x10 = Eco 253 | passed &= (frame.getTemperature() == 31); 254 | 255 | print_test_result("Parse Eco Mode Frame", passed); 256 | return passed; 257 | } 258 | 259 | /** 260 | * Test 5: Frame Parsing - Full Power Mode 261 | * Verifies that Full Power frames are parsed correctly 262 | */ 263 | bool test_parse_full_power_mode() { 264 | print_test_header("Parse Full Power Mode Frame"); 265 | 266 | // Frame from logs/Modes.txt: Full Power (Cool + Full Power) 267 | uint8_t fullpower_frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xcf, 0x61, 0x22, 0x00, 0xf8, 0x80, 0x01, 0xe4, 0xc1, 0x2a}; 268 | 269 | SharpModeFrame frame(fullpower_frame); 270 | 271 | bool passed = true; 272 | passed &= (frame.getPowerMode() == PowerMode::cool); // byte[6]=0x22, lower nibble 0x2 = Cool 273 | passed &= (frame.getState() == true); 274 | passed &= (frame.getPreset() == Preset::FULLPOWER); // byte[10]=0x01 for command frames 275 | passed &= (frame.getTemperature() == 31); 276 | 277 | print_test_result("Parse Full Power Mode Frame", passed); 278 | return passed; 279 | } 280 | 281 | /** 282 | * Test 6: Frame Parsing - Fan Modes 283 | * Verifiziert, dass verschiedene Lüfterstufen korrekt geparst werden 284 | */ 285 | bool test_parse_fan_modes() { 286 | print_test_header("Parse Fan Modes"); 287 | 288 | bool passed = true; 289 | 290 | // Test Auto Fan (aus Cool frame) 291 | uint8_t cool_frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xcf, 0x31, 0x32, 0x00, 0xf9, 0x80, 0x00, 0xe4, 0x81, 0x8a}; 292 | SharpModeFrame frame1(cool_frame); 293 | // Byte 6 = 0x32: bits 0-3 sind Fan Mode 294 | // 0x32 & 0x0F = 0x02 = Mid 295 | passed &= (frame1.getFanMode() == FanMode::mid); 296 | 297 | print_test_result("Parse Fan Modes", passed); 298 | return passed; 299 | } 300 | 301 | /** 302 | * Test 7: Frame Parsing - Temperature Range 303 | * Verifies that temperatures in the range 16-30°C are parsed correctly 304 | */ 305 | bool test_parse_temperature_range() { 306 | print_test_header("Parse Temperature Range"); 307 | 308 | bool passed = true; 309 | 310 | // Temperatur = (byte[4] & 0x0F) + 16 311 | // 16°C: 0xC0, 31°C: 0xCF 312 | 313 | uint8_t frame_16c[] = {0xdd, 0x0b, 0xfb, 0x60, 0xc0, 0x31, 0x32, 0x00, 0xf9, 0x80, 0x00, 0xe4, 0x81, 0x8a}; 314 | SharpModeFrame test_16c(frame_16c); 315 | passed &= (test_16c.getTemperature() == 16); 316 | 317 | uint8_t frame_25c[] = {0xdd, 0x0b, 0xfb, 0x60, 0xc9, 0x31, 0x32, 0x00, 0xf9, 0x80, 0x00, 0xe4, 0x81, 0x8a}; 318 | SharpModeFrame test_25c(frame_25c); 319 | passed &= (test_25c.getTemperature() == 25); 320 | 321 | uint8_t frame_30c[] = {0xdd, 0x0b, 0xfb, 0x60, 0xce, 0x31, 0x32, 0x00, 0xf9, 0x80, 0x00, 0xe4, 0x81, 0x8a}; 322 | SharpModeFrame test_30c(frame_30c); 323 | passed &= (test_30c.getTemperature() == 30); 324 | 325 | print_test_result("Parse Temperature Range", passed); 326 | return passed; 327 | } 328 | 329 | /** 330 | * Test 8: Frame Parsing - Swing Modes 331 | * Verifies that swing modes are parsed correctly 332 | */ 333 | bool test_parse_swing_modes() { 334 | print_test_header("Parse Swing Modes"); 335 | 336 | // Frame with swing info (byte 8 = 0xf9 for command frames) 337 | // byte[8] = 0xf9: SwingV = 0x9 = highest, SwingH = 0xF = swing 338 | uint8_t frame[] = {0xdd, 0x0b, 0xfb, 0x60, 0xcf, 0x31, 0x32, 0x00, 0xf9, 0x80, 0x00, 0xe4, 0x81, 0x8a}; 339 | SharpModeFrame test_frame(frame); 340 | 341 | bool passed = true; 342 | SwingHorizontal h = test_frame.getSwingHorizontal(); 343 | SwingVertical v = test_frame.getSwingVertical(); 344 | 345 | // Validate correct swing values from frame 346 | passed &= (v == SwingVertical::highest); // 0x9 from byte[8] lower nibble 347 | passed &= (h == SwingHorizontal::swing); // 0xF from byte[8] upper nibble 348 | 349 | print_test_result("Parse Swing Modes", passed); 350 | return passed; 351 | } 352 | 353 | /** 354 | * Test 9: processUpdate Integration 355 | * Verifies that processUpdate correctly updates the state 356 | */ 357 | bool test_process_update() { 358 | print_test_header("Process Update Integration"); 359 | 360 | MockHardwareInterface hw; 361 | MockStateCallback callback; 362 | SharpAcCore core(&hw, &callback); 363 | 364 | core.setup(); 365 | 366 | // Simulate successful initialization through multiple loop() calls 367 | // Status must be 8 for processUpdate to be called 368 | for (int i = 0; i < 20; i++) { 369 | hw.mock_millis += 100; // Move time forward 370 | core.loop(); 371 | } 372 | 373 | hw.reset_uart_buffer(); 374 | callback.reset_counters(); 375 | 376 | // Simulate incoming 14-byte response mode frame (Cool Mode) 377 | // Frame from logs/Modes.txt: Response frame with Cool mode 378 | uint8_t response_frame[] = {0xdc, 0x0b, 0xfc, 0x73, 0x1a, 0x22, 0x18, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0xb2}; 379 | std::cout << " Debug: Adding " << sizeof(response_frame) << " bytes to UART buffer" << std::endl; 380 | hw.add_incoming_frame(response_frame, sizeof(response_frame)); 381 | std::cout << " Debug: UART buffer has " << hw.available() << " bytes available" << std::endl; 382 | 383 | // Trigger loop to process 384 | core.loop(); 385 | 386 | const SharpState& state = core.getState(); 387 | 388 | std::cout << " Debug: state_update_count = " << callback.state_update_count << std::endl; 389 | std::cout << " Debug: state.mode = " << static_cast(state.mode) << " (Cool=2)" << std::endl; 390 | std::cout << " Debug: state.temperature = " << state.temperature << std::endl; 391 | 392 | bool passed = true; 393 | passed &= (callback.state_update_count > 0 || state.mode == PowerMode::cool); 394 | 395 | print_test_result("Process Update Integration", passed); 396 | return passed; 397 | } 398 | 399 | /** 400 | * Test 10: Control Mode 401 | * Verifies that controlMode works correctly 402 | */ 403 | bool test_control_mode() { 404 | print_test_header("Control Mode"); 405 | 406 | MockHardwareInterface hw; 407 | MockStateCallback callback; 408 | SharpAcCore core(&hw, &callback); 409 | 410 | core.setup(); 411 | hw.clear_sent_frames(); 412 | 413 | // Set mode to Cool 414 | core.controlMode(PowerMode::cool, true); 415 | 416 | // Should send a frame 417 | bool passed = (hw.sent_frames.size() > 0); 418 | 419 | if (passed) { 420 | const SharpState& state = core.getState(); 421 | passed &= (state.mode == PowerMode::cool); 422 | passed &= (state.state == true); 423 | } 424 | 425 | print_test_result("Control Mode", passed); 426 | return passed; 427 | } 428 | 429 | /** 430 | * Test 11: Control Temperature 431 | * Verifies that controlTemperature works correctly 432 | */ 433 | bool test_control_temperature() { 434 | print_test_header("Control Temperature"); 435 | 436 | MockHardwareInterface hw; 437 | MockStateCallback callback; 438 | SharpAcCore core(&hw, &callback); 439 | 440 | core.setup(); 441 | hw.clear_sent_frames(); 442 | 443 | // Set temperature 444 | core.controlTemperature(24); 445 | 446 | const SharpState& state = core.getState(); 447 | bool passed = (state.temperature == 24); 448 | 449 | // Should send a frame 450 | passed &= (hw.sent_frames.size() > 0); 451 | 452 | print_test_result("Control Temperature", passed); 453 | return passed; 454 | } 455 | 456 | /** 457 | * Test 12: Control Fan 458 | * Verifies that controlFan works correctly 459 | */ 460 | bool test_control_fan() { 461 | print_test_header("Control Fan"); 462 | 463 | MockHardwareInterface hw; 464 | MockStateCallback callback; 465 | SharpAcCore core(&hw, &callback); 466 | 467 | core.setup(); 468 | hw.clear_sent_frames(); 469 | 470 | core.controlFan(FanMode::high); 471 | 472 | const SharpState& state = core.getState(); 473 | bool passed = (state.fan == FanMode::high); 474 | 475 | passed &= (hw.sent_frames.size() > 0); 476 | 477 | print_test_result("Control Fan", passed); 478 | return passed; 479 | } 480 | 481 | /** 482 | * Test 13: Control Preset 483 | * Verifies that controlPreset works correctly 484 | */ 485 | bool test_control_preset() { 486 | print_test_header("Control Preset"); 487 | 488 | MockHardwareInterface hw; 489 | MockStateCallback callback; 490 | SharpAcCore core(&hw, &callback); 491 | 492 | core.setup(); 493 | hw.clear_sent_frames(); 494 | 495 | core.controlPreset(Preset::ECO); 496 | 497 | const SharpState& state = core.getState(); 498 | bool passed = (state.preset == Preset::ECO); 499 | 500 | passed &= (hw.sent_frames.size() > 0); 501 | 502 | print_test_result("Control Preset", passed); 503 | return passed; 504 | } 505 | 506 | /** 507 | * Test 14: Ion Control 508 | * Verifies that setIon works correctly 509 | */ 510 | bool test_ion_control() { 511 | print_test_header("Ion Control"); 512 | 513 | MockHardwareInterface hw; 514 | MockStateCallback callback; 515 | SharpAcCore core(&hw, &callback); 516 | 517 | core.setup(); 518 | hw.clear_sent_frames(); 519 | 520 | core.setIon(true); 521 | 522 | const SharpState& state = core.getState(); 523 | bool passed = (state.ion == true); 524 | 525 | print_test_result("Ion Control", passed); 526 | return passed; 527 | } 528 | 529 | /** 530 | * Test 15: Vane Control 531 | * Verifies that vane control works correctly 532 | */ 533 | bool test_vane_control() { 534 | print_test_header("Vane Control"); 535 | 536 | MockHardwareInterface hw; 537 | MockStateCallback callback; 538 | SharpAcCore core(&hw, &callback); 539 | 540 | core.setup(); 541 | hw.clear_sent_frames(); 542 | 543 | core.setVaneHorizontal(SwingHorizontal::swing); 544 | core.setVaneVertical(SwingVertical::swing); 545 | 546 | const SharpState& state = core.getState(); 547 | bool passed = true; 548 | passed &= (state.swingH == SwingHorizontal::swing); 549 | passed &= (state.swingV == SwingVertical::swing); 550 | 551 | print_test_result("Vane Control", passed); 552 | return passed; 553 | } 554 | 555 | // ============================================================================ 556 | // Main Test Runner 557 | // ============================================================================ 558 | 559 | int main() { 560 | std::cout << "\n╔════════════════════════════════════════════════════════════╗" << std::endl; 561 | std::cout << "║ Sharp AC Core Unit Tests (Refactored Component) ║" << std::endl; 562 | std::cout << "╚════════════════════════════════════════════════════════════╝" << std::endl; 563 | 564 | int passed = 0; 565 | int total = 0; 566 | 567 | #define RUN_TEST(test_func) \ 568 | total++; \ 569 | if (test_func()) passed++; 570 | 571 | // Frame Parsing Tests 572 | RUN_TEST(test_initialization); 573 | RUN_TEST(test_init_sequence); 574 | RUN_TEST(test_parse_cool_mode); 575 | RUN_TEST(test_parse_eco_mode); 576 | RUN_TEST(test_parse_full_power_mode); 577 | RUN_TEST(test_parse_fan_modes); 578 | RUN_TEST(test_parse_temperature_range); 579 | RUN_TEST(test_parse_swing_modes); 580 | 581 | // Integration Tests 582 | RUN_TEST(test_process_update); 583 | 584 | // Control Tests 585 | RUN_TEST(test_control_mode); 586 | RUN_TEST(test_control_temperature); 587 | RUN_TEST(test_control_fan); 588 | RUN_TEST(test_control_preset); 589 | RUN_TEST(test_ion_control); 590 | RUN_TEST(test_vane_control); 591 | 592 | std::cout << "\n╔════════════════════════════════════════════════════════════╗" << std::endl; 593 | std::cout << "║ Test Summary ║" << std::endl; 594 | std::cout << "╠════════════════════════════════════════════════════════════╣" << std::endl; 595 | printf("║ Tests Passed: %2d / %2d ║\n", passed, total); 596 | std::cout << "╚════════════════════════════════════════════════════════════╝" << std::endl; 597 | 598 | if (passed == total) { 599 | std::cout << "\n✓✓✓ All tests passed! ✓✓✓\n" << std::endl; 600 | return 0; 601 | } else { 602 | std::cout << "\n✗✗✗ Some tests failed! ✗✗✗\n" << std::endl; 603 | return 1; 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /tests/test_integration.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "core_logic.h" 8 | #include "core_frame.h" 9 | #include "core_messages.h" 10 | #include "core_types.h" 11 | #include "core_state.h" 12 | 13 | using namespace esphome::sharp_ac; 14 | 15 | // ============================================================================ 16 | // Mock Hardware Interface 17 | // ============================================================================ 18 | 19 | class TestHardwareInterface : public SharpAcHardwareInterface { 20 | private: 21 | std::vector uart_rx_buffer; 22 | std::vector uart_tx_buffer; 23 | size_t read_pos = 0; 24 | unsigned long current_millis = 0; 25 | 26 | public: 27 | std::vector> captured_frames; 28 | 29 | size_t read_array(uint8_t *data, size_t len) override { 30 | size_t bytes_read = 0; 31 | while (bytes_read < len && read_pos < uart_rx_buffer.size()) { 32 | data[bytes_read++] = uart_rx_buffer[read_pos++]; 33 | } 34 | return bytes_read; 35 | } 36 | 37 | size_t available() override { 38 | return uart_rx_buffer.size() - read_pos; 39 | } 40 | 41 | void write_array(const uint8_t *data, size_t len) override { 42 | std::vector frame(data, data + len); 43 | captured_frames.push_back(frame); 44 | uart_tx_buffer.insert(uart_tx_buffer.end(), data, data + len); 45 | } 46 | 47 | uint8_t peek() override { 48 | if (read_pos < uart_rx_buffer.size()) { 49 | return uart_rx_buffer[read_pos]; 50 | } 51 | return 0; 52 | } 53 | 54 | uint8_t read() override { 55 | if (read_pos < uart_rx_buffer.size()) { 56 | return uart_rx_buffer[read_pos++]; 57 | } 58 | return 0; 59 | } 60 | 61 | unsigned long get_millis() override { 62 | return current_millis; 63 | } 64 | 65 | void log_debug(const char* tag, const char* format, ...) override { 66 | #ifdef VERBOSE_TESTS 67 | va_list args; 68 | va_start(args, format); 69 | printf("[%s] ", tag); 70 | vprintf(format, args); 71 | printf("\n"); 72 | va_end(args); 73 | #else 74 | (void)tag; 75 | (void)format; 76 | #endif 77 | } 78 | 79 | std::string format_hex_pretty(const uint8_t *data, size_t len) override { 80 | std::string result; 81 | for (size_t i = 0; i < len; i++) { 82 | char buf[10]; 83 | snprintf(buf, sizeof(buf), "0x%02X", data[i]); 84 | if (i > 0) result += " "; 85 | result += buf; 86 | } 87 | return result; 88 | } 89 | 90 | // Test helpers 91 | void inject_rx_data(const uint8_t* data, size_t len) { 92 | uart_rx_buffer.insert(uart_rx_buffer.end(), data, data + len); 93 | } 94 | 95 | void advance_time(unsigned long ms) { 96 | current_millis += ms; 97 | } 98 | 99 | void reset() { 100 | uart_rx_buffer.clear(); 101 | uart_tx_buffer.clear(); 102 | captured_frames.clear(); 103 | read_pos = 0; 104 | } 105 | 106 | const std::vector& get_tx_buffer() const { 107 | return uart_tx_buffer; 108 | } 109 | }; 110 | 111 | class TestStateCallback : public SharpAcStateCallback { 112 | public: 113 | int update_count = 0; 114 | bool last_ion_state = false; 115 | SwingHorizontal last_swing_h = SwingHorizontal::left; 116 | SwingVertical last_swing_v = SwingVertical::lowest; 117 | 118 | void on_state_update() override { 119 | update_count++; 120 | } 121 | 122 | void on_ion_state_update(bool state) override { 123 | (void)state; 124 | last_ion_state = state; 125 | } 126 | 127 | void on_vane_horizontal_update(SwingHorizontal val) override { 128 | (void)val; 129 | last_swing_h = val; 130 | } 131 | 132 | void on_vane_vertical_update(SwingVertical val) override { 133 | (void)val; 134 | last_swing_v = val; 135 | } 136 | 137 | void reset() { 138 | update_count = 0; 139 | } 140 | }; 141 | 142 | // ============================================================================ 143 | // Test Utilities 144 | // ============================================================================ 145 | 146 | void print_frame_comparison(const char* name, const uint8_t* expected, const uint8_t* actual, size_t len) { 147 | std::cout << " Frame: " << name << std::endl; 148 | 149 | std::cout << " Expected: "; 150 | for (size_t i = 0; i < len; i++) { 151 | printf("0x%02X ", expected[i]); 152 | } 153 | std::cout << std::endl; 154 | 155 | std::cout << " Actual: "; 156 | for (size_t i = 0; i < len; i++) { 157 | printf("0x%02X ", actual[i]); 158 | } 159 | std::cout << std::endl; 160 | } 161 | 162 | bool compare_frames(const uint8_t* frame1, const uint8_t* frame2, size_t len) { 163 | return memcmp(frame1, frame2, len) == 0; 164 | } 165 | 166 | // ============================================================================ 167 | // Integration Tests 168 | // ============================================================================ 169 | /** 170 | * Test: Init Sequence Matching 171 | * Verifies that the initialization sequence matches the expected frames 172 | */ 173 | bool test_init_sequence_matching() { 174 | std::cout << "\n=== Test: Init Sequence Matching ===" << std::endl; 175 | 176 | TestHardwareInterface hw; 177 | TestStateCallback cb; 178 | SharpAcCore core(&hw, &cb); 179 | 180 | core.setup(); 181 | 182 | // Trigger initialization 183 | hw.advance_time(0); 184 | core.loop(); 185 | 186 | // Expect init_msg as first frame 187 | bool passed = hw.captured_frames.size() > 0; 188 | 189 | if (passed && hw.captured_frames.size() > 0) { 190 | const auto& first_frame = hw.captured_frames[0]; 191 | // Check if it starts with expected header 192 | passed &= (first_frame.size() > 0); 193 | passed &= (first_frame[0] == init_msg[0]); 194 | 195 | std::cout << " ✓ Init message sent" << std::endl; 196 | std::cout << " Frame size: " << first_frame.size() << " bytes" << std::endl; 197 | } else { 198 | std::cout << " ✗ No init message sent" << std::endl; 199 | } 200 | 201 | return passed; 202 | } 203 | 204 | /** 205 | * Test: Cool Mode Command Generation 206 | * Compares the generated Cool Mode command 207 | */ 208 | bool test_cool_mode_command_generation() { 209 | std::cout << "\n=== Test: Cool Mode Command Generation ===" << std::endl; 210 | 211 | TestHardwareInterface hw; 212 | TestStateCallback cb; 213 | SharpAcCore core(&hw, &cb); 214 | 215 | core.setup(); 216 | hw.reset(); 217 | 218 | core.controlMode(PowerMode::cool, true); 219 | core.controlTemperature(24); 220 | core.controlFan(FanMode::auto_fan); 221 | 222 | bool passed = hw.captured_frames.size() > 0; 223 | 224 | if (passed) { 225 | std::cout << " ✓ Cool mode command generated" << std::endl; 226 | std::cout << " Frames sent: " << hw.captured_frames.size() << std::endl; 227 | 228 | // Check state 229 | const SharpState& state = core.getState(); 230 | passed &= (state.mode == PowerMode::cool); 231 | passed &= (state.state == true); 232 | passed &= (state.temperature == 24); 233 | } 234 | 235 | return passed; 236 | } 237 | 238 | /** 239 | * Test: Frame Parsing Consistency 240 | * Verifies that incoming frames are parsed identically to the working example 241 | */ 242 | bool test_frame_parsing_consistency() { 243 | std::cout << "\n=== Test: Frame Parsing Consistency ===" << std::endl; 244 | 245 | struct TestCase { 246 | const char* name; 247 | uint8_t frame[14]; 248 | size_t size; 249 | PowerMode expected_mode; 250 | int expected_temp; 251 | bool expected_state; 252 | Preset expected_preset; 253 | }; 254 | 255 | TestCase test_cases[] = { 256 | { 257 | "Cool Mode", 258 | {0xdd, 0x0b, 0xfb, 0x60, 0xcf, 0x31, 0x32, 0x00, 0xf9, 0x80, 0x00, 0xe4, 0x81, 0x8a}, 259 | 14, 260 | PowerMode::cool, 261 | 31, 262 | true, 263 | Preset::NONE 264 | }, 265 | { 266 | "Cool Eco Mode", 267 | {0xdd, 0x0b, 0xfb, 0x60, 0xcf, 0x61, 0x32, 0x10, 0xf9, 0x80, 0x00, 0xf4, 0xd1, 0xea}, 268 | 14, 269 | PowerMode::cool, 270 | 31, 271 | true, 272 | Preset::ECO 273 | }, 274 | { 275 | "Full Power Mode", // Cool + Full Power (byte[6]=0x22, lower nibble 0x2 = Cool) 276 | {0xdd, 0x0b, 0xfb, 0x60, 0xcf, 0x61, 0x22, 0x00, 0xf8, 0x80, 0x01, 0xe4, 0xc1, 0x2a}, 277 | 14, 278 | PowerMode::cool, // Korrigiert: byte[6]=0x22 (lower nibble 0x2 = Cool) 279 | 31, 280 | true, 281 | Preset::FULLPOWER 282 | } 283 | }; 284 | 285 | bool all_passed = true; 286 | 287 | for (const auto& test : test_cases) { 288 | std::cout << "\n Testing: " << test.name << std::endl; 289 | 290 | SharpModeFrame frame(test.frame); 291 | 292 | bool test_passed = true; 293 | 294 | PowerMode parsed_mode = frame.getPowerMode(); 295 | int parsed_temp = frame.getTemperature(); 296 | bool parsed_state = frame.getState(); 297 | Preset parsed_preset = frame.getPreset(); 298 | 299 | if (parsed_mode != test.expected_mode) { 300 | std::cout << " ✗ Mode mismatch: expected " << static_cast(test.expected_mode) 301 | << ", got " << static_cast(parsed_mode) << std::endl; 302 | test_passed = false; 303 | } 304 | 305 | if (parsed_temp != test.expected_temp) { 306 | std::cout << " ✗ Temperature mismatch: expected " << test.expected_temp 307 | << ", got " << parsed_temp << std::endl; 308 | test_passed = false; 309 | } 310 | 311 | if (parsed_state != test.expected_state) { 312 | std::cout << " ✗ State mismatch: expected " << test.expected_state 313 | << ", got " << parsed_state << std::endl; 314 | test_passed = false; 315 | } 316 | 317 | if (parsed_preset != test.expected_preset) { 318 | std::cout << " ✗ Preset mismatch: expected " << static_cast(test.expected_preset) 319 | << ", got " << static_cast(parsed_preset) << std::endl; 320 | test_passed = false; 321 | } 322 | 323 | if (test_passed) { 324 | std::cout << " ✓ All fields parsed correctly" << std::endl; 325 | } 326 | 327 | all_passed &= test_passed; 328 | } 329 | 330 | return all_passed; 331 | } 332 | 333 | /** 334 | * Test: Temperature Control Accuracy 335 | * Verifies that temperature control works as in the Working Example 336 | */ 337 | bool test_temperature_control_accuracy() { 338 | std::cout << "\n=== Test: Temperature Control Accuracy ===" << std::endl; 339 | 340 | TestHardwareInterface hw; 341 | TestStateCallback cb; 342 | SharpAcCore core(&hw, &cb); 343 | 344 | core.setup(); 345 | 346 | bool passed = true; 347 | 348 | int test_temps[] = {16, 20, 24, 28, 30}; 349 | 350 | for (int temp : test_temps) { 351 | hw.reset(); 352 | core.controlTemperature(temp); 353 | 354 | const SharpState& state = core.getState(); 355 | if (state.temperature != temp) { 356 | std::cout << " ✗ Temperature " << temp << "°C not set correctly (got " 357 | << state.temperature << "°C)" << std::endl; 358 | passed = false; 359 | } else { 360 | std::cout << " ✓ Temperature " << temp << "°C set correctly" << std::endl; 361 | } 362 | } 363 | 364 | return passed; 365 | } 366 | 367 | /** 368 | * Test: Fan Mode Control 369 | * Verifies that all fan modes are set correctly 370 | */ 371 | bool test_fan_mode_control() { 372 | std::cout << "\n=== Test: Fan Mode Control ===" << std::endl; 373 | 374 | TestHardwareInterface hw; 375 | TestStateCallback cb; 376 | SharpAcCore core(&hw, &cb); 377 | 378 | core.setup(); 379 | 380 | bool passed = true; 381 | 382 | FanMode modes[] = {FanMode::auto_fan, FanMode::low, FanMode::mid, FanMode::high, FanMode::highest}; 383 | const char* mode_names[] = {"Auto", "Low", "Mid", "High", "Highest"}; 384 | 385 | for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) { 386 | hw.reset(); 387 | core.controlFan(modes[i]); 388 | 389 | const SharpState& state = core.getState(); 390 | if (state.fan != modes[i]) { 391 | std::cout << " ✗ Fan mode " << mode_names[i] << " not set correctly" << std::endl; 392 | passed = false; 393 | } else { 394 | std::cout << " ✓ Fan mode " << mode_names[i] << " set correctly" << std::endl; 395 | } 396 | } 397 | 398 | return passed; 399 | } 400 | 401 | /** 402 | * Test: Power Mode Control 403 | * Verifies that all power modes are set correctly 404 | */ 405 | bool test_power_mode_control() { 406 | std::cout << "\n=== Test: Power Mode Control ===" << std::endl; 407 | 408 | TestHardwareInterface hw; 409 | TestStateCallback cb; 410 | SharpAcCore core(&hw, &cb); 411 | 412 | core.setup(); 413 | 414 | bool passed = true; 415 | 416 | struct ModeTest { 417 | PowerMode mode; 418 | const char* name; 419 | }; 420 | 421 | ModeTest modes[] = { 422 | {PowerMode::cool, "Cool"}, 423 | {PowerMode::heat, "Heat"}, 424 | {PowerMode::dry, "Dry"}, 425 | {PowerMode::fan, "Fan"} 426 | }; 427 | 428 | for (const auto& test : modes) { 429 | hw.reset(); 430 | core.controlMode(test.mode, true); 431 | 432 | const SharpState& state = core.getState(); 433 | if (state.mode != test.mode || !state.state) { 434 | std::cout << " ✗ Mode " << test.name << " not set correctly" << std::endl; 435 | passed = false; 436 | } else { 437 | std::cout << " ✓ Mode " << test.name << " set correctly" << std::endl; 438 | } 439 | } 440 | 441 | // Test Power Off 442 | hw.reset(); 443 | core.controlMode(PowerMode::cool, false); 444 | const SharpState& state = core.getState(); 445 | if (state.state != false) { 446 | std::cout << " ✗ Power off not working" << std::endl; 447 | passed = false; 448 | } else { 449 | std::cout << " ✓ Power off working" << std::endl; 450 | } 451 | 452 | return passed; 453 | } 454 | 455 | /** 456 | * Test: Preset Control (Eco, Full Power) 457 | * Verifies that presets work as in the Working Example 458 | */ 459 | bool test_preset_control() { 460 | std::cout << "\n=== Test: Preset Control ===" << std::endl; 461 | 462 | TestHardwareInterface hw; 463 | TestStateCallback cb; 464 | SharpAcCore core(&hw, &cb); 465 | 466 | core.setup(); 467 | 468 | bool passed = true; 469 | 470 | // Test ECO Preset 471 | hw.reset(); 472 | core.controlPreset(Preset::ECO); 473 | 474 | const SharpState& state1 = core.getState(); 475 | if (state1.preset != Preset::ECO) { 476 | std::cout << " ✗ ECO preset not set correctly" << std::endl; 477 | passed = false; 478 | } else { 479 | std::cout << " ✓ ECO preset set correctly" << std::endl; 480 | } 481 | 482 | hw.reset(); 483 | core.controlPreset(Preset::FULLPOWER); 484 | 485 | const SharpState& state2 = core.getState(); 486 | if (state2.preset != Preset::FULLPOWER) { 487 | std::cout << " ✗ FULLPOWER preset not set correctly" << std::endl; 488 | passed = false; 489 | } else { 490 | std::cout << " ✓ FULLPOWER preset set correctly" << std::endl; 491 | } 492 | 493 | hw.reset(); 494 | core.controlPreset(Preset::NONE); 495 | 496 | const SharpState& state3 = core.getState(); 497 | if (state3.preset != Preset::NONE) { 498 | std::cout << " ✗ NONE preset not set correctly" << std::endl; 499 | passed = false; 500 | } else { 501 | std::cout << " ✓ NONE preset reset correctly" << std::endl; 502 | } 503 | 504 | return passed; 505 | } 506 | 507 | /** 508 | * Test: Swing Control 509 | * Verifies swing control (horizontal and vertical) 510 | */ 511 | bool test_swing_control() { 512 | std::cout << "\n=== Test: Swing Control ===" << std::endl; 513 | 514 | TestHardwareInterface hw; 515 | TestStateCallback cb; 516 | SharpAcCore core(&hw, &cb); 517 | 518 | core.setup(); 519 | 520 | bool passed = true; 521 | 522 | // Test Horizontal Swing 523 | hw.reset(); 524 | core.setVaneHorizontal(SwingHorizontal::swing); 525 | 526 | const SharpState& state1 = core.getState(); 527 | if (state1.swingH != SwingHorizontal::swing) { 528 | std::cout << " ✗ Horizontal swing not set" << std::endl; 529 | passed = false; 530 | } else { 531 | std::cout << " ✓ Horizontal swing set" << std::endl; 532 | } 533 | 534 | // Test Vertical Swing 535 | hw.reset(); 536 | core.setVaneVertical(SwingVertical::swing); 537 | 538 | const SharpState& state2 = core.getState(); 539 | if (state2.swingV != SwingVertical::swing) { 540 | std::cout << " ✗ Vertical swing not set" << std::endl; 541 | passed = false; 542 | } else { 543 | std::cout << " ✓ Vertical swing set" << std::endl; 544 | } 545 | 546 | return passed; 547 | } 548 | 549 | /** 550 | * Test: Ion Mode Control 551 | * Verifies Ion mode control 552 | */ 553 | bool test_ion_mode_control() { 554 | std::cout << "\n=== Test: Ion Mode Control ===" << std::endl; 555 | 556 | TestHardwareInterface hw; 557 | TestStateCallback cb; 558 | SharpAcCore core(&hw, &cb); 559 | 560 | core.setup(); 561 | 562 | bool passed = true; 563 | 564 | // Test Ion On 565 | hw.reset(); 566 | core.setIon(true); 567 | 568 | const SharpState& state1 = core.getState(); 569 | if (state1.ion != true) { 570 | std::cout << " ✗ Ion mode ON not set" << std::endl; 571 | passed = false; 572 | } else { 573 | std::cout << " ✓ Ion mode ON set" << std::endl; 574 | } 575 | 576 | // Test Ion Off 577 | hw.reset(); 578 | core.setIon(false); 579 | 580 | const SharpState& state2 = core.getState(); 581 | if (state2.ion != false) { 582 | std::cout << " ✗ Ion mode OFF not set" << std::endl; 583 | passed = false; 584 | } else { 585 | std::cout << " ✓ Ion mode OFF set" << std::endl; 586 | } 587 | 588 | return passed; 589 | } 590 | 591 | /** 592 | * Test: State Callback Invocation 593 | * Verifies that callbacks are invoked correctly 594 | */ 595 | bool test_state_callback_invocation() { 596 | std::cout << "\n=== Test: State Callback Invocation ===" << std::endl; 597 | 598 | TestHardwareInterface hw; 599 | TestStateCallback cb; 600 | SharpAcCore core(&hw, &cb); 601 | 602 | core.setup(); 603 | 604 | bool passed = true; 605 | 606 | int initial_count = cb.update_count; 607 | 608 | // Trigger State Update 609 | core.controlMode(PowerMode::cool, true); 610 | core.publishUpdate(); 611 | 612 | if (cb.update_count <= initial_count) { 613 | std::cout << " ✗ State callback not invoked" << std::endl; 614 | passed = false; 615 | } else { 616 | std::cout << " ✓ State callback invoked (" << (cb.update_count - initial_count) << " times)" << std::endl; 617 | } 618 | 619 | return passed; 620 | } 621 | 622 | // ============================================================================ 623 | // Main Test Runner 624 | // ============================================================================ 625 | 626 | int main() { 627 | std::cout << "\n╔══════════════════════════════════════════════════════════════╗" << std::endl; 628 | std::cout << "║ Integration Tests: Refactored vs Working Example ║" << std::endl; 629 | std::cout << "╚══════════════════════════════════════════════════════════════╝" << std::endl; 630 | 631 | int passed = 0; 632 | int total = 0; 633 | 634 | #define RUN_TEST(test_func) \ 635 | total++; \ 636 | if (test_func()) { \ 637 | passed++; \ 638 | std::cout << "✓ Test passed\n"; \ 639 | } else { \ 640 | std::cout << "✗ Test FAILED\n"; \ 641 | } 642 | 643 | // Initialization Tests 644 | RUN_TEST(test_init_sequence_matching); 645 | 646 | // Frame Parsing Tests 647 | RUN_TEST(test_frame_parsing_consistency); 648 | 649 | // Command Generation Tests 650 | RUN_TEST(test_cool_mode_command_generation); 651 | 652 | // Control Tests 653 | RUN_TEST(test_temperature_control_accuracy); 654 | RUN_TEST(test_fan_mode_control); 655 | RUN_TEST(test_power_mode_control); 656 | RUN_TEST(test_preset_control); 657 | RUN_TEST(test_swing_control); 658 | RUN_TEST(test_ion_mode_control); 659 | 660 | // Callback Tests 661 | RUN_TEST(test_state_callback_invocation); 662 | 663 | std::cout << "\n╔══════════════════════════════════════════════════════════════╗" << std::endl; 664 | std::cout << "║ Test Summary ║" << std::endl; 665 | std::cout << "╠══════════════════════════════════════════════════════════════╣" << std::endl; 666 | printf("║ Tests Passed: %2d / %2d ║\n", passed, total); 667 | std::cout << "╚══════════════════════════════════════════════════════════════╝" << std::endl; 668 | 669 | if (passed == total) { 670 | std::cout << "\n✓✓✓ All integration tests passed! ✓✓✓\n" << std::endl; 671 | std::cout << "The refactored component behaves identically to the working example.\n" << std::endl; 672 | return 0; 673 | } else { 674 | std::cout << "\n✗✗✗ Some integration tests failed! ✗✗✗\n" << std::endl; 675 | std::cout << "Review the differences between refactored and working example.\n" << std::endl; 676 | return 1; 677 | } 678 | } 679 | --------------------------------------------------------------------------------