From 7f63aa4f06e57b3e1ac16afefd9042084f31b0f6 Mon Sep 17 00:00:00 2001 From: Kostiantyn Synytsia <20212876+martaisty@users.noreply.github.com> Date: Sat, 27 May 2023 12:42:24 +0300 Subject: [PATCH 1/3] add effects for HALight --- src/device-types/HALight.cpp | 114 +++++++++++- src/device-types/HALight.h | 94 +++++++++- src/utils/HADictionary.cpp | 3 + src/utils/HADictionary.h | 3 + tests/LightTest/LightTest.ino | 324 +++++++++++++++++++++++++++++++++- 5 files changed, 532 insertions(+), 6 deletions(-) diff --git a/src/device-types/HALight.cpp b/src/device-types/HALight.cpp index 03f6dfe2..d889390c 100644 --- a/src/device-types/HALight.cpp +++ b/src/device-types/HALight.cpp @@ -58,14 +58,31 @@ HALight::HALight(const char* uniqueId, const uint8_t features) : _maxMireds(), _currentColorTemperature(0), _currentRGBColor(), + _effects(nullptr), + _currentEffect(0), _stateCallback(nullptr), _brightnessCallback(nullptr), _colorTemperatureCallback(nullptr), - _rgbColorCallback(nullptr) + _rgbColorCallback(nullptr), + _effectCallback(nullptr) { } +HALight::~HALight() +{ + if (_effects) { + const uint8_t effectsNb = _effects->getItemsNb(); + const HASerializerArray::ItemType* effects = _effects->getItems(); + + for (uint8_t i = 0; i < effectsNb; i++) { + delete[] effects[i]; + } + + delete _effects; + } +} + bool HALight::setState(const bool state, const bool force) { if (!force && state == _currentState) { @@ -122,13 +139,49 @@ bool HALight::setRGBColor(const RGBColor& color, const bool force) return false; } +void HALight::setEffects(const char* const effects[], const uint8_t size) +{ + if (!(_features & EffectsFeature) || !effects || size == 0 || _effects) { // effects can be set only once + return; + } + + _effects = new HASerializerArray(size, false); + + uint8_t effectLen = 0; + for (uint8_t i = 0; i < size; i++) { + effectLen = strlen(effects[i]); + + char* const effect = new char[effectLen + 1]; // include null terminator + effect[effectLen] = '\0'; + memcpy(effect, effects[i], effectLen); + + _effects->add(effect); + } +} + +bool HALight::setEffect(const uint8_t effect, const bool force) +{ + if (!force && effect == _currentEffect) { + return true; + } + + if (publishEffect(effect)) { + _currentEffect = effect; + return true; + } + + return false; +} + void HALight::buildSerializer() { - if (_serializer || !uniqueId()) { + // EffectsFeature enabled and no _effects set => unlogical + const bool effectsEnabledButNotSet = (_features & EffectsFeature) && !_effects; + if (_serializer || !uniqueId() || effectsEnabledButNotSet) { return; } - _serializer = new HASerializer(this, 19); // 19 - max properties nb + _serializer = new HASerializer(this, 22); // 22 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); _serializer->set(HASerializer::WithUniqueId); @@ -189,6 +242,17 @@ void HALight::buildSerializer() _serializer->topic(AHATOFSTR(HARGBStateTopic)); } + if (_features & EffectsFeature) { + _serializer->topic(AHATOFSTR(HAEffectStateTopic)); + _serializer->topic(AHATOFSTR(HAEffectCommandTopic)); + + _serializer->set( + AHATOFSTR(HAEffectsProperty), + _effects, + HASerializer::ArrayPropertyType + ); + } + _serializer->set(HASerializer::WithDevice); _serializer->set(HASerializer::WithAvailability); _serializer->topic(AHATOFSTR(HAStateTopic)); @@ -209,6 +273,7 @@ void HALight::onMqttConnected() publishBrightness(_currentBrightness); publishColorTemperature(_currentColorTemperature); publishRGBColor(_currentRGBColor); + publishEffect(_currentEffect); } subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); @@ -224,6 +289,10 @@ void HALight::onMqttConnected() if (_features & RGBFeature) { subscribeTopic(uniqueId(), AHATOFSTR(HARGBCommandTopic)); } + + if (_features & EffectsFeature) { + subscribeTopic(uniqueId(), AHATOFSTR(HAEffectCommandTopic)); + } } void HALight::onMqttMessage( @@ -258,6 +327,14 @@ void HALight::onMqttMessage( ) ) { handleRGBCommand(payload, length); + } else if ( + HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HAEffectCommandTopic) + ) + ) { + handleEffectCommand(payload, length); } } @@ -317,6 +394,20 @@ bool HALight::publishRGBColor(const RGBColor& color) return publishOnDataTopic(AHATOFSTR(HARGBStateTopic), str, true); } +bool HALight::publishEffect(const uint8_t effect) +{ + if (!(_features & EffectsFeature) || !_effects || effect >= _effects->getItemsNb()) { + return false; + } + + const char* effectName = _effects->getItems()[effect]; + if (!effectName) { + return false; + } + + return publishOnDataTopic(AHATOFSTR(HAEffectStateTopic), effectName, true); +} + void HALight::handleStateCommand(const uint8_t* cmd, const uint16_t length) { (void)cmd; @@ -370,4 +461,21 @@ void HALight::handleRGBCommand(const uint8_t* cmd, const uint16_t length) } } +void HALight::handleEffectCommand(const uint8_t* cmd, const uint16_t length) +{ + if (!_effectCallback) { + return; + } + + const uint8_t effectsNb = _effects->getItemsNb(); + const HASerializerArray::ItemType* effects = _effects->getItems(); + + for (uint8_t i = 0; i < effectsNb; i++) { + if (strlen(effects[i]) == length && memcmp(cmd, effects[i], length) == 0) { + _effectCallback(i, this); + return; + } + } +} + #endif diff --git a/src/device-types/HALight.h b/src/device-types/HALight.h index 021d32f5..07ba9d3d 100644 --- a/src/device-types/HALight.h +++ b/src/device-types/HALight.h @@ -6,14 +6,17 @@ #ifndef EX_ARDUINOHA_LIGHT +class HASerializerArray; + #define HALIGHT_STATE_CALLBACK(name) void (*name)(bool state, HALight* sender) #define HALIGHT_BRIGHTNESS_CALLBACK(name) void (*name)(uint8_t brightness, HALight* sender) #define HALIGHT_COLOR_TEMP_CALLBACK(name) void (*name)(uint16_t temperature, HALight* sender) #define HALIGHT_RGB_COLOR_CALLBACK(name) void (*name)(HALight::RGBColor color, HALight* sender) +#define HALIGHT_EFFECT_CALLBACK(name) void (*name)(uint8_t index, HALight* sender) /** * HALight allows adding a controllable light in the Home Assistant panel. - * The library supports only the state, brightness, color temperature and RGB color. + * The library supports only the state, brightness, color temperature, RGB color and effects. * If you need more features please open a new GitHub issue. * * @note @@ -29,7 +32,8 @@ class HALight : public HABaseDeviceType DefaultFeatures = 0, BrightnessFeature = 1, ColorTemperatureFeature = 2, - RGBFeature = 4 + RGBFeature = 4, + EffectsFeature = 8 }; struct RGBColor { @@ -77,6 +81,7 @@ class HALight : public HABaseDeviceType * `HALight::BrightnessFeature | HALight::ColorTemperatureFeature` */ HALight(const char* uniqueId, const uint8_t features = DefaultFeatures); + ~HALight(); /** * Changes state of the light and publishes MQTT message. @@ -122,6 +127,33 @@ class HALight : public HABaseDeviceType */ bool setRGBColor(const RGBColor& color, const bool force = false); + /** + * Sets the list of available effects that will be listed. + * For example: + * ` + * const char* const lightEffects[] = {"Fire","Rainbow","Snowflales","Rain","Smoke"}; + * light.setEffects(lightEffects, 5); + * ` + * + * + * @param effects The list of effects i.e. array of strings. + * @param size The size of the effects list i.e. total number of effects. + * @note The effects list can be set only once. + */ + void setEffects(const char* const effects[], const uint8_t size); + + /** + * Changes the effect of the light and publishes MQTT message. + * Effect represents the index of the effect that was set using setEffects method. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param effect The new effect index of the light. + * @param force Forces to update the value without comparing it to a previous known value. + * @return Returns `true` if the effect is set successfully. + */ + bool setEffect(const uint8_t effect, const bool force = false); + /** * Alias for `setState(true)`. */ @@ -202,6 +234,24 @@ class HALight : public HABaseDeviceType inline const RGBColor& getCurrentRGBColor() const { return _currentRGBColor; } + /** + * Sets the current effect of the light without pushing the value to Home Assistant. + * This method may be useful if you want to change the effect before the connection + * with the MQTT broker is acquired. + * + * @param effect The new effect. + */ + inline void setCurrentEffect(const uint8_t effect) + { _currentEffect = effect; } + + /** + * Returns the last known effect of the light. + * Effect represents the index of the effect that was set using setEffects method. + * By default the effect is set to `0`. + */ + inline uint8_t getCurrentEffect() const + { return _currentEffect; } + /** * Sets icon of the light. * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). @@ -297,6 +347,21 @@ class HALight : public HABaseDeviceType inline void onRGBColorCommand(HALIGHT_RGB_COLOR_CALLBACK(callback)) { _rgbColorCallback = callback; } + /** + * Registers callback that will be called each time the effect command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same light. + * + * @param callback + * @note In non-optimistic mode, the effect must be reported back to HA using the HALight::setEffect method. + */ + inline void onEffectCommand(HALIGHT_EFFECT_CALLBACK(callback)) + { _effectCallback = callback; } + +#ifdef ARDUINOHA_TEST + inline HASerializerArray* getEffects() const + { return _effects; } +#endif + protected: virtual void buildSerializer() override; virtual void onMqttConnected() override; @@ -339,6 +404,14 @@ class HALight : public HABaseDeviceType */ bool publishRGBColor(const RGBColor& color); + /** + * Publishes the MQTT message with the given effect. + * + * @param effect The effect to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishEffect(const uint8_t effect); + /** * Parses the given state command and executes the callback with proper value. * @@ -371,6 +444,14 @@ class HALight : public HABaseDeviceType */ void handleRGBCommand(const uint8_t* cmd, const uint16_t length); + /** + * Parses the given effect command and executes the callback with proper value. + * + * @param cmd The data of the command. + * @param length Length of the command. + */ + void handleEffectCommand(const uint8_t* cmd, const uint16_t length); + /// Features enabled for the light. const uint8_t _features; @@ -404,6 +485,12 @@ class HALight : public HABaseDeviceType /// The current RBB color. By default the value is not set. RGBColor _currentRGBColor; + /// Array of effects for the serializer. + HASerializerArray* _effects; + + /// The current effect (the current effect's index). By default it's `0`. + uint8_t _currentEffect; + /// The callback that will be called when the state command is received from the HA. HALIGHT_STATE_CALLBACK(_stateCallback); @@ -415,6 +502,9 @@ class HALight : public HABaseDeviceType /// The callback that will be called when the RGB command is received from the HA. HALIGHT_RGB_COLOR_CALLBACK(_rgbColorCallback); + + /// The callback that will be called when the effect is received from the HA. + HALIGHT_EFFECT_CALLBACK(_effectCallback); }; #endif diff --git a/src/utils/HADictionary.cpp b/src/utils/HADictionary.cpp index 266d21dd..ba14aa9c 100644 --- a/src/utils/HADictionary.cpp +++ b/src/utils/HADictionary.cpp @@ -65,6 +65,7 @@ const char HASpeedRangeMinProperty[] PROGMEM = {"spd_rng_min"}; const char HABrightnessScaleProperty[] PROGMEM = {"bri_scl"}; const char HAMinMiredsProperty[] PROGMEM = {"min_mirs"}; const char HAMaxMiredsProperty[] PROGMEM = {"max_mirs"}; +const char HAEffectsProperty[] PROGMEM = {"fx_list"}; const char HATemperatureUnitProperty[] PROGMEM = {"temp_unit"}; const char HAMinTempProperty[] PROGMEM = {"min_temp"}; const char HAMaxTempProperty[] PROGMEM = {"max_temp"}; @@ -105,6 +106,8 @@ const char HATemperatureStateTopic[] PROGMEM = {"temp_stat_t"}; const char HARGBCommandTopic[] PROGMEM = {"rgb_cmd_t"}; const char HARGBStateTopic[] PROGMEM = {"rgb_stat_t"}; const char HAJsonAttributesTopic[] PROGMEM = {"json_attr_t"}; +const char HAEffectCommandTopic[] PROGMEM = {"fx_cmd_t"}; +const char HAEffectStateTopic[] PROGMEM = {"fx_stat_t"}; // misc const char HAOnline[] PROGMEM = {"online"}; diff --git a/src/utils/HADictionary.h b/src/utils/HADictionary.h index 609f38df..27527e06 100644 --- a/src/utils/HADictionary.h +++ b/src/utils/HADictionary.h @@ -65,6 +65,7 @@ extern const char HASpeedRangeMinProperty[]; extern const char HABrightnessScaleProperty[]; extern const char HAMinMiredsProperty[]; extern const char HAMaxMiredsProperty[]; +extern const char HAEffectsProperty[]; extern const char HATemperatureUnitProperty[]; extern const char HAMinTempProperty[]; extern const char HAMaxTempProperty[]; @@ -105,6 +106,8 @@ extern const char HATemperatureStateTopic[]; extern const char HARGBCommandTopic[]; extern const char HARGBStateTopic[]; extern const char HAJsonAttributesTopic[]; +extern const char HAEffectCommandTopic[]; +extern const char HAEffectStateTopic[]; // misc extern const char HAOnline[]; diff --git a/tests/LightTest/LightTest.ino b/tests/LightTest/LightTest.ino index cd2ce1a5..d1f4d49b 100644 --- a/tests/LightTest/LightTest.ino +++ b/tests/LightTest/LightTest.ino @@ -6,7 +6,8 @@ lastStateCallbackCall.reset(); \ lastBrightnessCallbackCall.reset(); \ lastColorTempCallbackCall.reset(); \ - lastRGBColorCallbackCall.reset(); + lastRGBColorCallbackCall.reset(); \ + lastEffectCallbackCall.reset(); #define assertStateCallbackCalled(expectedState, callerPtr) \ assertTrue(lastStateCallbackCall.called); \ @@ -41,6 +42,14 @@ #define assertRGBColorCallbackNotCalled() \ assertFalse(lastRGBColorCallbackCall.called); +#define assertEffectCallbackCalled(expectedIndex, callerPtr) \ + assertTrue(lastEffectCallbackCall.called); \ + assertEqual(expectedIndex, lastEffectCallbackCall.index); \ + assertEqual(callerPtr, lastEffectCallbackCall.caller); + +#define assertEffectCallbackNotCalled() \ + assertFalse(lastEffectCallbackCall.called); + using aunit::TestRunner; struct StateCallback { @@ -91,12 +100,25 @@ struct RGBCommandCallback { } }; +struct EffectCallback { + bool called = false; + uint8_t index = 0; + HALight* caller = nullptr; + + void reset() { + called = false; + index = 0; + caller = nullptr; + } +}; + static const char* testDeviceId = "testDevice"; static const char* testUniqueId = "uniqueLight"; static StateCallback lastStateCallbackCall; static BrightnessCallback lastBrightnessCallbackCall; static ColorTemperatureCallback lastColorTempCallbackCall; static RGBCommandCallback lastRGBColorCallbackCall; +static EffectCallback lastEffectCallbackCall; const char ConfigTopic[] PROGMEM = {"homeassistant/light/testDevice/uniqueLight/config"}; const char StateTopic[] PROGMEM = {"testData/testDevice/uniqueLight/stat_t"}; @@ -107,6 +129,8 @@ const char BrightnessCommandTopic[] PROGMEM = {"testData/testDevice/uniqueLight/ const char ColorTemperatureCommandTopic[] PROGMEM = {"testData/testDevice/uniqueLight/clr_temp_cmd_t"}; const char RGBCommandTopic[] PROGMEM = {"testData/testDevice/uniqueLight/rgb_cmd_t"}; const char RGBStateTopic[] PROGMEM = {"testData/testDevice/uniqueLight/rgb_stat_t"}; +const char EffectCommandTopic[] PROGMEM = {"testData/testDevice/uniqueLight/fx_cmd_t"}; +const char EffectStateTopic[] PROGMEM = {"testData/testDevice/uniqueLight/fx_stat_t"}; void onStateCommandReceived(bool state, HALight* caller) { @@ -136,6 +160,13 @@ void onRGBColorCommand(HALight::RGBColor color, HALight* caller) lastRGBColorCallbackCall.caller = caller; } +void onEffectCommandReceived(uint8_t index, HALight* caller) +{ + lastEffectCallbackCall.called = true; + lastEffectCallbackCall.index = index; + lastEffectCallbackCall.caller = caller; +} + AHA_TEST(LightTest, invalid_unique_id) { prepareTest @@ -278,6 +309,106 @@ AHA_TEST(LightTest, default_params_with_brightness_and_color_temp) { assertEqual(4, mock->getFlushedMessagesNb()); } +AHA_TEST(LightTest, default_params_with_no_effects_set) { + prepareTest + + HALight light(testUniqueId, HALight::EffectsFeature); + light.buildSerializerTest(); + HASerializer* serializer = light.getSerializer(); + + assertTrue(serializer == nullptr); + assertTrue(light.getEffects() == nullptr); +} + +AHA_TEST(LightTest, default_params_with_invalid_effects_disabled_feature) { + prepareTest + + const char* const effects[] = { "Effect 1", "Effect 2" }; + + HALight light(testUniqueId); + light.setEffects(effects, 2); + light.buildSerializerTest(); + HASerializer* serializer = light.getSerializer(); + + assertTrue(serializer != nullptr); + assertTrue(light.getEffects() == nullptr); +} + +AHA_TEST(LightTest, default_params_with_invalid_effects_nullptr) { + prepareTest + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(nullptr, 1); + light.buildSerializerTest(); + HASerializer* serializer = light.getSerializer(); + + assertTrue(serializer == nullptr); + assertTrue(light.getEffects() == nullptr); +} + +AHA_TEST(LightTest, default_params_with_invalid_effects_zero_size) { + prepareTest + + const char* const effects[] = { "Effect 1", "Effect 2" }; + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 0); + light.buildSerializerTest(); + HASerializer* serializer = light.getSerializer(); + + assertTrue(serializer == nullptr); + assertTrue(light.getEffects() == nullptr); +} + +AHA_TEST(LightTest, default_params_with_effects_single_init) { + prepareTest + + const char* const effects[] = { "Effect 1" }; + const char* const notAbleToSetEffects[] = { "Effect 2" }; + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 1); + light.setEffects(notAbleToSetEffects, 1); // effects already initialized at previous line + light.buildSerializerTest(); + HASerializer* serializer = light.getSerializer(); + + assertTrue(serializer != nullptr); + + HASerializerArray* lightEffects = light.getEffects(); + + assertTrue(lightEffects != nullptr); + assertEqual(1, lightEffects->getItemsNb()); + assertEqual(0, strcmp(effects[0], lightEffects->getItems()[0])); +} + +AHA_TEST(LightTest, default_params_with_effects) { + prepareTest + + const char* const effects[] = { "Effect 1", "Effect 2" }; + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 2); + + assertEntityConfig( + mock, + light, + ( + "{" + "\"uniq_id\":\"uniqueLight\"," + "\"fx_stat_t\":\"testData/testDevice/uniqueLight/fx_stat_t\"," + "\"fx_cmd_t\":\"testData/testDevice/uniqueLight/fx_cmd_t\"," + "\"fx_list\":[\"Effect 1\",\"Effect 2\"]," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueLight/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueLight/cmd_t\"" + "}" + ) + ) + + // config + default state + default effect + assertEqual(3, mock->getFlushedMessagesNb()); +} + AHA_TEST(LightTest, state_command_subscription) { prepareTest @@ -330,6 +461,22 @@ AHA_TEST(LightTest, rgb_command_subscription) { ); } +AHA_TEST(LightTest, effect_command_subscription) { + prepareTest + + const char* const effects[] = { "Effect 1", "Effect 2" }; + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 2); + mqtt.loop(); + + assertEqual(2, mock->getSubscriptionsNb()); + assertEqual( + AHATOFSTR(EffectCommandTopic), + mock->getSubscriptions()[1]->topic + ); +} + AHA_TEST(LightTest, availability) { prepareTest @@ -390,6 +537,20 @@ AHA_TEST(LightTest, publish_last_known_rgb_color) { assertMqttMessage(2, AHATOFSTR(RGBStateTopic), "255,123,15", true) } +AHA_TEST(LightTest, publish_last_known_effect) { + prepareTest + + const char* const effects[] = { "Effect 1", "Effect 2" }; + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 2); + light.setCurrentEffect(1); + mqtt.loop(); + + assertEqual(3, mock->getFlushedMessagesNb()); + assertMqttMessage(2, AHATOFSTR(EffectStateTopic), "Effect 2", true) +} + AHA_TEST(LightTest, publish_nothing_if_retained) { prepareTest @@ -636,6 +797,16 @@ AHA_TEST(LightTest, current_rgb_color_setter) { assertTrue(HALight::RGBColor(255,123,111) == light.getCurrentRGBColor()); } +AHA_TEST(LightTest, current_effect_setter) { + prepareTest + + HALight light(testUniqueId); + light.setCurrentEffect(3); + + assertEqual(0, mock->getFlushedMessagesNb()); + assertEqual((uint8_t)3, light.getCurrentEffect()); +} + AHA_TEST(LightTest, publish_state) { prepareTest @@ -788,6 +959,89 @@ AHA_TEST(LightTest, publish_rgb_color_debounce_skip) { assertSingleMqttMessage(AHATOFSTR(RGBStateTopic), "255,123,111", true) } +AHA_TEST(LightTest, publish_nothing_if_effects_feature_is_disabled) { + prepareTest + + mock->connectDummy(); + HALight light(testUniqueId); + + assertFalse(light.setEffect(5)); + assertEqual(mock->getFlushedMessagesNb(), 0); +} + +AHA_TEST(LightTest, publish_effect_first) { + prepareTest + + const char* const effects[] = { "Effect 1", "Effect 2", "Effect 3" }; + + mock->connectDummy(); + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 3); + + assertTrue(light.setEffect(0)); + // First effect is a default one, shouldn't publish + assertEqual(mock->getFlushedMessagesNb(), 0); +} + +AHA_TEST(LightTest, publish_effect_last) { + prepareTest + + const char* const effects[] = { "Effect 1", "Effect 2", "Effect 3" }; + + mock->connectDummy(); + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 3); + + assertTrue(light.setEffect(2)); + assertSingleMqttMessage(AHATOFSTR(EffectStateTopic), "Effect 3", true) +} + +AHA_TEST(LightTest, publish_effect_only) { + prepareTest + + const char* const effects[] = { "Effect 1" }; + + mock->connectDummy(); + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 1); + + assertTrue(light.setEffect(0)); + // First effect is a default one, shouldn't publish + assertEqual(mock->getFlushedMessagesNb(), 0); +} + +AHA_TEST(LightTest, publish_effect_debounce) { + prepareTest + + const char* const effects[] = { "Effect 1", "Effect 2" }; + + mock->connectDummy(); + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 2); + light.setCurrentEffect(1); + + // it shouldn't publish data if value doesn't change + assertTrue(light.setEffect(1)); + assertEqual(mock->getFlushedMessagesNb(), 0); +} + +AHA_TEST(LightTest, publish_effect_debounce_skip) { + prepareTest + + const char* const effects[] = { "Effect 1", "Effect 2" }; + + mock->connectDummy(); + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 2); + light.setCurrentEffect(1); + + assertTrue(light.setEffect(1, true)); + assertSingleMqttMessage(AHATOFSTR(EffectStateTopic), "Effect 2", true) +} + AHA_TEST(LightTest, state_command_on) { prepareTest @@ -1040,6 +1294,74 @@ AHA_TEST(LightTest, rgb_color_command_different_light) { assertRGBColorCallbackNotCalled() } +AHA_TEST(LightTest, effect_command) { + prepareTest + + const char* const effects[] = { "Fire", "Rainbow", "Snowflakes" }; + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 3); + light.onEffectCommand(onEffectCommandReceived); + mock->fakeMessage(AHATOFSTR(EffectCommandTopic), F("Rainbow")); + + assertEffectCallbackCalled(1, &light) +} + +AHA_TEST(LightTest, effect_command_first) { + prepareTest + + const char* const effects[] = { "Fire", "Rainbow", "Snowflakes" }; + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 3); + light.onEffectCommand(onEffectCommandReceived); + mock->fakeMessage(AHATOFSTR(EffectCommandTopic), F("Fire")); + + assertEffectCallbackCalled(0, &light) +} + +AHA_TEST(LightTest, effect_command_last) { + prepareTest + + const char* const effects[] = { "Fire", "Rainbow", "Snowflakes" }; + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 3); + light.onEffectCommand(onEffectCommandReceived); + mock->fakeMessage(AHATOFSTR(EffectCommandTopic), F("Snowflakes")); + + assertEffectCallbackCalled(2, &light) +} + +AHA_TEST(LightTest, effect_command_non_existing) { + prepareTest + + const char* const effects[] = { "Fire", "Rainbow", "Snowflakes" }; + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 3); + light.onEffectCommand(onEffectCommandReceived); + mock->fakeMessage(AHATOFSTR(EffectCommandTopic), F("NON_EXISTING")); + + assertEffectCallbackNotCalled() +} + +AHA_TEST(LightTest, effect_command_different_light) { + prepareTest + + const char* const effects[] = { "Fire", "Rainbow", "Snowflakes" }; + + HALight light(testUniqueId, HALight::EffectsFeature); + light.setEffects(effects, 3); + light.onEffectCommand(onEffectCommandReceived); + mock->fakeMessage( + F("testData/testDevice/uniqueLightDifferent/fx_cmd_t"), + F("Fire") + ); + + assertEffectCallbackNotCalled() +} + void setup() { delay(1000); From dcdc4373311d55c034e3126343622aeb2066d0de Mon Sep 17 00:00:00 2001 From: Kostiantyn Synytsia <20212876+martaisty@users.noreply.github.com> Date: Mon, 29 May 2023 00:31:30 +0300 Subject: [PATCH 2/3] added light effects example --- examples/light/light.ino | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/examples/light/light.ino b/examples/light/light.ino index c0d7c611..4117e920 100644 --- a/examples/light/light.ino +++ b/examples/light/light.ino @@ -11,9 +11,21 @@ HAMqtt mqtt(client, device); // HALight::BrightnessFeature enables support for setting brightness of the light. // HALight::ColorTemperatureFeature enables support for setting color temperature of the light. -// Both features are optional and you can remove them if they're not needed. +// HALight::EffectsFeature enables support for setting effect of the light. +// All features are optional, and you can remove them if they're not needed. // "prettyLight" is unique ID of the light. You should define your own ID. -HALight light("prettyLight", HALight::BrightnessFeature | HALight::ColorTemperatureFeature | HALight::RGBFeature); +HALight light("prettyLight", HALight::BrightnessFeature | + HALight::ColorTemperatureFeature | + HALight::RGBFeature | + HALight::EffectsFeature); + +const char* const lightEffects[] = { + "Fire", + "Rainbow", + "Polar light", + "Rain", + "Smoke" +}; void onStateCommand(bool state, HALight* sender) { Serial.print("State: "); @@ -47,6 +59,15 @@ void onRGBColorCommand(HALight::RGBColor color, HALight* sender) { sender->setRGBColor(color); // report color back to the Home Assistant } +void onEffectCommand(uint8_t index, HALight* sender) { + Serial.print("Effect index: "); + Serial.println(index); + Serial.print("Effect name: "); + Serial.println(lightEffects[index]); + + sender->setEffect(index); // report effect back to the Home Assistant +} + void setup() { Serial.begin(9600); @@ -74,11 +95,15 @@ void setup() { // light.setMinMireds(50); // light.setMaxMireds(200); + // Light effects (mandatory if HALight::EffectsFeature is set, optional otherwise) + light.setEffects(lightEffects, 5); + // handle light states light.onStateCommand(onStateCommand); light.onBrightnessCommand(onBrightnessCommand); // optional light.onColorTemperatureCommand(onColorTemperatureCommand); // optional light.onRGBColorCommand(onRGBColorCommand); // optional + light.onEffectCommand(onEffectCommand); // optional mqtt.begin(BROKER_ADDR); } From 2b08ebe63d65ad8be773289c302aefb31769d518 Mon Sep 17 00:00:00 2001 From: Kostiantyn Synytsia <20212876+martaisty@users.noreply.github.com> Date: Sun, 23 Jul 2023 17:09:40 +0300 Subject: [PATCH 3/3] add documentation for light effects --- docs/documents/api/device-types/ha-light.html | 125 +++++++++++++++++- docs/genindex.html | 28 +++- docs/objects.inv | Bin 33483 -> 33920 bytes docs/searchindex.js | 2 +- 4 files changed, 151 insertions(+), 4 deletions(-) diff --git a/docs/documents/api/device-types/ha-light.html b/docs/documents/api/device-types/ha-light.html index 9369dc5c..a9f4f5de 100644 --- a/docs/documents/api/device-types/ha-light.html +++ b/docs/documents/api/device-types/ha-light.html @@ -154,7 +154,7 @@
HALight allows adding a controllable light in the Home Assistant panel. The library supports only the state, brightness, color temperature and RGB color. If you need more features please open a new GitHub issue.
+HALight allows adding a controllable light in the Home Assistant panel. The library supports only the state, brightness, color temperature, RGB color and effects. If you need more features please open a new GitHub issue.
Note
You can find more information about this entity in the Home Assistant documentation: https://www.home-assistant.io/integrations/light.mqtt/
@@ -185,6 +185,11 @@Sets the list of available effects that will be listed. For example: const char* const lightEffects[] = {"Fire","Rainbow","Snowflales","Rain","Smoke"}; light.setEffects(lightEffects, 5);
Note
+The effects list can be set only once.
+effects – The list of effects i.e. array of strings.
size – The size of the effects list i.e. total number of effects.
Changes the effect of the light and publishes MQTT message. Effect represents the index of the effect that was set using setEffects method. Please note that if a new value is the same as previous one, the MQTT message won’t be published.
+effect – The new effect index of the light.
force – Forces to update the value without comparing it to a previous known value.
Returns true
if the effect is set successfully.
Sets the current effect of the light without pushing the value to Home Assistant. This method may be useful if you want to change the effect before the connection with the MQTT broker is acquired.
+effect – The new effect.
+Returns the last known effect of the light. Effect represents the index of the effect that was set using setEffects method. By default the effect is set to 0
.
Registers callback that will be called each time the effect command from HA is received. Please note that it’s not possible to register multiple callbacks for the same light.
+Note
+In non-optimistic mode, the effect must be reported back to HA using the HALight::setEffect method.
+callback –
+Public Static Attributes
@@ -562,6 +639,20 @@Publishes the MQTT message with the given effect.
+effect – The effect to publish.
+Returns true
if the MQTT message has been published successfully.
Parses the given effect command and executes the callback with proper value.
+cmd – The data of the command.
length – Length of the command.
Private Members
@@ -687,6 +792,18 @@Array of effects for the serializer.
+The current effect (the current effect’s index). By default it’s 0
.
The callback that will be called when the effect is received from the HA.
+hDa5WcnGEzZV$X-&SWX8)gR+UEEf4qK-vmBHW0xZaABvA?9-|>Ql
z%c40VxrY&C%n<`tIvW^MjxXOzWwI$99-O7aAc4iZZYty)6GXi3H
ze0e>U4!?qd&^^HzW-lO_Qwp*d;Teo9gwui)f6l0a#VcxxfWI@4Ix)rxmS?9iGw4lG
zSgRq5g#gw_ZLLayFuZb3O?J#gydO~5v2J#yB4mI{Il_d-2lVi~B`!V1tV(MM1muu!
zbB7gND2)oHLQYX&5l)+fzbqQA*c$;MJHu)?;Nv1pQ$=x^!$MxD>4oVi2=9eaOj(Bq
ze@(GgDcEzP@S!=p9DQLrLE_s>kO@GTWn9jX7+cN^!@Ghxmd=FF)A7
dw(#iw~~A_RQuA6n34xz+E+iYYcOX?fd!PAAe4Uj?Q=Y#^d?DuN{B%
zgX3mkTA@~hFbNjn&J%2asAmJS9T34Nr(9(ror93Y0Vl9J(<%Jv*Fxt)zy@2vbY;W)
z*y|WDT%|=}nM;3~P@W6H?sU-EB{$4bu*OovVhT_#iA_{@Kv>6eFJrnSuuNE?&0vM=
zNTyg4x&s21J%5J@`{IgA+*pLp${t2$CA#bl5WeEnEv#q)EeEloWNoQ4&Isyv$BgpY
zT7n<0!G{F0SwrqpCwGIr
+A7&y^V2c79GDXky{v