From dcfc6a30fb7054d36128459cbefe95c891067cf5 Mon Sep 17 00:00:00 2001 From: Abko Wijma Date: Sat, 7 Mar 2026 14:34:07 +0100 Subject: [PATCH 1/4] Add ModuleHomeAutomation with Homebridge HTTP and HomeKit Native support Introduces a new standalone ModuleHomeAutomation that adds home automation integration selectable at runtime via a GUI dropdown: - Off: no integration active - Homebridge HTTP: logs the existing /api/lightscontrol endpoint URL for use with any homebridge-http-* plugin; zero extra firmware bytes - HomeKit Native (S3/P4 only): implements Apple HomeKit Accessory Protocol directly on the ESP32 via HomeSpan, enabling the device to appear in Apple Home without a Homebridge server The HomeKit Native option is gated behind FT_HOMEKIT=1 (added to S3/P4 board configs) to avoid flash overflow on esp32-d0. ModuleLightsControl is unchanged. State syncs bidirectionally between HomeKit and MoonLight. Co-Authored-By: Claude Sonnet 4.6 --- firmware/esp32-p4.ini | 6 +- firmware/esp32-s3.ini | 6 +- platformio.ini | 8 + src/MoonLight/Modules/ModuleHomeAutomation.h | 145 +++++++++++++++++++ src/main.cpp | 3 + 5 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 src/MoonLight/Modules/ModuleHomeAutomation.h diff --git a/firmware/esp32-p4.ini b/firmware/esp32-p4.ini index 0fa711b30..0e94ed08c 100644 --- a/firmware/esp32-p4.ini +++ b/firmware/esp32-p4.ini @@ -1,13 +1,15 @@ [esp32-p4-base] -build_flags = +build_flags = ${env.build_flags} ${moonlight.build_flags} ${HP_ALL_DRIVERS.build_flags} ;use the LedsDriver class only for setBrightness and setColorCorrection and LUT + ${homekit.build_flags} -D CONFIG_IDF_TARGET_ESP32P4=1 -lib_deps = +lib_deps = ${env.lib_deps} ${moonlight.lib_deps} ${HP_ALL_DRIVERS.lib_deps} + ${homekit.lib_deps} ; "possible fix if idf.py instakk fails ; platform = https://github.com/Jason2866/platform-espressif32.git#Arduino/IDF53 ; https://github.com/arendst/Tasmota/discussions/23758#discussioncomment-14010813 diff --git a/firmware/esp32-s3.ini b/firmware/esp32-s3.ini index 5c3648eda..91d66fd72 100644 --- a/firmware/esp32-s3.ini +++ b/firmware/esp32-s3.ini @@ -1,20 +1,22 @@ [esp32-s3-base] -build_flags = +build_flags = ${env.build_flags} ${moonlight.build_flags} ${livescripts.build_flags} ${HP_ALL_DRIVERS.build_flags} + ${homekit.build_flags} -D CONFIG_IDF_TARGET_ESP32S3=1 ; -D ARDUINO_USB_MODE=1 ; which USB device classes are enabled on your ESP32 at boot. default 1 in board definition (serial only) ; -D ARDUINO_USB_CDC_ON_BOOT=1 ;Communications Device Class: controls whether the ESP32's USB serial port is enabled automatically at boot, not set in board definition! ; -D ARDUINO_USB_MSC_ON_BOOT=0 ;Mass Storage Class, disable ; -D ARDUINO_USB_DFU_ON_BOOT=0 ;download firmware update, disable ; -D ML_LIVE_MAPPING -lib_deps = +lib_deps = ${env.lib_deps} ${moonlight.lib_deps} ${livescripts.lib_deps} ${HP_ALL_DRIVERS.lib_deps} + ${homekit.lib_deps} diff --git a/platformio.ini b/platformio.ini index e741b16c8..ed5fe555c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -264,6 +264,14 @@ lib_deps = ; https://github.com/hpwit/ESPLiveScript.git#9f002b4ec450cd47f5d417bfaddd616dd764d1f3 ; vjson 16-06-2025 ; Comment if FT_LIVESCRIPT=0 https://github.com/ewowi/ESPLiveScript.git#e02c2dccdd5044c9aa3edb874d26759afb0e1cb9 ; 02-11-2025 fix warnings +; 💫 HomeKit (HAP) support via HomeSpan — only enabled on boards with enough flash (S3/P4) +; Requires FT_HOMEKIT=1 in board build_flags. Off by default on esp32-d0 (flash too tight). +[homekit] +build_flags = + -D FT_HOMEKIT=1 +lib_deps = + HomeSpan/HomeSpan + ;For all boards [ESP32_LEDSDRIVER] build_flags = diff --git a/src/MoonLight/Modules/ModuleHomeAutomation.h b/src/MoonLight/Modules/ModuleHomeAutomation.h new file mode 100644 index 000000000..cc17a9203 --- /dev/null +++ b/src/MoonLight/Modules/ModuleHomeAutomation.h @@ -0,0 +1,145 @@ +/** + @title MoonLight + @file ModuleHomeAutomation.h + @repo https://github.com/MoonModules/MoonLight, submit changes to this file as PRs + @Authors https://github.com/MoonModules/MoonLight/commits/main + @Doc https://moonmodules.org/MoonLight/moonlight/homeautomation/ + @Copyright © 2026 Github MoonLight Commit Authors + @license GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 + @license For non GPL-v3 usage, commercial licenses must be purchased. Contact us for more information. +**/ + +#ifndef ModuleHomeAutomation_h +#define ModuleHomeAutomation_h + +#if FT_MOONLIGHT + +#include "MoonBase/Module.h" +#include "MoonLight/Modules/ModuleLightsControl.h" + +// 💫 HomeKit native support via HomeSpan (only compiled on boards with FT_HOMEKIT) +#if FT_ENABLED(FT_HOMEKIT) + #include "HomeSpan.h" + static void rgbToHsv(uint8_t r, uint8_t g, uint8_t b, float &h, float &s, float &v) { + float rf=r/255.f, gf=g/255.f, bf=b/255.f; + float mx=max(max(rf,gf),bf), mn=min(min(rf,gf),bf), d=mx-mn; + v=mx; s=(mx>0)?d/mx:0; + if(d==0){h=0;} else if(mx==rf){h=60.f*fmod((gf-bf)/d,6.f);} else if(mx==gf){h=60.f*((bf-rf)/d+2);} else{h=60.f*((rf-gf)/d+4);} + if(h<0)h+=360.f; + } + static void hsvToRgb(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) { + float c=v*s, x=c*(1.f-fabs(fmod(h/60.f,2.f)-1.f)), m=v-c, rf,gf,bf; + if(h<60){rf=c;gf=x;bf=0;} else if(h<120){rf=x;gf=c;bf=0;} else if(h<180){rf=0;gf=c;bf=x;} + else if(h<240){rf=0;gf=x;bf=c;} else if(h<300){rf=x;gf=0;bf=c;} else{rf=c;gf=0;bf=x;} + r=(uint8_t)((rf+m)*255.5f); g=(uint8_t)((gf+m)*255.5f); b=(uint8_t)((bf+m)*255.5f); + } + class ModuleHomeAutomation; + struct MoonLightBulb : Service::LightBulb { + SpanCharacteristic *power, *brightness, *hue, *saturation; + ModuleHomeAutomation *module; + explicit MoonLightBulb(ModuleHomeAutomation *mod); + boolean update() override; + }; +#endif + +// 💫 +class ModuleHomeAutomation : public Module { + private: + ModuleLightsControl *_lightsControl; +#if FT_ENABLED(FT_HOMEKIT) + MoonLightBulb *_lightBulb = nullptr; + bool _homeKitStarted = false; +#endif + + public: + ModuleHomeAutomation(PsychicHttpServer *server, ESP32SvelteKit *sveltekit, ModuleLightsControl *lightsControl) + : Module("homeautomation", server, sveltekit), _lightsControl(lightsControl) {} + + void begin() override { + Module::begin(); +#if FT_ENABLED(FT_HOMEKIT) + if (_state.data["homeAutomation"] == 2) startHomeKit(); + _lightsControl->addUpdateHandler([this](const String &originId) { if (originId != "homekit") syncToHomeKit(); }, false); +#endif + } + + void setupDefinition(const JsonArray &controls) override { + JsonObject control = addControl(controls, "homeAutomation", "select"); + control["default"] = 0; + JsonArray values = control["values"].to(); + values.add("Off"); + values.add("Homebridge HTTP"); +#if FT_ENABLED(FT_HOMEKIT) + values.add("HomeKit Native"); +#endif + } + + void onUpdate(const UpdatedItem &updatedItem) override { + if (updatedItem.name != "homeAutomation") return; + uint8_t mode = _state.data["homeAutomation"]; + if (mode == 1) + EXT_LOGI(ML_TAG, "Homebridge HTTP: configure plugin → http://%s/api/lightscontrol", + _sveltekit->getWiFiSettingsService()->getHostname().c_str()); +#if FT_ENABLED(FT_HOMEKIT) + if (mode == 2) { if (!_homeKitStarted) startHomeKit(); else syncToHomeKit(); } +#endif + } + +#if FT_ENABLED(FT_HOMEKIT) + void loop() override { if (_homeKitStarted) homeSpan.poll(); } + + void startHomeKit() { + _homeKitStarted = true; + String hn = _sveltekit->getWiFiSettingsService()->getHostname(); + homeSpan.setHostNameSuffix(""); homeSpan.setPortNum(1201); + homeSpan.begin(Category::Lighting, hn.c_str()); + new SpanAccessory(); + new Service::AccessoryInformation(); + new Characteristic::Name(hn.c_str()); new Characteristic::Manufacturer("MoonModules"); + new Characteristic::SerialNumber("MoonLight"); new Characteristic::Model("MoonLight"); + new Characteristic::FirmwareRevision("1.0.0"); new Characteristic::Identify(); + _lightBulb = new MoonLightBulb(this); + syncToHomeKit(); + EXT_LOGI(ML_TAG, "HomeKit started as \"%s\"", hn.c_str()); + } + + void syncToHomeKit() { + if (!_lightBulb) return; + _lightsControl->read([&](ModuleState &state) { + float h, s, v; + rgbToHsv(state.data["red"], state.data["green"], state.data["blue"], h, s, v); + _lightBulb->power->setVal(state.data["lightsOn"].as()); + _lightBulb->brightness->setVal((int)(state.data["brightness"].as() / 2.55f + 0.5f)); + _lightBulb->hue->setVal(h); _lightBulb->saturation->setVal(s * 100.f); + }, "homekit-sync"); + } + + void applyFromHomeKit(bool on, int hkBri, float h, float s) { + uint8_t r, g, b; + hsvToRgb(h, s / 100.f, 1.f, r, g, b); + JsonDocument doc; JsonObject ns = doc.to(); + ns["lightsOn"]=on; ns["brightness"]=(uint8_t)(hkBri*2.55f+0.5f); + ns["red"]=r; ns["green"]=g; ns["blue"]=b; + _lightsControl->update(ns, ModuleState::update, "homekit"); + } +#endif +}; // class ModuleHomeAutomation + +#if FT_ENABLED(FT_HOMEKIT) +MoonLightBulb::MoonLightBulb(ModuleHomeAutomation *mod) : Service::LightBulb(), module(mod) { + power=new Characteristic::On(false); brightness=new Characteristic::Brightness(20); + hue=new Characteristic::Hue(0); saturation=new Characteristic::Saturation(0); +} +boolean MoonLightBulb::update() { + if (!module) return false; + module->applyFromHomeKit( + power->updated() ? power->getNewVal() : power->getVal(), + brightness->updated() ? brightness->getNewVal() : brightness->getVal(), + hue->updated() ? hue->getNewVal() : hue->getVal(), + saturation->updated() ? saturation->getNewVal() : saturation->getVal()); + return true; +} +#endif + +#endif // FT_MOONLIGHT +#endif // ModuleHomeAutomation_h diff --git a/src/main.cpp b/src/main.cpp index 0e7fb5786..d72e937cc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -107,6 +107,8 @@ ModuleDrivers moduleDrivers = ModuleDrivers(&server, &esp32sveltekit, &fileManag #include "MoonLight/Modules/ModuleLiveScripts.h" ModuleLiveScripts moduleLiveScripts = ModuleLiveScripts(&server, &esp32sveltekit, &fileManager, &moduleEffects, &moduleDrivers); #endif + #include "MoonLight/Modules/ModuleHomeAutomation.h" +ModuleHomeAutomation moduleHomeAutomation = ModuleHomeAutomation(&server, &esp32sveltekit, &moduleLightsControl); ModuleChannels moduleChannels = ModuleChannels(&server, &esp32sveltekit); ModuleMoonLightInfo moduleMoonLightInfo = ModuleMoonLightInfo(&server, &esp32sveltekit); @@ -293,6 +295,7 @@ void setup() { modules.push_back(&moduleDevices); // In MoonLight for the time being, should move to MoonBase using moduleControlCenter ... modules.push_back(&moduleDrivers); modules.push_back(&moduleLightsControl); + modules.push_back(&moduleHomeAutomation); modules.push_back(&moduleChannels); modules.push_back(&moduleMoonLightInfo); #if FT_ENABLED(FT_LIVESCRIPT) From 161ccbf11183c675a572ad4b5c4e8b8e280ba46f Mon Sep 17 00:00:00 2001 From: Abko Wijma Date: Sat, 7 Mar 2026 14:45:44 +0100 Subject: [PATCH 2/4] Add docstrings to all functions in ModuleHomeAutomation Brings docstring coverage to 100% (13/13 functions documented). Co-Authored-By: Claude Sonnet 4.6 --- src/MoonLight/Modules/ModuleHomeAutomation.h | 27 +++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/MoonLight/Modules/ModuleHomeAutomation.h b/src/MoonLight/Modules/ModuleHomeAutomation.h index cc17a9203..74e0a1998 100644 --- a/src/MoonLight/Modules/ModuleHomeAutomation.h +++ b/src/MoonLight/Modules/ModuleHomeAutomation.h @@ -20,6 +20,8 @@ // 💫 HomeKit native support via HomeSpan (only compiled on boards with FT_HOMEKIT) #if FT_ENABLED(FT_HOMEKIT) #include "HomeSpan.h" + + /** @brief Convert RGB (0-255 each) to HSV. h is 0-360, s and v are 0-1. */ static void rgbToHsv(uint8_t r, uint8_t g, uint8_t b, float &h, float &s, float &v) { float rf=r/255.f, gf=g/255.f, bf=b/255.f; float mx=max(max(rf,gf),bf), mn=min(min(rf,gf),bf), d=mx-mn; @@ -27,22 +29,35 @@ if(d==0){h=0;} else if(mx==rf){h=60.f*fmod((gf-bf)/d,6.f);} else if(mx==gf){h=60.f*((bf-rf)/d+2);} else{h=60.f*((rf-gf)/d+4);} if(h<0)h+=360.f; } + + /** @brief Convert HSV (h: 0-360, s: 0-1, v: 0-1) to RGB (0-255 each). */ static void hsvToRgb(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) { float c=v*s, x=c*(1.f-fabs(fmod(h/60.f,2.f)-1.f)), m=v-c, rf,gf,bf; if(h<60){rf=c;gf=x;bf=0;} else if(h<120){rf=x;gf=c;bf=0;} else if(h<180){rf=0;gf=c;bf=x;} else if(h<240){rf=0;gf=x;bf=c;} else if(h<300){rf=x;gf=0;bf=c;} else{rf=c;gf=0;bf=x;} r=(uint8_t)((rf+m)*255.5f); g=(uint8_t)((gf+m)*255.5f); b=(uint8_t)((bf+m)*255.5f); } + class ModuleHomeAutomation; + + /** @brief HomeSpan LightBulb service bridging HomeKit commands to ModuleLightsControl state. */ struct MoonLightBulb : Service::LightBulb { SpanCharacteristic *power, *brightness, *hue, *saturation; ModuleHomeAutomation *module; + /** @brief Initialise HomeKit characteristics and store back-reference to the owning module. */ explicit MoonLightBulb(ModuleHomeAutomation *mod); + /** @brief Called by HomeSpan when Apple Home sends a command; forwards to applyFromHomeKit(). */ boolean update() override; }; #endif -// 💫 +/** @brief Module that exposes a home automation mode selector in the MoonLight UI. + * + * Supported modes (runtime-selectable via the GUI dropdown): + * - 0 Off — no integration active + * - 1 Homebridge HTTP — logs the existing /api/lightscontrol URL for use with an HTTP Homebridge plugin; zero extra flash + * - 2 HomeKit Native — starts a HomeKit Accessory Protocol server via HomeSpan (S3/P4 only, requires FT_HOMEKIT=1) + */ class ModuleHomeAutomation : public Module { private: ModuleLightsControl *_lightsControl; @@ -52,9 +67,11 @@ class ModuleHomeAutomation : public Module { #endif public: + /** @brief Construct the module, storing a reference to ModuleLightsControl for bidirectional state sync. */ ModuleHomeAutomation(PsychicHttpServer *server, ESP32SvelteKit *sveltekit, ModuleLightsControl *lightsControl) : Module("homeautomation", server, sveltekit), _lightsControl(lightsControl) {} + /** @brief Start HomeKit if the persisted mode is HomeKit Native, and register a state-change listener on ModuleLightsControl. */ void begin() override { Module::begin(); #if FT_ENABLED(FT_HOMEKIT) @@ -63,6 +80,7 @@ class ModuleHomeAutomation : public Module { #endif } + /** @brief Register the homeAutomation select control. Options depend on compile-time FT_HOMEKIT flag. */ void setupDefinition(const JsonArray &controls) override { JsonObject control = addControl(controls, "homeAutomation", "select"); control["default"] = 0; @@ -74,6 +92,7 @@ class ModuleHomeAutomation : public Module { #endif } + /** @brief React to homeAutomation mode changes: log the Homebridge URL or start the HomeKit server. */ void onUpdate(const UpdatedItem &updatedItem) override { if (updatedItem.name != "homeAutomation") return; uint8_t mode = _state.data["homeAutomation"]; @@ -86,8 +105,10 @@ class ModuleHomeAutomation : public Module { } #if FT_ENABLED(FT_HOMEKIT) + /** @brief Poll the HomeSpan HAP server every loop iteration while HomeKit Native mode is active. */ void loop() override { if (_homeKitStarted) homeSpan.poll(); } + /** @brief Initialise the HomeSpan accessory tree and start the HAP server. Called once on first activation. */ void startHomeKit() { _homeKitStarted = true; String hn = _sveltekit->getWiFiSettingsService()->getHostname(); @@ -103,6 +124,7 @@ class ModuleHomeAutomation : public Module { EXT_LOGI(ML_TAG, "HomeKit started as \"%s\"", hn.c_str()); } + /** @brief Push the current ModuleLightsControl state (on/off, brightness, RGB→HSV) into HomeKit characteristics. */ void syncToHomeKit() { if (!_lightBulb) return; _lightsControl->read([&](ModuleState &state) { @@ -114,6 +136,7 @@ class ModuleHomeAutomation : public Module { }, "homekit-sync"); } + /** @brief Apply a HomeKit command to ModuleLightsControl state, converting HSV→RGB and HomeKit brightness to 0-255. */ void applyFromHomeKit(bool on, int hkBri, float h, float s) { uint8_t r, g, b; hsvToRgb(h, s / 100.f, 1.f, r, g, b); @@ -126,10 +149,12 @@ class ModuleHomeAutomation : public Module { }; // class ModuleHomeAutomation #if FT_ENABLED(FT_HOMEKIT) +/** @brief Initialise all four HomeKit characteristics with sensible defaults. */ MoonLightBulb::MoonLightBulb(ModuleHomeAutomation *mod) : Service::LightBulb(), module(mod) { power=new Characteristic::On(false); brightness=new Characteristic::Brightness(20); hue=new Characteristic::Hue(0); saturation=new Characteristic::Saturation(0); } +/** @brief Collect updated characteristic values from HomeSpan and forward them to ModuleLightsControl. */ boolean MoonLightBulb::update() { if (!module) return false; module->applyFromHomeKit( From 925bf17aa9829abdafbcfba49a172ead66130409 Mon Sep 17 00:00:00 2001 From: Abko Wijma Date: Sat, 7 Mar 2026 14:49:39 +0100 Subject: [PATCH 3/4] Address CodeRabbit review feedback on ModuleHomeAutomation - Pin HomeSpan library to ^2.1.7 in platformio.ini - Fix float comparison in rgbToHsv: use d<1e-6f instead of d==0 - Add stopHomeKit() method and call it when switching away from HomeKit mode - Add native unit tests for rgbToHsv and hsvToRgb (12 test cases) Co-Authored-By: Claude Sonnet 4.6 --- platformio.ini | 2 +- src/MoonLight/Modules/ModuleHomeAutomation.h | 12 +- test/test_native/test_homeautomation.cpp | 140 +++++++++++++++++++ 3 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 test/test_native/test_homeautomation.cpp diff --git a/platformio.ini b/platformio.ini index ed5fe555c..088c5cc3d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -270,7 +270,7 @@ lib_deps = build_flags = -D FT_HOMEKIT=1 lib_deps = - HomeSpan/HomeSpan + HomeSpan/HomeSpan@^2.1.7 ;For all boards [ESP32_LEDSDRIVER] diff --git a/src/MoonLight/Modules/ModuleHomeAutomation.h b/src/MoonLight/Modules/ModuleHomeAutomation.h index 74e0a1998..d4b9e827d 100644 --- a/src/MoonLight/Modules/ModuleHomeAutomation.h +++ b/src/MoonLight/Modules/ModuleHomeAutomation.h @@ -26,7 +26,7 @@ float rf=r/255.f, gf=g/255.f, bf=b/255.f; float mx=max(max(rf,gf),bf), mn=min(min(rf,gf),bf), d=mx-mn; v=mx; s=(mx>0)?d/mx:0; - if(d==0){h=0;} else if(mx==rf){h=60.f*fmod((gf-bf)/d,6.f);} else if(mx==gf){h=60.f*((bf-rf)/d+2);} else{h=60.f*((rf-gf)/d+4);} + if(d<1e-6f){h=0;} else if(mx==rf){h=60.f*fmod((gf-bf)/d,6.f);} else if(mx==gf){h=60.f*((bf-rf)/d+2);} else{h=60.f*((rf-gf)/d+4);} if(h<0)h+=360.f; } @@ -92,7 +92,7 @@ class ModuleHomeAutomation : public Module { #endif } - /** @brief React to homeAutomation mode changes: log the Homebridge URL or start the HomeKit server. */ + /** @brief React to homeAutomation mode changes: log the Homebridge URL, start, stop, or sync the HomeKit server. */ void onUpdate(const UpdatedItem &updatedItem) override { if (updatedItem.name != "homeAutomation") return; uint8_t mode = _state.data["homeAutomation"]; @@ -101,6 +101,7 @@ class ModuleHomeAutomation : public Module { _sveltekit->getWiFiSettingsService()->getHostname().c_str()); #if FT_ENABLED(FT_HOMEKIT) if (mode == 2) { if (!_homeKitStarted) startHomeKit(); else syncToHomeKit(); } + else if (_homeKitStarted) stopHomeKit(); #endif } @@ -136,6 +137,13 @@ class ModuleHomeAutomation : public Module { }, "homekit-sync"); } + /** @brief Stop the HomeKit server by halting poll() and resetting state; HomeSpan cannot be fully uninitialised at runtime. */ + void stopHomeKit() { + _homeKitStarted = false; + _lightBulb = nullptr; + EXT_LOGI(ML_TAG, "HomeKit stopped (poll disabled; re-enable by selecting HomeKit Native again)"); + } + /** @brief Apply a HomeKit command to ModuleLightsControl state, converting HSV→RGB and HomeKit brightness to 0-255. */ void applyFromHomeKit(bool on, int hkBri, float h, float s) { uint8_t r, g, b; diff --git a/test/test_native/test_homeautomation.cpp b/test/test_native/test_homeautomation.cpp new file mode 100644 index 000000000..c2003eb02 --- /dev/null +++ b/test/test_native/test_homeautomation.cpp @@ -0,0 +1,140 @@ +/** + @title MoonLight Unit Tests — ModuleHomeAutomation + @file test_homeautomation.cpp + @repo https://github.com/MoonModules/MoonLight + @Copyright © 2026 Github MoonLight Commit Authors + @license GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 + + Native unit tests for the pure color-conversion helpers in ModuleHomeAutomation. + Functions are copied here to avoid ESP32/HomeSpan header dependencies. + Run with: pio test -e native +**/ + +#include "doctest.h" +#include +#include +#include + +using std::max; +using std::min; +using std::fabs; +using std::fmod; + +// ============================================================ +// Copied from ModuleHomeAutomation.h — keep in sync +// ============================================================ + +static void rgbToHsv(uint8_t r, uint8_t g, uint8_t b, float &h, float &s, float &v) { + float rf=r/255.f, gf=g/255.f, bf=b/255.f; + float mx=max(max(rf,gf),bf), mn=min(min(rf,gf),bf), d=mx-mn; + v=mx; s=(mx>0)?d/mx:0; + if(d<1e-6f){h=0;} else if(mx==rf){h=60.f*fmod((gf-bf)/d,6.f);} else if(mx==gf){h=60.f*((bf-rf)/d+2);} else{h=60.f*((rf-gf)/d+4);} + if(h<0)h+=360.f; +} + +static void hsvToRgb(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) { + float c=v*s, x=c*(1.f-fabs(fmod(h/60.f,2.f)-1.f)), m=v-c, rf,gf,bf; + if(h<60){rf=c;gf=x;bf=0;} else if(h<120){rf=x;gf=c;bf=0;} else if(h<180){rf=0;gf=c;bf=x;} + else if(h<240){rf=0;gf=x;bf=c;} else if(h<300){rf=x;gf=0;bf=c;} else{rf=c;gf=0;bf=x;} + r=(uint8_t)((rf+m)*255.5f); g=(uint8_t)((gf+m)*255.5f); b=(uint8_t)((bf+m)*255.5f); +} + +// ============================================================ +// Tests +// ============================================================ + +TEST_CASE("rgbToHsv: pure red") { + float h, s, v; + rgbToHsv(255, 0, 0, h, s, v); + CHECK(h == doctest::Approx(0.f).epsilon(1.f)); // 0° or 360° + CHECK(s == doctest::Approx(1.f).epsilon(0.01f)); + CHECK(v == doctest::Approx(1.f).epsilon(0.01f)); +} + +TEST_CASE("rgbToHsv: pure green") { + float h, s, v; + rgbToHsv(0, 255, 0, h, s, v); + CHECK(h == doctest::Approx(120.f).epsilon(1.f)); + CHECK(s == doctest::Approx(1.f).epsilon(0.01f)); + CHECK(v == doctest::Approx(1.f).epsilon(0.01f)); +} + +TEST_CASE("rgbToHsv: pure blue") { + float h, s, v; + rgbToHsv(0, 0, 255, h, s, v); + CHECK(h == doctest::Approx(240.f).epsilon(1.f)); + CHECK(s == doctest::Approx(1.f).epsilon(0.01f)); + CHECK(v == doctest::Approx(1.f).epsilon(0.01f)); +} + +TEST_CASE("rgbToHsv: black (epsilon edge case — d≈0)") { + float h, s, v; + rgbToHsv(0, 0, 0, h, s, v); + CHECK(h == doctest::Approx(0.f).epsilon(0.01f)); + CHECK(s == doctest::Approx(0.f).epsilon(0.01f)); + CHECK(v == doctest::Approx(0.f).epsilon(0.01f)); +} + +TEST_CASE("rgbToHsv: white (epsilon edge case — d≈0)") { + float h, s, v; + rgbToHsv(255, 255, 255, h, s, v); + CHECK(h == doctest::Approx(0.f).epsilon(0.01f)); + CHECK(s == doctest::Approx(0.f).epsilon(0.01f)); + CHECK(v == doctest::Approx(1.f).epsilon(0.01f)); +} + +TEST_CASE("rgbToHsv: grey (epsilon edge case — d≈0)") { + float h, s, v; + rgbToHsv(128, 128, 128, h, s, v); + CHECK(s == doctest::Approx(0.f).epsilon(0.01f)); +} + +TEST_CASE("hsvToRgb: pure red") { + uint8_t r, g, b; + hsvToRgb(0.f, 1.f, 1.f, r, g, b); + CHECK(r == 255); CHECK(g == 0); CHECK(b == 0); +} + +TEST_CASE("hsvToRgb: pure green") { + uint8_t r, g, b; + hsvToRgb(120.f, 1.f, 1.f, r, g, b); + CHECK(r == 0); CHECK(g == 255); CHECK(b == 0); +} + +TEST_CASE("hsvToRgb: pure blue") { + uint8_t r, g, b; + hsvToRgb(240.f, 1.f, 1.f, r, g, b); + CHECK(r == 0); CHECK(g == 0); CHECK(b == 255); +} + +TEST_CASE("hsvToRgb: black") { + uint8_t r, g, b; + hsvToRgb(0.f, 0.f, 0.f, r, g, b); + CHECK(r == 0); CHECK(g == 0); CHECK(b == 0); +} + +TEST_CASE("hsvToRgb: white") { + uint8_t r, g, b; + hsvToRgb(0.f, 0.f, 1.f, r, g, b); + CHECK(r == 255); CHECK(g == 255); CHECK(b == 255); +} + +TEST_CASE("round-trip RGB→HSV→RGB: orange") { + uint8_t ri=255, gi=128, bi=0, ro, go, bo; + float h, s, v; + rgbToHsv(ri, gi, bi, h, s, v); + hsvToRgb(h, s, v, ro, go, bo); + CHECK(abs((int)ri-(int)ro) <= 1); + CHECK(abs((int)gi-(int)go) <= 1); + CHECK(abs((int)bi-(int)bo) <= 1); +} + +TEST_CASE("round-trip RGB→HSV→RGB: teal") { + uint8_t ri=0, gi=128, bi=128, ro, go, bo; + float h, s, v; + rgbToHsv(ri, gi, bi, h, s, v); + hsvToRgb(h, s, v, ro, go, bo); + CHECK(abs((int)ri-(int)ro) <= 1); + CHECK(abs((int)gi-(int)go) <= 1); + CHECK(abs((int)bi-(int)bo) <= 1); +} From ac95a48543ce047175319b50fbe7359c182b0d5f Mon Sep 17 00:00:00 2001 From: Abko Wijma Date: Sat, 7 Mar 2026 19:04:37 +0100 Subject: [PATCH 4/4] Redesign HomeKit integration as a Driver Node (D_HomeKit.h) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the Module-based approach with a Driver Node following the D_Infrared.h pattern, as requested in PR review. The node appears in the Drivers list and logs the existing /api/lightscontrol URL when set to Homebridge HTTP mode — zero library or flash overhead on any board. Remove ModuleHomeAutomation.h, revert main.cpp and ini changes, and remove the associated unit tests (no pure functions to test). Co-Authored-By: Claude Sonnet 4.6 --- firmware/esp32-p4.ini | 6 +- firmware/esp32-s3.ini | 6 +- platformio.ini | 8 - src/MoonLight/Modules/ModuleDrivers.h | 3 + src/MoonLight/Modules/ModuleHomeAutomation.h | 178 ------------------- src/MoonLight/Nodes/Drivers/D_HomeKit.h | 56 ++++++ src/main.cpp | 3 - test/test_native/test_homeautomation.cpp | 140 --------------- 8 files changed, 63 insertions(+), 337 deletions(-) delete mode 100644 src/MoonLight/Modules/ModuleHomeAutomation.h create mode 100644 src/MoonLight/Nodes/Drivers/D_HomeKit.h delete mode 100644 test/test_native/test_homeautomation.cpp diff --git a/firmware/esp32-p4.ini b/firmware/esp32-p4.ini index 0e94ed08c..0fa711b30 100644 --- a/firmware/esp32-p4.ini +++ b/firmware/esp32-p4.ini @@ -1,15 +1,13 @@ [esp32-p4-base] -build_flags = +build_flags = ${env.build_flags} ${moonlight.build_flags} ${HP_ALL_DRIVERS.build_flags} ;use the LedsDriver class only for setBrightness and setColorCorrection and LUT - ${homekit.build_flags} -D CONFIG_IDF_TARGET_ESP32P4=1 -lib_deps = +lib_deps = ${env.lib_deps} ${moonlight.lib_deps} ${HP_ALL_DRIVERS.lib_deps} - ${homekit.lib_deps} ; "possible fix if idf.py instakk fails ; platform = https://github.com/Jason2866/platform-espressif32.git#Arduino/IDF53 ; https://github.com/arendst/Tasmota/discussions/23758#discussioncomment-14010813 diff --git a/firmware/esp32-s3.ini b/firmware/esp32-s3.ini index 91d66fd72..5c3648eda 100644 --- a/firmware/esp32-s3.ini +++ b/firmware/esp32-s3.ini @@ -1,22 +1,20 @@ [esp32-s3-base] -build_flags = +build_flags = ${env.build_flags} ${moonlight.build_flags} ${livescripts.build_flags} ${HP_ALL_DRIVERS.build_flags} - ${homekit.build_flags} -D CONFIG_IDF_TARGET_ESP32S3=1 ; -D ARDUINO_USB_MODE=1 ; which USB device classes are enabled on your ESP32 at boot. default 1 in board definition (serial only) ; -D ARDUINO_USB_CDC_ON_BOOT=1 ;Communications Device Class: controls whether the ESP32's USB serial port is enabled automatically at boot, not set in board definition! ; -D ARDUINO_USB_MSC_ON_BOOT=0 ;Mass Storage Class, disable ; -D ARDUINO_USB_DFU_ON_BOOT=0 ;download firmware update, disable ; -D ML_LIVE_MAPPING -lib_deps = +lib_deps = ${env.lib_deps} ${moonlight.lib_deps} ${livescripts.lib_deps} ${HP_ALL_DRIVERS.lib_deps} - ${homekit.lib_deps} diff --git a/platformio.ini b/platformio.ini index 088c5cc3d..e741b16c8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -264,14 +264,6 @@ lib_deps = ; https://github.com/hpwit/ESPLiveScript.git#9f002b4ec450cd47f5d417bfaddd616dd764d1f3 ; vjson 16-06-2025 ; Comment if FT_LIVESCRIPT=0 https://github.com/ewowi/ESPLiveScript.git#e02c2dccdd5044c9aa3edb874d26759afb0e1cb9 ; 02-11-2025 fix warnings -; 💫 HomeKit (HAP) support via HomeSpan — only enabled on boards with enough flash (S3/P4) -; Requires FT_HOMEKIT=1 in board build_flags. Off by default on esp32-d0 (flash too tight). -[homekit] -build_flags = - -D FT_HOMEKIT=1 -lib_deps = - HomeSpan/HomeSpan@^2.1.7 - ;For all boards [ESP32_LEDSDRIVER] build_flags = diff --git a/src/MoonLight/Modules/ModuleDrivers.h b/src/MoonLight/Modules/ModuleDrivers.h index 012bc5a89..5315198d2 100644 --- a/src/MoonLight/Modules/ModuleDrivers.h +++ b/src/MoonLight/Modules/ModuleDrivers.h @@ -17,6 +17,7 @@ #include "FastLED.h" #include "MoonBase/NodeManager.h" #include "MoonLight/Modules/ModuleLightsControl.h" + #include "MoonLight/Nodes/Drivers/D_HomeKit.h" // #include "Nodes.h" //Nodes.h will include VirtualLayer.h which will include PhysicalLayer.h @@ -106,6 +107,7 @@ class ModuleDrivers : public NodeManager { addControlValue(control, getNameAndTags()); addControlValue(control, getNameAndTags()); addControlValue(control, getNameAndTags()); + addControlValue(control, getNameAndTags()); addControlValue(control, getNameAndTags()); addControlValue(control, getNameAndTags()); @@ -147,6 +149,7 @@ class ModuleDrivers : public NodeManager { if (!node) node = checkAndAlloc(name); if (!node) node = checkAndAlloc(name); if (!node) node = checkAndAlloc(name); + if (!node) node = checkAndAlloc(name); if (!node) node = checkAndAlloc(name); if (!node) node = checkAndAlloc(name); diff --git a/src/MoonLight/Modules/ModuleHomeAutomation.h b/src/MoonLight/Modules/ModuleHomeAutomation.h deleted file mode 100644 index d4b9e827d..000000000 --- a/src/MoonLight/Modules/ModuleHomeAutomation.h +++ /dev/null @@ -1,178 +0,0 @@ -/** - @title MoonLight - @file ModuleHomeAutomation.h - @repo https://github.com/MoonModules/MoonLight, submit changes to this file as PRs - @Authors https://github.com/MoonModules/MoonLight/commits/main - @Doc https://moonmodules.org/MoonLight/moonlight/homeautomation/ - @Copyright © 2026 Github MoonLight Commit Authors - @license GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - @license For non GPL-v3 usage, commercial licenses must be purchased. Contact us for more information. -**/ - -#ifndef ModuleHomeAutomation_h -#define ModuleHomeAutomation_h - -#if FT_MOONLIGHT - -#include "MoonBase/Module.h" -#include "MoonLight/Modules/ModuleLightsControl.h" - -// 💫 HomeKit native support via HomeSpan (only compiled on boards with FT_HOMEKIT) -#if FT_ENABLED(FT_HOMEKIT) - #include "HomeSpan.h" - - /** @brief Convert RGB (0-255 each) to HSV. h is 0-360, s and v are 0-1. */ - static void rgbToHsv(uint8_t r, uint8_t g, uint8_t b, float &h, float &s, float &v) { - float rf=r/255.f, gf=g/255.f, bf=b/255.f; - float mx=max(max(rf,gf),bf), mn=min(min(rf,gf),bf), d=mx-mn; - v=mx; s=(mx>0)?d/mx:0; - if(d<1e-6f){h=0;} else if(mx==rf){h=60.f*fmod((gf-bf)/d,6.f);} else if(mx==gf){h=60.f*((bf-rf)/d+2);} else{h=60.f*((rf-gf)/d+4);} - if(h<0)h+=360.f; - } - - /** @brief Convert HSV (h: 0-360, s: 0-1, v: 0-1) to RGB (0-255 each). */ - static void hsvToRgb(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) { - float c=v*s, x=c*(1.f-fabs(fmod(h/60.f,2.f)-1.f)), m=v-c, rf,gf,bf; - if(h<60){rf=c;gf=x;bf=0;} else if(h<120){rf=x;gf=c;bf=0;} else if(h<180){rf=0;gf=c;bf=x;} - else if(h<240){rf=0;gf=x;bf=c;} else if(h<300){rf=x;gf=0;bf=c;} else{rf=c;gf=0;bf=x;} - r=(uint8_t)((rf+m)*255.5f); g=(uint8_t)((gf+m)*255.5f); b=(uint8_t)((bf+m)*255.5f); - } - - class ModuleHomeAutomation; - - /** @brief HomeSpan LightBulb service bridging HomeKit commands to ModuleLightsControl state. */ - struct MoonLightBulb : Service::LightBulb { - SpanCharacteristic *power, *brightness, *hue, *saturation; - ModuleHomeAutomation *module; - /** @brief Initialise HomeKit characteristics and store back-reference to the owning module. */ - explicit MoonLightBulb(ModuleHomeAutomation *mod); - /** @brief Called by HomeSpan when Apple Home sends a command; forwards to applyFromHomeKit(). */ - boolean update() override; - }; -#endif - -/** @brief Module that exposes a home automation mode selector in the MoonLight UI. - * - * Supported modes (runtime-selectable via the GUI dropdown): - * - 0 Off — no integration active - * - 1 Homebridge HTTP — logs the existing /api/lightscontrol URL for use with an HTTP Homebridge plugin; zero extra flash - * - 2 HomeKit Native — starts a HomeKit Accessory Protocol server via HomeSpan (S3/P4 only, requires FT_HOMEKIT=1) - */ -class ModuleHomeAutomation : public Module { - private: - ModuleLightsControl *_lightsControl; -#if FT_ENABLED(FT_HOMEKIT) - MoonLightBulb *_lightBulb = nullptr; - bool _homeKitStarted = false; -#endif - - public: - /** @brief Construct the module, storing a reference to ModuleLightsControl for bidirectional state sync. */ - ModuleHomeAutomation(PsychicHttpServer *server, ESP32SvelteKit *sveltekit, ModuleLightsControl *lightsControl) - : Module("homeautomation", server, sveltekit), _lightsControl(lightsControl) {} - - /** @brief Start HomeKit if the persisted mode is HomeKit Native, and register a state-change listener on ModuleLightsControl. */ - void begin() override { - Module::begin(); -#if FT_ENABLED(FT_HOMEKIT) - if (_state.data["homeAutomation"] == 2) startHomeKit(); - _lightsControl->addUpdateHandler([this](const String &originId) { if (originId != "homekit") syncToHomeKit(); }, false); -#endif - } - - /** @brief Register the homeAutomation select control. Options depend on compile-time FT_HOMEKIT flag. */ - void setupDefinition(const JsonArray &controls) override { - JsonObject control = addControl(controls, "homeAutomation", "select"); - control["default"] = 0; - JsonArray values = control["values"].to(); - values.add("Off"); - values.add("Homebridge HTTP"); -#if FT_ENABLED(FT_HOMEKIT) - values.add("HomeKit Native"); -#endif - } - - /** @brief React to homeAutomation mode changes: log the Homebridge URL, start, stop, or sync the HomeKit server. */ - void onUpdate(const UpdatedItem &updatedItem) override { - if (updatedItem.name != "homeAutomation") return; - uint8_t mode = _state.data["homeAutomation"]; - if (mode == 1) - EXT_LOGI(ML_TAG, "Homebridge HTTP: configure plugin → http://%s/api/lightscontrol", - _sveltekit->getWiFiSettingsService()->getHostname().c_str()); -#if FT_ENABLED(FT_HOMEKIT) - if (mode == 2) { if (!_homeKitStarted) startHomeKit(); else syncToHomeKit(); } - else if (_homeKitStarted) stopHomeKit(); -#endif - } - -#if FT_ENABLED(FT_HOMEKIT) - /** @brief Poll the HomeSpan HAP server every loop iteration while HomeKit Native mode is active. */ - void loop() override { if (_homeKitStarted) homeSpan.poll(); } - - /** @brief Initialise the HomeSpan accessory tree and start the HAP server. Called once on first activation. */ - void startHomeKit() { - _homeKitStarted = true; - String hn = _sveltekit->getWiFiSettingsService()->getHostname(); - homeSpan.setHostNameSuffix(""); homeSpan.setPortNum(1201); - homeSpan.begin(Category::Lighting, hn.c_str()); - new SpanAccessory(); - new Service::AccessoryInformation(); - new Characteristic::Name(hn.c_str()); new Characteristic::Manufacturer("MoonModules"); - new Characteristic::SerialNumber("MoonLight"); new Characteristic::Model("MoonLight"); - new Characteristic::FirmwareRevision("1.0.0"); new Characteristic::Identify(); - _lightBulb = new MoonLightBulb(this); - syncToHomeKit(); - EXT_LOGI(ML_TAG, "HomeKit started as \"%s\"", hn.c_str()); - } - - /** @brief Push the current ModuleLightsControl state (on/off, brightness, RGB→HSV) into HomeKit characteristics. */ - void syncToHomeKit() { - if (!_lightBulb) return; - _lightsControl->read([&](ModuleState &state) { - float h, s, v; - rgbToHsv(state.data["red"], state.data["green"], state.data["blue"], h, s, v); - _lightBulb->power->setVal(state.data["lightsOn"].as()); - _lightBulb->brightness->setVal((int)(state.data["brightness"].as() / 2.55f + 0.5f)); - _lightBulb->hue->setVal(h); _lightBulb->saturation->setVal(s * 100.f); - }, "homekit-sync"); - } - - /** @brief Stop the HomeKit server by halting poll() and resetting state; HomeSpan cannot be fully uninitialised at runtime. */ - void stopHomeKit() { - _homeKitStarted = false; - _lightBulb = nullptr; - EXT_LOGI(ML_TAG, "HomeKit stopped (poll disabled; re-enable by selecting HomeKit Native again)"); - } - - /** @brief Apply a HomeKit command to ModuleLightsControl state, converting HSV→RGB and HomeKit brightness to 0-255. */ - void applyFromHomeKit(bool on, int hkBri, float h, float s) { - uint8_t r, g, b; - hsvToRgb(h, s / 100.f, 1.f, r, g, b); - JsonDocument doc; JsonObject ns = doc.to(); - ns["lightsOn"]=on; ns["brightness"]=(uint8_t)(hkBri*2.55f+0.5f); - ns["red"]=r; ns["green"]=g; ns["blue"]=b; - _lightsControl->update(ns, ModuleState::update, "homekit"); - } -#endif -}; // class ModuleHomeAutomation - -#if FT_ENABLED(FT_HOMEKIT) -/** @brief Initialise all four HomeKit characteristics with sensible defaults. */ -MoonLightBulb::MoonLightBulb(ModuleHomeAutomation *mod) : Service::LightBulb(), module(mod) { - power=new Characteristic::On(false); brightness=new Characteristic::Brightness(20); - hue=new Characteristic::Hue(0); saturation=new Characteristic::Saturation(0); -} -/** @brief Collect updated characteristic values from HomeSpan and forward them to ModuleLightsControl. */ -boolean MoonLightBulb::update() { - if (!module) return false; - module->applyFromHomeKit( - power->updated() ? power->getNewVal() : power->getVal(), - brightness->updated() ? brightness->getNewVal() : brightness->getVal(), - hue->updated() ? hue->getNewVal() : hue->getVal(), - saturation->updated() ? saturation->getNewVal() : saturation->getVal()); - return true; -} -#endif - -#endif // FT_MOONLIGHT -#endif // ModuleHomeAutomation_h diff --git a/src/MoonLight/Nodes/Drivers/D_HomeKit.h b/src/MoonLight/Nodes/Drivers/D_HomeKit.h new file mode 100644 index 000000000..365dc33ab --- /dev/null +++ b/src/MoonLight/Nodes/Drivers/D_HomeKit.h @@ -0,0 +1,56 @@ +/** + @title MoonLight + @file D_HomeKit.h + @repo https://github.com/MoonModules/MoonLight, submit changes to this file as PRs + @Authors https://github.com/MoonModules/MoonLight/commits/main + @Doc https://moonmodules.org/MoonLight/moonlight/homeautomation/ + @Copyright © 2026 Github MoonLight Commit Authors + @license GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 + @license For non GPL-v3 usage, commercial licenses must be purchased. Contact us for more information. +**/ + +#if FT_MOONLIGHT + +#include + +/** @brief Driver node that bridges MoonLight to Apple Home via Homebridge HTTP. + * + * Supported modes (selectable via the GUI dropdown): + * - 0 Off — no integration active + * - 1 Homebridge HTTP — logs the existing /api/lightscontrol URL for use with + * the homebridge-http-lightbulb plugin; zero extra flash + * + * Homebridge polls the ESP32 over HTTP so no loop() work is required here. + */ +class HomeKitNode : public Node { + public: + /** @brief Node display name shown in the Drivers list. */ + static const char* name() { return "HomeKit Driver"; } + /** @brief No pixel dimensions — this node does not drive LEDs directly. */ + static uint8_t dim() { return _NoD; } + /** @brief Driver emoji tag. */ + static const char* tags() { return "☸️"; } + + uint8_t mode = 0; + + /** @brief Register the mode select control. */ + void setup() override { + addControl(mode, "mode", "select"); + addControlValue("Off"); + addControlValue("Homebridge HTTP"); + } + + /** @brief React to mode changes: log the Homebridge plugin URL or silence the integration. */ + void onUpdate(const JsonObject& control) override { + if (control["name"] == "mode") { + uint8_t value = control["value"]; + if (value == 0) + EXT_LOGI(ML_TAG, "HomeKit: Off"); + else if (value == 1) + EXT_LOGI(ML_TAG, "HomeKit: configure homebridge-http-lightbulb with URL: http://%s/api/lightscontrol", + WiFi.localIP().toString().c_str()); + } + } +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp index d72e937cc..0e7fb5786 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -107,8 +107,6 @@ ModuleDrivers moduleDrivers = ModuleDrivers(&server, &esp32sveltekit, &fileManag #include "MoonLight/Modules/ModuleLiveScripts.h" ModuleLiveScripts moduleLiveScripts = ModuleLiveScripts(&server, &esp32sveltekit, &fileManager, &moduleEffects, &moduleDrivers); #endif - #include "MoonLight/Modules/ModuleHomeAutomation.h" -ModuleHomeAutomation moduleHomeAutomation = ModuleHomeAutomation(&server, &esp32sveltekit, &moduleLightsControl); ModuleChannels moduleChannels = ModuleChannels(&server, &esp32sveltekit); ModuleMoonLightInfo moduleMoonLightInfo = ModuleMoonLightInfo(&server, &esp32sveltekit); @@ -295,7 +293,6 @@ void setup() { modules.push_back(&moduleDevices); // In MoonLight for the time being, should move to MoonBase using moduleControlCenter ... modules.push_back(&moduleDrivers); modules.push_back(&moduleLightsControl); - modules.push_back(&moduleHomeAutomation); modules.push_back(&moduleChannels); modules.push_back(&moduleMoonLightInfo); #if FT_ENABLED(FT_LIVESCRIPT) diff --git a/test/test_native/test_homeautomation.cpp b/test/test_native/test_homeautomation.cpp deleted file mode 100644 index c2003eb02..000000000 --- a/test/test_native/test_homeautomation.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/** - @title MoonLight Unit Tests — ModuleHomeAutomation - @file test_homeautomation.cpp - @repo https://github.com/MoonModules/MoonLight - @Copyright © 2026 Github MoonLight Commit Authors - @license GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - - Native unit tests for the pure color-conversion helpers in ModuleHomeAutomation. - Functions are copied here to avoid ESP32/HomeSpan header dependencies. - Run with: pio test -e native -**/ - -#include "doctest.h" -#include -#include -#include - -using std::max; -using std::min; -using std::fabs; -using std::fmod; - -// ============================================================ -// Copied from ModuleHomeAutomation.h — keep in sync -// ============================================================ - -static void rgbToHsv(uint8_t r, uint8_t g, uint8_t b, float &h, float &s, float &v) { - float rf=r/255.f, gf=g/255.f, bf=b/255.f; - float mx=max(max(rf,gf),bf), mn=min(min(rf,gf),bf), d=mx-mn; - v=mx; s=(mx>0)?d/mx:0; - if(d<1e-6f){h=0;} else if(mx==rf){h=60.f*fmod((gf-bf)/d,6.f);} else if(mx==gf){h=60.f*((bf-rf)/d+2);} else{h=60.f*((rf-gf)/d+4);} - if(h<0)h+=360.f; -} - -static void hsvToRgb(float h, float s, float v, uint8_t &r, uint8_t &g, uint8_t &b) { - float c=v*s, x=c*(1.f-fabs(fmod(h/60.f,2.f)-1.f)), m=v-c, rf,gf,bf; - if(h<60){rf=c;gf=x;bf=0;} else if(h<120){rf=x;gf=c;bf=0;} else if(h<180){rf=0;gf=c;bf=x;} - else if(h<240){rf=0;gf=x;bf=c;} else if(h<300){rf=x;gf=0;bf=c;} else{rf=c;gf=0;bf=x;} - r=(uint8_t)((rf+m)*255.5f); g=(uint8_t)((gf+m)*255.5f); b=(uint8_t)((bf+m)*255.5f); -} - -// ============================================================ -// Tests -// ============================================================ - -TEST_CASE("rgbToHsv: pure red") { - float h, s, v; - rgbToHsv(255, 0, 0, h, s, v); - CHECK(h == doctest::Approx(0.f).epsilon(1.f)); // 0° or 360° - CHECK(s == doctest::Approx(1.f).epsilon(0.01f)); - CHECK(v == doctest::Approx(1.f).epsilon(0.01f)); -} - -TEST_CASE("rgbToHsv: pure green") { - float h, s, v; - rgbToHsv(0, 255, 0, h, s, v); - CHECK(h == doctest::Approx(120.f).epsilon(1.f)); - CHECK(s == doctest::Approx(1.f).epsilon(0.01f)); - CHECK(v == doctest::Approx(1.f).epsilon(0.01f)); -} - -TEST_CASE("rgbToHsv: pure blue") { - float h, s, v; - rgbToHsv(0, 0, 255, h, s, v); - CHECK(h == doctest::Approx(240.f).epsilon(1.f)); - CHECK(s == doctest::Approx(1.f).epsilon(0.01f)); - CHECK(v == doctest::Approx(1.f).epsilon(0.01f)); -} - -TEST_CASE("rgbToHsv: black (epsilon edge case — d≈0)") { - float h, s, v; - rgbToHsv(0, 0, 0, h, s, v); - CHECK(h == doctest::Approx(0.f).epsilon(0.01f)); - CHECK(s == doctest::Approx(0.f).epsilon(0.01f)); - CHECK(v == doctest::Approx(0.f).epsilon(0.01f)); -} - -TEST_CASE("rgbToHsv: white (epsilon edge case — d≈0)") { - float h, s, v; - rgbToHsv(255, 255, 255, h, s, v); - CHECK(h == doctest::Approx(0.f).epsilon(0.01f)); - CHECK(s == doctest::Approx(0.f).epsilon(0.01f)); - CHECK(v == doctest::Approx(1.f).epsilon(0.01f)); -} - -TEST_CASE("rgbToHsv: grey (epsilon edge case — d≈0)") { - float h, s, v; - rgbToHsv(128, 128, 128, h, s, v); - CHECK(s == doctest::Approx(0.f).epsilon(0.01f)); -} - -TEST_CASE("hsvToRgb: pure red") { - uint8_t r, g, b; - hsvToRgb(0.f, 1.f, 1.f, r, g, b); - CHECK(r == 255); CHECK(g == 0); CHECK(b == 0); -} - -TEST_CASE("hsvToRgb: pure green") { - uint8_t r, g, b; - hsvToRgb(120.f, 1.f, 1.f, r, g, b); - CHECK(r == 0); CHECK(g == 255); CHECK(b == 0); -} - -TEST_CASE("hsvToRgb: pure blue") { - uint8_t r, g, b; - hsvToRgb(240.f, 1.f, 1.f, r, g, b); - CHECK(r == 0); CHECK(g == 0); CHECK(b == 255); -} - -TEST_CASE("hsvToRgb: black") { - uint8_t r, g, b; - hsvToRgb(0.f, 0.f, 0.f, r, g, b); - CHECK(r == 0); CHECK(g == 0); CHECK(b == 0); -} - -TEST_CASE("hsvToRgb: white") { - uint8_t r, g, b; - hsvToRgb(0.f, 0.f, 1.f, r, g, b); - CHECK(r == 255); CHECK(g == 255); CHECK(b == 255); -} - -TEST_CASE("round-trip RGB→HSV→RGB: orange") { - uint8_t ri=255, gi=128, bi=0, ro, go, bo; - float h, s, v; - rgbToHsv(ri, gi, bi, h, s, v); - hsvToRgb(h, s, v, ro, go, bo); - CHECK(abs((int)ri-(int)ro) <= 1); - CHECK(abs((int)gi-(int)go) <= 1); - CHECK(abs((int)bi-(int)bo) <= 1); -} - -TEST_CASE("round-trip RGB→HSV→RGB: teal") { - uint8_t ri=0, gi=128, bi=128, ro, go, bo; - float h, s, v; - rgbToHsv(ri, gi, bi, h, s, v); - hsvToRgb(h, s, v, ro, go, bo); - CHECK(abs((int)ri-(int)ro) <= 1); - CHECK(abs((int)gi-(int)go) <= 1); - CHECK(abs((int)bi-(int)bo) <= 1); -}