diff --git a/examples/color-light/README.md b/examples/color-light/README.md index ca5189b..fb03116 100644 --- a/examples/color-light/README.md +++ b/examples/color-light/README.md @@ -5,17 +5,21 @@ A smart light device with addressable RGB LEDs. - [Documentation and firmware install page](https://homegenie.it/mini/1.2/examples/smart-led-lights/) -## Firmware configuration (in addition to default system options) - -| Key | Description | Default | -|------------|----------------------------|-----------------------------------------| -| `leds-pin` | LEDs strip GPIO pin number | -1 (-1=not used) | -| `leds-cnt` | Number of LEDs | 1 (1 - 200) | -| `stld-typ` | LEDs type | RGB/RGBW order mask (see code for ref.) | -| `leds-spd` | Data transfer speed | 0 (0=800kHz, 256=400kHz) | -| `stld-pin` | Status LED (RGB) pin | -1 (-1=not used) | -| `stld-typ` | Status LED type | RGB/RGBW order mask (see code for ref.) | -| `stld-spd` | Status LED speed | 0 (0=800kHz, 256=400kHz) | +## Firmware configuration + +In addition to default system options the following configuration options are available: + +| Key | Description | Default | +|------------|-----------------------------|-----------------------------------------| +| `leds-pin` | LED strip GPIO pin number | -1 (-1=not used) | +| `leds-cnt` | Number of LEDs | 1 (1 - 200) | +| `stld-typ` | LEDs type | RGB/RGBW order mask (see code for ref.) | +| `leds-spd` | Data transfer speed | 0 (0=800kHz, 256=400kHz) | +| `stld-pin` | Status LED (RGB) pin | -1 (-1=not used) | +| `stld-typ` | Status LED type | RGB/RGBW order mask (see code for ref.) | +| `stld-spd` | Status LED speed | 0 (0=800kHz, 256=400kHz) | +| `btn1-pin` | Toggle button GPIO pin num. | -1 (-1=not used) | +| `btn2-pin` | Mode button GPIO pin num. | -1 (-1=not used) | ## Manual build and install diff --git a/examples/color-light/color-fx.h b/examples/color-light/color-fx.h index c5fc727..bbf6316 100644 --- a/examples/color-light/color-fx.h +++ b/examples/color-light/color-fx.h @@ -106,7 +106,7 @@ float stripe_previous_hue; void fx_white_stripes(Adafruit_NeoPixel* pixels, LightColor& color, bool brightWhite = false) { if (pixels == nullptr) return; - int stripe_length = (int)round((float)pixels->numPixels() / 7.0f); + int stripe_length = (int)round((float)pixels->numPixels() / 5.0f); float shift; // animate diff --git a/examples/color-light/color-light.cpp b/examples/color-light/color-light.cpp index 555f9eb..c4ebede 100644 --- a/examples/color-light/color-light.cpp +++ b/examples/color-light/color-light.cpp @@ -23,126 +23,48 @@ * */ -#include - -#include - -#include "configuration.h" -#include "color-fx.h" - -using namespace Service; -using namespace Service::API::devices; - -HomeGenie* homeGenie; -bool isConfigured = false; - -// Optional RGB Status LED -Adafruit_NeoPixel* statusLED = nullptr; - -// LED strip/array -uint16_t ledsCount = 0; int16_t ledsPin = -1; -int16_t pixelsType, pixelsSpeed; -Adafruit_NeoPixel* pixels = nullptr; - -// LED Blink callback when statusLED is configured -void statusLedCallback(bool isLedOn) { - if (isLedOn) { - statusLED->setPixelColor(0, Adafruit_NeoPixel::Color(1, 1, 0)); - } else { - statusLED->setPixelColor(0, Adafruit_NeoPixel::Color(0, 0, 0)); - } - statusLED->show(); -} - -unsigned int refreshMs = 10; -unsigned long lastRefreshTs = 0; - -LightColor currentColor; -ColorLight* mainModule; - -ModuleParameter* mpLedCount; -ModuleParameter* mpFxStyle; -ModuleParameter* mpFxStrobe; - -unsigned long strobeFxTickMs = 0; -unsigned int strobeFxDurationMs = 20; -unsigned int strobeFxIntervalMs = 80; // limit strobe to 10Hz (20+80 -> 100ms interval) -bool strobeOff = true; - -String currentStyle = "solid"; - -void refresh() { - if (statusLED != nullptr) { - statusLED->show(); - } - if (pixels != nullptr) { - pixels->show(); - } -} - -void renderPixels() { - if (pixels != nullptr) { - for (int i = 0; i < ledsCount; i++) { - pixels->setPixelColor(i, - animatedColors[i]->getRed(), - animatedColors[i]->getGreen(), - animatedColors[i]->getBlue()); - } - } - if (statusLED != nullptr) { - statusLED->setPixelColor(0, currentColor.getRed(), currentColor.getGreen(), currentColor.getBlue()); - } -} - -void createPixels() -{ - if (ledsPin >= 0) { - pixels = new Adafruit_NeoPixel(ledsCount, ledsPin, pixelsType + pixelsSpeed); - // turn off - //pixels->clear(); - //* - for (int i = 0; i < ledsCount; i++) { - pixels->setPixelColor(i, 0, 0, 0); - } - //*/ - pixels->begin(); - } -} -void disposePixels() { - - // turn off all pixels - if (pixels != nullptr) { - //pixels->clear(); - //* - for (int i = 0; i < ledsCount; i++) { - pixels->setPixelColor(i, 0, 0, 0); - } - refresh(); - //*/ - delete pixels; - delay(1000); - } - -} +#include "color-light.h" void setup() { + + // Get a reference to HomeGenie Mini running instance homeGenie = HomeGenie::getInstance(); + + // Get the default system module auto miniModule = homeGenie->getDefaultModule(); miniModule->name = "Smart light"; + // Add to the default module some configuration properties. + // Properties starting with `Widget.OptionField.` are special + // properties that are used in the HomeGenie Panel app to + // display custom buttons, sliders, selectors and other UI controls + // to configure parameters of this device implementation + + // Number select control to set the number of LEDs of the strip miniModule->properties.add( new ModuleParameter("Widget.OptionField.LED.count", "number:LED.count:1:1920:1:led_count")); + // Number select control to set maximum percentage of power that can be drawn + miniModule->properties.add( + new ModuleParameter("Widget.OptionField.LED.power", + "number:LED.power:5:100:25:led_power")); + // Dropdown list control to select the light animation effect miniModule->properties.add( new ModuleParameter("Widget.OptionField.FX.Rainbow", "select:FX.Style:light_style:solid|rainbow|rainbow_2|white_stripes|white_stripes_2|kaleidoscope")); + // Dropdown list control to enable the strobe effect miniModule->properties.add( new ModuleParameter("Widget.OptionField.FX.Strobe", "select:FX.Strobe:strobe_effect:off|slow|medium|fast")); + // The following are the actual properties where + // UI controls implemented by `Widget.Options` + // read/write values from/to. mpLedCount = new ModuleParameter("LED.count", "0"); miniModule->properties.add(mpLedCount); - mpFxStyle = new ModuleParameter("FX.Style", currentStyle); + mpMaxPower = new ModuleParameter("LED.power", "25"); + miniModule->properties.add(mpMaxPower); + mpFxStyle = new ModuleParameter("FX.Style", lightStyleNames[0]); miniModule->properties.add(mpFxStyle); mpFxStrobe = new ModuleParameter("FX.Strobe", "off"); miniModule->properties.add(mpFxStrobe); @@ -150,7 +72,7 @@ void setup() { // Get status LED config int statusLedPin = Config::getSetting("stld-pin", "-1").toInt(); if (statusLedPin >= 0) { - int statusLedType = Config::getSetting("stld-typ", "6").toInt(); + int statusLedType = Config::getSetting("stld-typ", "82").toInt(); int statusLedSpeed = Config::getSetting("stld-spd", "0").toInt(); statusLED = new Adafruit_NeoPixel(1, statusLedPin, statusLedType + statusLedSpeed); statusLED->setPixelColor(0, 0, 0, 0); @@ -160,7 +82,7 @@ void setup() { isConfigured = Config::isDeviceConfigured(); if (!isConfigured) { - // Custom status led (builtin NeoPixel RGB on pin 10) + // Custom status led (builtin NeoPixel RGB LED) if (statusLED != nullptr) { Config::statusLedCallback(&statusLedCallback); } @@ -180,9 +102,12 @@ void setup() { ledsPin = (int16_t)Config::getSetting("leds-pin").toInt(); pixelsType = (int16_t)Config::getSetting("leds-typ").toInt(); pixelsSpeed = (int16_t)Config::getSetting("leds-spd").toInt(); + maxPower = Config::getSetting("leds-pwr").toInt(); + if (maxPower <= 0) maxPower = DEFAULT_MAX_POWER; createPixels(); mpLedCount->value = String(ledsCount); + mpMaxPower->value = String(maxPower); // Setup main LEDs control module mainModule = new ColorLight(IO::IOEventDomains::HomeAutomation_HomeGenie, "C1", "Main"); @@ -194,12 +119,17 @@ void setup() { }); homeGenie->addAPIHandler(mainModule); - // TODO: add 2 "GpioButton" modules for handling manual control (on/off, level, switch fx, ...) + // Setup control buttons + setupControlButtons(miniModule); // Initialize FX buffer fx_init(ledsCount, currentColor); // TODO: implement color/status recall on start + // Set default color + float h = presetColors[0][0]; + float s = presetColors[0][1]; + mainModule->setColor(h, s, 0, 0); } @@ -217,8 +147,9 @@ void loop() if (millis() - lastRefreshTs > refreshMs) { // Update current rendering style if changed - if (currentStyle != mpFxStyle->value) { - currentStyle = mpFxStyle->value; + uint8_t selectedStyleIndex = getLightStyleIndex(mpFxStyle->value); + if (currentStyleIndex != selectedStyleIndex) { + currentStyleIndex = selectedStyleIndex; } // enable / disable strobe light @@ -228,7 +159,6 @@ void loop() strobeFxTickMs = millis(); } - if (!strobeOff && strobeFxTickMs > 0 && millis() - strobeFxTickMs > ((strobeFxDurationMs + strobeFxIntervalMs) * (mpFxStrobe->value == "slow" ? 3 : mpFxStrobe->value == "medium" ? 2 : 1))) { // strobe off @@ -245,18 +175,26 @@ void loop() } else { // apply selected light style - if (currentStyle == "solid") { - fx_solid(pixels, currentColor, strobeFxTickMs > 0 ? 0 : 200); - } else if (currentStyle == "rainbow") { - fx_rainbow(pixels, currentColor, 1); - } else if (currentStyle == "rainbow_2") { - fx_rainbow(pixels, currentColor, 2); - } else if (currentStyle == "kaleidoscope") { - fx_kaleidoscope(pixels, currentColor); - } else if (currentStyle == "white_stripes") { - fx_white_stripes(pixels, currentColor); - } else if (currentStyle == "white_stripes_2") { - fx_white_stripes(pixels, currentColor, true); + switch (currentStyleIndex) { + case LightStyles::RAINBOW: + fx_rainbow(pixels, currentColor, 1); + break; + case LightStyles::RAINBOW_2: + fx_rainbow(pixels, currentColor, 2); + break; + case LightStyles::WHITE_STRIPES: + fx_white_stripes(pixels, currentColor); + break; + case LightStyles::WHITE_STRIPES_2: + fx_white_stripes(pixels, currentColor, true); + break; + case LightStyles::KALEIDOSCOPE: + fx_kaleidoscope(pixels, currentColor); + break; + case LightStyles::SOLID: + default: + fx_solid(pixels, currentColor, strobeFxTickMs > 0 ? 0 : 200); + break; } } @@ -271,6 +209,15 @@ void loop() } } + // check for commands issued via physical button + if (buttonCommand == BUTTON_COMMAND_STEP_DIM && millis() - lastStepCommandTs > BUTTON_COMMAND_STEP_INTERVAL) { + lastStepCommandTs = millis(); + mainModule->dim(0); + } else if (buttonCommand == BUTTON_COMMAND_STEP_BRIGHT && millis() - lastStepCommandTs > BUTTON_COMMAND_STEP_INTERVAL) { + lastStepCommandTs = millis(); + mainModule->bright(0); + } + // check if number of pixels was changed if (ledsCount != mpLedCount->value.toInt()) { @@ -283,6 +230,11 @@ void loop() createPixels(); } + // check if max power value was changed + if (maxPower != mpMaxPower->value.toInt() && mpMaxPower->value.toInt() > 0) { + maxPower = mpMaxPower->value.toInt(); + Config::saveSetting("leds-pwr", mpMaxPower->value); + } } } diff --git a/examples/color-light/color-light.h b/examples/color-light/color-light.h new file mode 100644 index 0000000..f99304b --- /dev/null +++ b/examples/color-light/color-light.h @@ -0,0 +1,246 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + */ + +#include +#include + +#include + +#include "configuration.h" +#include "color-fx.h" + +using namespace Service; +using namespace Service::API::devices; + +HomeGenie* homeGenie; +bool isConfigured = false; + +// Optional RGB Status LED +Adafruit_NeoPixel* statusLED = nullptr; + +// LED strip +uint16_t ledsCount = 0; int16_t ledsPin = -1; +int16_t pixelsType, pixelsSpeed; +Adafruit_NeoPixel* pixels = nullptr; +uint8_t maxPower = DEFAULT_MAX_POWER; + +// Preset colors (normalized HSV) +const int presetColorsCount = 7; +float presetColors[presetColorsCount][3] = { + { 202 / 360.0f, 1, 0.61 }, // Dodger Blue + { 0, 0, 1 }, // White + { 23 / 360.0f, 1, 0.65 }, // Coral + { 123 / 360.0f, 1, 0.42 }, // Fun Green + { 1 / 360.0f, 1, 0.54 }, // Scarlet + { 48 / 360.0f, 1, 0.67 }, // Mustard + { 260 / 360.0f, 1, 0.66 } // Heliotrope +}; +uint8_t presetColorIndex = 0; + +// Light animation styles +enum LightStyles { + SOLID = 0, + RAINBOW, + RAINBOW_2, + WHITE_STRIPES, + WHITE_STRIPES_2, + KALEIDOSCOPE, + STYLES_COUNT // this must be always the last element +}; +const char* const lightStyleNames[] = { + "solid", + "rainbow", + "rainbow_2", + "white_stripes", + "white_stripes_2", + "kaleidoscope" +}; +uint8_t getLightStyleIndex(String& styleName) { + for (int j = 0; j < LightStyles::STYLES_COUNT; j++) { + if (styleName == lightStyleNames[j]) { + return j; + } + } + return 0; +} + +// Status LED Blink callback used if statusLED is enabled +void statusLedCallback(bool isLedOn) { + if (isLedOn) { + statusLED->setPixelColor(0, Adafruit_NeoPixel::Color(1, 1, 0)); + } else { + statusLED->setPixelColor(0, Adafruit_NeoPixel::Color(0, 0, 0)); + } + statusLED->show(); +} + +ColorLight* mainModule; +LightColor currentColor; + +ModuleParameter* mpLedCount; +ModuleParameter* mpMaxPower; +ModuleParameter* mpFxStyle; +ModuleParameter* mpFxStrobe; + +// Buttons state variables +unsigned long lastStepCommandTs = -1; +uint8_t buttonCommand = -1; + +// Light animation effects state variables +unsigned int refreshMs = 10; +unsigned long lastRefreshTs = 0; +unsigned long strobeFxTickMs = 0; +unsigned int strobeFxDurationMs = 20; +unsigned int strobeFxIntervalMs = 80; // limit strobe to 10Hz (20 + 80 -> 100ms interval) +bool strobeOff = true; +uint8_t currentStyleIndex = 0; + +void refresh() { + if (statusLED != nullptr) { + statusLED->show(); + } + if (pixels != nullptr) { + pixels->show(); + } +} + +void renderPixels() { + if (pixels != nullptr) { + float pwr = ((float)maxPower / 100.0f); + for (int i = 0; i < ledsCount; i++) { + pixels->setPixelColor(i, + static_cast(round(animatedColors[i]->getRed() * pwr)), + static_cast(round(animatedColors[i]->getGreen() * pwr)), + static_cast(round(animatedColors[i]->getBlue() * pwr))); + } + } + if (statusLED != nullptr) { + statusLED->setPixelColor(0, + static_cast(round(currentColor.getRed())), + static_cast(round(currentColor.getGreen())), + static_cast(round(currentColor.getBlue()))); + } +} + +void createPixels() +{ + if (ledsPin >= 0) { + pixels = new Adafruit_NeoPixel(ledsCount, ledsPin, pixelsType + pixelsSpeed); + // turn off + //pixels->clear(); + //* + for (int i = 0; i < ledsCount; i++) { + pixels->setPixelColor(i, 0, 0, 0); + } + //*/ + pixels->begin(); + } +} +void disposePixels() { + + // turn off all pixels + if (pixels != nullptr) { + //pixels->clear(); + //* + for (int i = 0; i < ledsCount; i++) { + pixels->setPixelColor(i, 0, 0, 0); + } + refresh(); + //*/ + delete pixels; + delay(1000); + } + +} + +// Adds 2 "GpioButton" modules for handling manual control (on/off, level, switch fx, ...) +void setupControlButtons(Module* miniModule) { + static bool button1Pressed = false; + + auto btn1_pin = Config::getSetting("btn1-pin", "-1").toInt(); + if (btn1_pin >= 0) { + auto button1 = new Button(IO::IOEventDomains::HomeAutomation_HomeGenie, "B1", "Button 1", btn1_pin); + button1->onSetStatus([](Service::API::devices::ButtonStatus status) { + if (status == BUTTON_STATUS_PRESSED) { + button1Pressed = true; + } else { + button1Pressed = false; + buttonCommand = BUTTON_COMMAND_NONE; + } + }); + button1->onGesture([](Service::API::devices::ButtonGesture gesture) { + if (buttonCommand != BUTTON_COMMAND_NONE) return; + switch (gesture) { + case ButtonGesture::BUTTON_GESTURE_CLICK: { + mainModule->toggle(); + } + break; + case ButtonGesture::BUTTON_GESTURE_LONG_PRESS: { + if (mainModule->isOn()) { + buttonCommand = BUTTON_COMMAND_STEP_DIM; + } + } + break; + } + }); + } + auto btn2_pin = Config::getSetting("btn2-pin", "-1").toInt(); + if (btn2_pin >= 0) { + auto button2 = new Button(IO::IOEventDomains::HomeAutomation_HomeGenie, "B1", "Button 1", btn2_pin); + button2->onSetStatus([](Service::API::devices::ButtonStatus status) { + if (status == BUTTON_STATUS_PRESSED) { + if (button1Pressed && (buttonCommand == BUTTON_COMMAND_NONE || buttonCommand == BUTTON_COMMAND_PRESET_COLORS)) { + // Rotate preset colors + buttonCommand = BUTTON_COMMAND_PRESET_COLORS; + float h = presetColors[presetColorIndex][0]; + float s = presetColors[presetColorIndex][1]; + float v = presetColors[presetColorIndex][2]; + mainModule->setColor(h, s, v, 0); + presetColorIndex = (presetColorIndex + 1) % presetColorsCount; + } + } else if (!button1Pressed) { + buttonCommand = BUTTON_COMMAND_NONE; + } + }); + button2->onGesture([miniModule](Service::API::devices::ButtonGesture gesture) { + if (buttonCommand != BUTTON_COMMAND_NONE || button1Pressed || !mainModule->isOn()) return; + switch (gesture) { + case ButtonGesture::BUTTON_GESTURE_CLICK: { + currentStyleIndex = (currentStyleIndex + 1) % LightStyles::STYLES_COUNT; + auto msg = QueuedMessage(miniModule, "FX.Style", lightStyleNames[currentStyleIndex], nullptr, + IOEventDataType::Undefined); + miniModule->setProperty("FX.Style", lightStyleNames[currentStyleIndex], nullptr, IOEventDataType::Undefined); + HomeGenie::getInstance()->getEventRouter().signalEvent(msg); + } + break; + case ButtonGesture::BUTTON_GESTURE_LONG_PRESS: { + buttonCommand = BUTTON_COMMAND_STEP_BRIGHT; + } + break; + } + }); + } + +} diff --git a/examples/color-light/configuration.h b/examples/color-light/configuration.h index e69de29..18cd740 100644 --- a/examples/color-light/configuration.h +++ b/examples/color-light/configuration.h @@ -0,0 +1,9 @@ +// Limits LED strip power (percent [5% .. 100%]) +#define DEFAULT_MAX_POWER 25 + +#define BUTTON_COMMAND_STEP_INTERVAL 200 + +#define BUTTON_COMMAND_NONE 0 +#define BUTTON_COMMAND_STEP_BRIGHT 1 +#define BUTTON_COMMAND_STEP_DIM 2 +#define BUTTON_COMMAND_PRESET_COLORS 3 diff --git a/examples/smart-sensor-display/README.md b/examples/smart-sensor-display/README.md index b765612..fc82e10 100644 --- a/examples/smart-sensor-display/README.md +++ b/examples/smart-sensor-display/README.md @@ -6,7 +6,9 @@ Smart multi-sensor device with display. HomeGenie Mini UI example use. - [Documentation and firmware install page](https://homegenie.it/mini/1.2/examples/smart-sensor/) -## Firmware configuration (in addition to default system options) +## Firmware configuration + +In addition to default system options the following configuration options are available: | Key | Description | Default | |------------|-------------------------------------|---------| diff --git a/examples/smart-sensor/README.md b/examples/smart-sensor/README.md index 2de5abc..a359bce 100644 --- a/examples/smart-sensor/README.md +++ b/examples/smart-sensor/README.md @@ -5,7 +5,9 @@ A smart multi-sensor device. - [Documentation and firmware install page](https://homegenie.it/mini/1.2/examples/smart-sensor/) -## Firmware configuration (in addition to default system options) +## Firmware configuration + +In addition to default system options the following configuration options are available: | Key | Description | Default | |------------|-------------------------------------|---------| diff --git a/src/service/api/devices/Button.cpp b/src/service/api/devices/Button.cpp new file mode 100644 index 0000000..cae2546 --- /dev/null +++ b/src/service/api/devices/Button.cpp @@ -0,0 +1,120 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + */ + +#include "Button.h" + +namespace Service { namespace API { namespace devices { + + Button::Button(const char* domain, const char* address, const char* name, uint8_t pin, PullResistor resistor) { + module = new Module(); + module->domain = domain; + module->address = address; + module->type = "Switch"; + module->name = name; + // add properties + auto propStatusLevel = new ModuleParameter(IOEventPaths::Status_Level); + propStatusLevel->value = "0"; + module->properties.add(propStatusLevel); + moduleList.add(module); + + pinNumber = pin; + pinMode(pin, resistor); + } + + void Button::init() { + setLoopInterval(50); // ~50ms response time + } + void Button::loop() { + bool pressed = digitalRead(pinNumber) == LOW; + bool changed = false; + + if (pressed && status != BUTTON_STATUS_PRESSED) { + status = BUTTON_STATUS_PRESSED; + pressedTs = millis(); + changed = true; + } else if (!pressed && status == BUTTON_STATUS_PRESSED) { + status = BUTTON_STATUS_NORMAL; + changed = true; + } + + if (changed) { + + if (setStatusCallback != nullptr) { + setStatusCallback(status); + } + if (status == BUTTON_STATUS_NORMAL && millis() - pressedTs < 500) { + onGestureCallback(ButtonGesture::BUTTON_GESTURE_CLICK); + } + + // Event Stream Message Enqueue (for MQTT/SSE/WebSocket propagation) + auto eventValue = String(status); + auto msg = QueuedMessage(module, IOEventPaths::Status_Level, eventValue, &status, IOEventDataType::Number); + module->setProperty(IOEventPaths::Status_Level, eventValue, &status, IOEventDataType::Number); + HomeGenie::getInstance()->getEventRouter().signalEvent(msg); + + } else if (status == BUTTON_STATUS_PRESSED && millis() - pressedTs > 500 && pressedTs != -1) { + + pressedTs = -1; + onGestureCallback(ButtonGesture::BUTTON_GESTURE_LONG_PRESS); + + } + } + + bool Button::handleRequest(APIRequest *command, ResponseCallback* responseCallback) { + auto m = getModule(command->Domain.c_str(), command->Address.c_str()); + if (m != nullptr) { + + // TODO: implement device API + // return true; // <-- if API call was handled + + } + // not handled + return false; + } + + bool Button::canHandleDomain(String* domain) { + return domain->equals(IO::IOEventDomains::HomeAutomation_HomeGenie); + } + + bool Button::handleEvent(IIOEventSender *sender, + const char* domain, const char* address, + const char *eventPath, void *eventData, IOEventDataType dataType) { + return false; + } + + Module* Button::getModule(const char* domain, const char* address) { + for (int i = 0; i < moduleList.size(); i++) { + Module* m = moduleList.get(i); + if (m->domain.equals(domain) && m->address.equals(address)) + return m; + } + return nullptr; + } + + LinkedList* Button::getModuleList() { + return &moduleList; + } + +}}} diff --git a/src/service/api/devices/Button.h b/src/service/api/devices/Button.h new file mode 100644 index 0000000..b92cdf8 --- /dev/null +++ b/src/service/api/devices/Button.h @@ -0,0 +1,88 @@ +/* + * HomeGenie-Mini (c) 2018-2024 G-Labs + * + * + * This file is part of HomeGenie-Mini (HGM). + * + * HomeGenie-Mini is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * HomeGenie-Mini is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with HomeGenie-Mini. If not, see . + * + * + * Authors: + * - Generoso Martello + * + */ + +#ifndef HOMEGENIE_MINI_BUTTON_H +#define HOMEGENIE_MINI_BUTTON_H + +#include + +namespace Service { namespace API { namespace devices { + + enum ButtonStatus { + BUTTON_STATUS_NOT_SET = -1, + BUTTON_STATUS_NORMAL, + BUTTON_STATUS_PRESSED, + }; + enum ButtonGesture { + BUTTON_GESTURE_CLICK, + //BUTTON_GESTURE_DOUBLE_CLICK, + BUTTON_GESTURE_LONG_PRESS + }; + enum PullResistor { + NONE = INPUT, + PULL_UP = INPUT_PULLUP, +#ifndef ESP8266 + PULL_DOWN = INPUT_PULLDOWN +#endif + }; + + class Button: public Task, public APIHandler { + public: + Button(const char* domain, const char* address, const char* name, uint8_t pin, PullResistor mode = PULL_UP); + void init() override; + void loop() override; + bool canHandleDomain(String* domain) override; + bool handleRequest(APIRequest*, ResponseCallback*) override; + bool handleEvent(IIOEventSender*, + const char* domain, const char* address, + const char *eventPath, void* eventData, IOEventDataType) override; + + Module* getModule(const char* domain, const char* address) override; + LinkedList* getModuleList() override; + void onSetStatus(std::function callback) { + setStatusCallback = std::move(callback); + } + void onGesture(std::function callback) { + onGestureCallback = std::move(callback); + } + bool isPressed() { + return status == BUTTON_STATUS_PRESSED; + } + + Module* module; + private: + LinkedList moduleList; + std::function setStatusCallback = nullptr; + std::function onGestureCallback = nullptr; + uint8_t pinNumber; + unsigned long pressedTs = 0; + protected: + ButtonStatus status = BUTTON_STATUS_NOT_SET; + float onLevel = 1; + }; + +}}} + +#endif //HOMEGENIE_MINI_BUTTON_H diff --git a/src/service/api/devices/ColorLight.cpp b/src/service/api/devices/ColorLight.cpp index 4cf7a22..4c3ce3d 100644 --- a/src/service/api/devices/ColorLight.cpp +++ b/src/service/api/devices/ColorLight.cpp @@ -71,29 +71,7 @@ namespace Service { namespace API { namespace devices { hsvString = hsvString.substring(ci + 1); } while (oi < 4); - color.setColor(o[0], o[1], o[2], o[3]*1000); - - // Event Stream Message Enqueue (for MQTT/SSE/WebSocket propagation) - auto eventsDisable = module->getProperty(IOEventPaths::Events_Disabled); - if (eventsDisable == nullptr || eventsDisable->value == nullptr || eventsDisable->value != "1") { - // color - auto eventValue = command->getOption(0); - auto msg = QueuedMessage(m, IOEventPaths::Status_ColorHsb, eventValue, nullptr, IOEventDataType::Undefined); - m->setProperty(IOEventPaths::Status_ColorHsb, eventValue, nullptr, IOEventDataType::Undefined); - HomeGenie::getInstance()->getEventRouter().signalEvent(msg); - // level - auto levelValue = String(o[2]); // TODO: use sprintf %.6f - auto msg2 = QueuedMessage(m, IOEventPaths::Status_Level, levelValue, nullptr, IOEventDataType::Undefined); - m->setProperty(IOEventPaths::Status_Level, levelValue, nullptr, IOEventDataType::Undefined); - HomeGenie::getInstance()->getEventRouter().signalEvent(msg2); - } - - if (o[2] > 0) { - Switch::status = SWITCH_STATUS_ON; - Dimmer::onLevel = Switch::onLevel = o[2]; - } else { - Switch::status = SWITCH_STATUS_OFF; - } + setColor(o[0], o[1], o[2], o[3]*1000); responseCallback->writeAll(ApiHandlerResponseText::OK); return true; @@ -103,4 +81,36 @@ namespace Service { namespace API { namespace devices { return false; } -}}} \ No newline at end of file + void ColorLight::setColor(float h, float s, float v, unsigned long transition) { + + color.setColor(h, s, v, transition); + + // Event Stream Message Enqueue (for MQTT/SSE/WebSocket propagation) + auto eventsDisable = module->getProperty(IOEventPaths::Events_Disabled); + if (eventsDisable == nullptr || eventsDisable->value == nullptr || eventsDisable->value != "1") { + // color + int size = snprintf(nullptr, 0, "%f,%f,%f", h, s, v); + char eventValue[size + 1]; + sprintf(eventValue, "%f,%f,%f", h, s, v); + auto msg = QueuedMessage(module, IOEventPaths::Status_ColorHsb, eventValue, nullptr, IOEventDataType::Undefined); + module->setProperty(IOEventPaths::Status_ColorHsb, eventValue, nullptr, IOEventDataType::Undefined); + HomeGenie::getInstance()->getEventRouter().signalEvent(msg); + // level + size = snprintf(nullptr, 0, "%f", v); + char levelValue[size + 1]; + sprintf(levelValue, "%f", v); + auto msg2 = QueuedMessage(module, IOEventPaths::Status_Level, levelValue, nullptr, IOEventDataType::Undefined); + module->setProperty(IOEventPaths::Status_Level, levelValue, nullptr, IOEventDataType::Undefined); + HomeGenie::getInstance()->getEventRouter().signalEvent(msg2); + } + + if (v > 0) { + Switch::status = SWITCH_STATUS_ON; + Dimmer::onLevel = Switch::onLevel = v; + } else { + Switch::status = SWITCH_STATUS_OFF; + } + + } + +}}} diff --git a/src/service/api/devices/ColorLight.h b/src/service/api/devices/ColorLight.h index f892f73..fcf2102 100644 --- a/src/service/api/devices/ColorLight.h +++ b/src/service/api/devices/ColorLight.h @@ -77,20 +77,20 @@ namespace Service { namespace API { namespace devices { v = val; } float getRed() const { - auto orgb = Utility::hsv2rgb(hueFix(oh), os, ov); - auto crgb = Utility::hsv2rgb(hueFix(h), s, v); + auto orgb = Utility::hsv2rgb(oh, os, ov); + auto crgb = Utility::hsv2rgb(h, s, v); float r = (float)orgb.r + ((float)(crgb.r - orgb.r) * getProgress()); return r; } float getGreen() const { - auto orgb = Utility::hsv2rgb(hueFix(oh), os, ov); - auto crgb = Utility::hsv2rgb(hueFix(h), s, v); + auto orgb = Utility::hsv2rgb(oh, os, ov); + auto crgb = Utility::hsv2rgb(h, s, v); float g = (float)orgb.g + ((float)(crgb.g - orgb.g) * getProgress()); return g; } float getBlue() const { - auto orgb = Utility::hsv2rgb(hueFix(oh), os, ov); - auto crgb = Utility::hsv2rgb(hueFix(h), s, v); + auto orgb = Utility::hsv2rgb(oh, os, ov); + auto crgb = Utility::hsv2rgb(h, s, v); float b = (float)orgb.b + ((float)(crgb.b - orgb.b) * getProgress()); return b; } @@ -99,9 +99,6 @@ namespace Service { namespace API { namespace devices { float oh = 0, os = 0, ov = 0; unsigned long startTime = -1; unsigned long duration = 0; - static float hueFix(float h) { - return 1.325f - h; - } }; @@ -112,6 +109,8 @@ namespace Service { namespace API { namespace devices { void loop() override; bool handleRequest(APIRequest*, ResponseCallback*) override; + void setColor(float h, float s, float v, unsigned long transition); + void onSetColor(std::function callback) { setColorCallback = std::move(callback); } diff --git a/src/service/api/devices/Dimmer.cpp b/src/service/api/devices/Dimmer.cpp index d2a7ef9..9622ee9 100644 --- a/src/service/api/devices/Dimmer.cpp +++ b/src/service/api/devices/Dimmer.cpp @@ -51,27 +51,10 @@ namespace Service { namespace API { namespace devices { auto m = getModule(command->Domain.c_str(), command->Address.c_str()); if (m != nullptr && command->Command == ControlApi::Control_Level) { - auto l = command->getOption(0).toFloat() / 100.0F; // 0.00 - 1.00 0 = OFF, 1.00 = MAX + auto l = command->getOption(0).toFloat() / 100.0f; // 0.00 - 1.00 0 = OFF, 1.00 = MAX auto transition = command->getOption(1).isEmpty() ? defaultTransitionMs : command->getOption(1).toFloat(); // ms - level.setLevel(l, transition); - - // Event Stream Message Enqueue (for MQTT/SSE/WebSocket propagation) - auto eventsDisable = module->getProperty(IOEventPaths::Events_Disabled); - if (eventsDisable == nullptr || eventsDisable->value == nullptr || eventsDisable->value != "1") { - auto eventPath = IOEventPaths::Status_Level; - auto eventValue = String(l); - auto msg = QueuedMessage(m, eventPath, eventValue, &l, IOEventDataType::Float); - m->setProperty(eventPath, eventValue, &l, IOEventDataType::Float); - HomeGenie::getInstance()->getEventRouter().signalEvent(msg); - } - - if (l > 0) { - Switch::status = SWITCH_STATUS_ON; - Switch::onLevel = l; - } else { - Switch::status = SWITCH_STATUS_OFF; - } + setLevel(l, transition); responseCallback->writeAll(ApiHandlerResponseText::OK); return true; @@ -81,4 +64,39 @@ namespace Service { namespace API { namespace devices { return false; } + void Dimmer::dim(float transition) { + if (level.getLevel() <= 0.05) return; + auto l = level.getLevel() - 0.05f; + if (l < 0.05) l = 0.05; + setLevel(l, transition); + } + void Dimmer::bright(float transition) { + if (level.getLevel() == 1) return; + auto l = level.getLevel() + 0.05f; + if (l > 1) l = 1; + setLevel(l, transition); + } + void Dimmer::setLevel(float l, float transition) { + + level.setLevel(l, transition); + + // Event Stream Message Enqueue (for MQTT/SSE/WebSocket propagation) + auto eventsDisable = module->getProperty(IOEventPaths::Events_Disabled); + if (eventsDisable == nullptr || eventsDisable->value == nullptr || eventsDisable->value != "1") { + auto eventPath = IOEventPaths::Status_Level; + auto eventValue = String(l); + auto msg = QueuedMessage(module, eventPath, eventValue, &l, IOEventDataType::Float); + module->setProperty(eventPath, eventValue, &l, IOEventDataType::Float); + HomeGenie::getInstance()->getEventRouter().signalEvent(msg); + } + + if (l > 0) { + Switch::status = SWITCH_STATUS_ON; + Switch::onLevel = l; + } else { + Switch::status = SWITCH_STATUS_OFF; + } + + } + }}} \ No newline at end of file diff --git a/src/service/api/devices/Dimmer.h b/src/service/api/devices/Dimmer.h index 4d19409..d5b17a5 100644 --- a/src/service/api/devices/Dimmer.h +++ b/src/service/api/devices/Dimmer.h @@ -38,6 +38,8 @@ namespace Service { namespace API { namespace devices { bool isAnimating = false; void setLevel(float l, unsigned long transitionMs) { duration = transitionMs; + if (l < 0) l = 0; + else if (l > 1) l = 1; ol = level; level = l; startTime = millis(); @@ -68,6 +70,10 @@ namespace Service { namespace API { namespace devices { bool handleRequest(APIRequest*, ResponseCallback*) override; + void dim(float transition = defaultTransitionMs); + void bright(float transition = defaultTransitionMs); + void setLevel(float l, float transition); + void onSetLevel(std::function callback) { setLevelCallback = std::move(callback); } @@ -75,7 +81,7 @@ namespace Service { namespace API { namespace devices { std::function setLevelCallback = nullptr; protected: DimmerLevel level; - const unsigned long defaultTransitionMs = 500; + static const unsigned long defaultTransitionMs = 400; }; }}} diff --git a/src/service/api/devices/Switch.cpp b/src/service/api/devices/Switch.cpp index 2959c5a..5b7136f 100644 --- a/src/service/api/devices/Switch.cpp +++ b/src/service/api/devices/Switch.cpp @@ -50,35 +50,19 @@ namespace Service { namespace API { namespace devices { auto m = getModule(command->Domain.c_str(), command->Address.c_str()); if (m != nullptr) { - auto eventPath = IOEventPaths::Status_Level; - SwitchStatus s = SWITCH_STATUS_NOT_SET; + bool handled = true; if (command->Command == ControlApi::Control_On) { - s = SWITCH_STATUS_ON; + on(); } else if (command->Command == ControlApi::Control_Off) { - s = SWITCH_STATUS_OFF; + off(); } else if (command->Command == ControlApi::Control_Toggle) { - s = status == SWITCH_STATUS_ON ? SWITCH_STATUS_OFF : SWITCH_STATUS_ON; + toggle(); + } else { + handled = false; } - if (s != SWITCH_STATUS_NOT_SET) { - status = s; - - if (setStatusCallback != nullptr) { - setStatusCallback(status); - } - - // Event Stream Message Enqueue (for MQTT/SSE/WebSocket propagation) - auto eventsDisable = module->getProperty(IOEventPaths::Events_Disabled); - if (eventsDisable == nullptr || eventsDisable->value == nullptr || eventsDisable->value != "1") { - float l = status == SWITCH_STATUS_ON ? onLevel : 0; - auto eventValue = String(l); - auto msg = QueuedMessage(m, eventPath, eventValue, &l, IOEventDataType::Float); - m->setProperty(eventPath, eventValue, &l, IOEventDataType::Float); - HomeGenie::getInstance()->getEventRouter().signalEvent(msg); - } - + if (handled) { responseCallback->writeAll(ApiHandlerResponseText::OK); - return true; } @@ -110,4 +94,36 @@ namespace Service { namespace API { namespace devices { return &moduleList; } + void Switch::on() { + setStatus(SWITCH_STATUS_ON); + } + + void Switch::off() { + setStatus(SWITCH_STATUS_OFF); + } + + void Switch::toggle() { + setStatus(status == SWITCH_STATUS_ON ? SWITCH_STATUS_OFF : SWITCH_STATUS_ON); + } + + void Switch::setStatus(SwitchStatus s) { + + status = s; + + if (setStatusCallback != nullptr) { + setStatusCallback(status); + } + + // Event Stream Message Enqueue (for MQTT/SSE/WebSocket propagation) + auto eventPath = IOEventPaths::Status_Level; + auto eventsDisable = module->getProperty(IOEventPaths::Events_Disabled); + if (eventsDisable == nullptr || eventsDisable->value == nullptr || eventsDisable->value != "1") { + float l = status == SWITCH_STATUS_ON ? onLevel : 0; + auto eventValue = String(l); + auto msg = QueuedMessage(module, eventPath, eventValue, &l, IOEventDataType::Float); + module->setProperty(eventPath, eventValue, &l, IOEventDataType::Float); + HomeGenie::getInstance()->getEventRouter().signalEvent(msg); + } + + } }}} diff --git a/src/service/api/devices/Switch.h b/src/service/api/devices/Switch.h index 52a9234..b5ce5b4 100644 --- a/src/service/api/devices/Switch.h +++ b/src/service/api/devices/Switch.h @@ -49,17 +49,24 @@ namespace Service { namespace API { namespace devices { Module* getModule(const char* domain, const char* address) override; LinkedList* getModuleList() override; - void onSetStatus(std::function callback) { - setStatusCallback = std::move(callback); - } + bool isOn() { return status == SWITCH_STATUS_ON; } + void on(); + void off(); + void toggle(); + + void onSetStatus(std::function callback) { + setStatusCallback = std::move(callback); + } + Module* module; private: LinkedList moduleList; std::function setStatusCallback = nullptr; + void setStatus(SwitchStatus s); protected: SwitchStatus status = SWITCH_STATUS_NOT_SET; float onLevel = 1;