diff --git a/src/ArduinoHA.h b/src/ArduinoHA.h index 974a516..5d04537 100644 --- a/src/ArduinoHA.h +++ b/src/ArduinoHA.h @@ -20,6 +20,7 @@ #include "device-types/HASensorNumber.h" #include "device-types/HASwitch.h" #include "device-types/HATagScanner.h" +#include "device-types/HAText.h" #include "utils/HAUtils.h" #include "utils/HANumeric.h" diff --git a/src/ArduinoHADefines.h b/src/ArduinoHADefines.h index a27c741..a021b59 100644 --- a/src/ArduinoHADefines.h +++ b/src/ArduinoHADefines.h @@ -20,6 +20,14 @@ // #define EX_ARDUINOHA_SENSOR // #define EX_ARDUINOHA_SWITCH // #define EX_ARDUINOHA_TAG_SCANNER +// #define EX_ARDUINOHA_TEXT + +// Enables std::function overloads for entity callbacks. +// Useful for capturing lambdas, but may increase binary size on constrained boards. +// Define ARDUINOHA_DISABLE_STDFUNCTION to opt out. +#if !defined(ARDUINOHA_DISABLE_STDFUNCTION) + #define ARDUINOHA_ENABLE_STDFUNCTION +#endif #if defined(ARDUINOHA_DEBUG) #include diff --git a/src/HAMqtt.cpp b/src/HAMqtt.cpp index ed1c15c..7ff1a5a 100644 --- a/src/HAMqtt.cpp +++ b/src/HAMqtt.cpp @@ -20,6 +20,7 @@ _username(nullptr), \ _password(nullptr), \ _lastConnectionAttemptAt(0), \ + _reconnectInterval(DefaultReconnectInterval), \ _devicesTypesNb(0), \ _maxDevicesTypesNb(maxDevicesTypesNb), \ _devicesTypes(new HABaseDeviceType*[maxDevicesTypesNb]), \ @@ -205,6 +206,13 @@ bool HAMqtt::setBufferSize(uint16_t size) return _mqtt->setBufferSize(size); } +void HAMqtt::setReconnectInterval(uint16_t interval) +{ + if (interval > 0) { + _reconnectInterval = interval; + } +} + void HAMqtt::addDeviceType(HABaseDeviceType* deviceType) { if (_devicesTypesNb + 1 > _maxDevicesTypesNb) { @@ -291,7 +299,7 @@ void HAMqtt::processMessage(const char* topic, const uint8_t* payload, uint16_t void HAMqtt::connectToServer() { if (_lastConnectionAttemptAt > 0 && - (millis() - _lastConnectionAttemptAt) < ReconnectInterval) { + (millis() - _lastConnectionAttemptAt) < _reconnectInterval) { return; } diff --git a/src/HAMqtt.h b/src/HAMqtt.h index 915d846..e0b3188 100644 --- a/src/HAMqtt.h +++ b/src/HAMqtt.h @@ -262,6 +262,20 @@ public: */ bool setBufferSize(uint16_t size); + /** + * Sets minimal interval between MQTT reconnect attempts. + * By default it's 10000 milliseconds. + * + * @param interval Interval in milliseconds. Zero is ignored. + */ + void setReconnectInterval(uint16_t interval); + + /** + * Returns minimal interval between MQTT reconnect attempts. + */ + inline uint16_t getReconnectInterval() const + { return _reconnectInterval; } + /** * Adds a new device's type to the MQTT. * Each time the connection with MQTT broker is acquired, the HAMqtt class @@ -378,8 +392,8 @@ public: #endif private: - /// Interval between MQTT reconnects (milliseconds). - static const uint16_t ReconnectInterval = 10000; + /// Default interval between MQTT reconnect attempts (milliseconds). + static const uint16_t DefaultReconnectInterval = 10000; /// Living instance of the HAMqtt class. It can be nullptr. static HAMqtt* _instance; @@ -440,6 +454,9 @@ private: /// Time of the last connection attemps (milliseconds since boot). uint32_t _lastConnectionAttemptAt; + /// Interval between MQTT reconnect attempts (milliseconds). + uint16_t _reconnectInterval; + /// The amount of registered devices types. uint8_t _devicesTypesNb; diff --git a/src/device-types/HABinarySensor.cpp b/src/device-types/HABinarySensor.cpp index 20ab76f..b320cf3 100644 --- a/src/device-types/HABinarySensor.cpp +++ b/src/device-types/HABinarySensor.cpp @@ -7,6 +7,7 @@ HABinarySensor::HABinarySensor(const char* uniqueId) : HABaseDeviceType(AHATOFSTR(HAComponentBinarySensor), uniqueId), _class(nullptr), + _entityCategory(nullptr), _icon(nullptr), _currentState(false) { @@ -42,11 +43,12 @@ void HABinarySensor::buildSerializer() return; } - _serializer = new HASerializer(this, 9); // 9 - max properties nb + _serializer = new HASerializer(this, 10); // 10 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + _serializer->set(AHATOFSTR(HAStateEntityCategory), _entityCategory); _serializer->set(AHATOFSTR(HAIconProperty), _icon); if (_expireAfter.isSet()) { diff --git a/src/device-types/HABinarySensor.h b/src/device-types/HABinarySensor.h index 518e546..7adddf7 100644 --- a/src/device-types/HABinarySensor.h +++ b/src/device-types/HABinarySensor.h @@ -63,6 +63,15 @@ public: inline void setDeviceClass(const char* deviceClass) { _class = deviceClass; } + /** + * Sets the entity category for the sensor. + * See: https://www.home-assistant.io/integrations/binary_sensor.mqtt/#entity_category + * + * @param entityCategory The category name. + */ + inline void setEntityCategory(const char* entityCategory) + { _entityCategory = entityCategory; } + /** * Sets icon of the sensor. * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). @@ -88,6 +97,9 @@ private: /// The device class. It can be nullptr. const char* _class; + /// The entity category for the sensor. It can be nullptr. + const char* _entityCategory; + /// The icon of the sensor. It can be nullptr. const char* _icon; diff --git a/src/device-types/HAButton.cpp b/src/device-types/HAButton.cpp index 4a913c6..d173fb4 100644 --- a/src/device-types/HAButton.cpp +++ b/src/device-types/HAButton.cpp @@ -7,6 +7,7 @@ HAButton::HAButton(const char* uniqueId) : HABaseDeviceType(AHATOFSTR(HAComponentButton), uniqueId), _class(nullptr), + _entityCategory(nullptr), _icon(nullptr), _retain(false), _commandCallback(nullptr) @@ -20,11 +21,12 @@ void HAButton::buildSerializer() return; } - _serializer = new HASerializer(this, 9); // 9 - max properties nb + _serializer = new HASerializer(this, 10); // 10 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + _serializer->set(AHATOFSTR(HAStateEntityCategory), _entityCategory); _serializer->set(AHATOFSTR(HAIconProperty), _icon); // optional property @@ -61,12 +63,26 @@ void HAButton::onMqttMessage( (void)payload; (void)length; - if (_commandCallback && HASerializer::compareDataTopics( + const bool hasCommandCallback = + _commandCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_commandStdCallback) +#endif + ; + + if (hasCommandCallback && HASerializer::compareDataTopics( topic, uniqueId(), AHATOFSTR(HACommandTopic) )) { - _commandCallback(this); + if (_commandCallback) { + _commandCallback(this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_commandStdCallback) { + _commandStdCallback(this); + } +#endif } } diff --git a/src/device-types/HAButton.h b/src/device-types/HAButton.h index 58c88fd..e3366b6 100644 --- a/src/device-types/HAButton.h +++ b/src/device-types/HAButton.h @@ -3,6 +3,10 @@ #include "HABaseDeviceType.h" +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) +#include +#endif + #ifndef EX_ARDUINOHA_BUTTON #define HABUTTON_CALLBACK(name) void (*name)(HAButton* sender) @@ -32,6 +36,15 @@ public: inline void setDeviceClass(const char* deviceClass) { _class = deviceClass; } + /** + * Sets the entity category for the button. + * See: https://www.home-assistant.io/integrations/button.mqtt/#entity_category + * + * @param entityCategory The category name. + */ + inline void setEntityCategory(const char* entityCategory) + { _entityCategory = entityCategory; } + /** * Sets icon of the button. * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). @@ -57,7 +70,26 @@ public: * @param callback */ inline void onCommand(HABUTTON_CALLBACK(callback)) - { _commandCallback = callback; } + { + _commandCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _commandStdCallback = nullptr; +#endif + } + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /** + * Registers callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onCommand(const std::function& callback) + { + _commandCallback = nullptr; + _commandStdCallback = callback; + } +#endif protected: virtual void buildSerializer() override; @@ -72,6 +104,9 @@ private: /// The device class. It can be nullptr. const char* _class; + /// The entity category for the button. It can be nullptr. + const char* _entityCategory; + /// The icon of the button. It can be nullptr. const char* _icon; @@ -80,6 +115,11 @@ private: /// The command callback that will be called once clicking the button in HA panel. HABUTTON_CALLBACK(_commandCallback); + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /// The std::function command callback. + std::function _commandStdCallback; +#endif }; #endif diff --git a/src/device-types/HACover.cpp b/src/device-types/HACover.cpp index 78cd5cd..e09d0fb 100644 --- a/src/device-types/HACover.cpp +++ b/src/device-types/HACover.cpp @@ -16,6 +16,9 @@ HACover::HACover(const char* uniqueId, const Features features) : _retain(false), _optimistic(false), _commandCallback(nullptr) +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + , _commandStdCallback() +#endif { } @@ -168,17 +171,42 @@ bool HACover::publishPosition(int16_t position) void HACover::handleCommand(const uint8_t* cmd, const uint16_t length) { - if (!_commandCallback) { + const bool hasCommandCallback = + _commandCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_commandStdCallback) +#endif + ; + + if (!hasCommandCallback) { return; } + CoverCommand command; + bool handled = false; if (memcmp_P(cmd, HACloseCommand, length) == 0) { - _commandCallback(CommandClose, this); + command = CommandClose; + handled = true; } else if (memcmp_P(cmd, HAOpenCommand, length) == 0) { - _commandCallback(CommandOpen, this); + command = CommandOpen; + handled = true; } else if (memcmp_P(cmd, HAStopCommand, length) == 0) { - _commandCallback(CommandStop, this); + command = CommandStop; + handled = true; } + + if (!handled) { + return; + } + + if (_commandCallback) { + _commandCallback(command, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_commandStdCallback) { + _commandStdCallback(command, this); + } +#endif } #endif diff --git a/src/device-types/HACover.h b/src/device-types/HACover.h index 92a1ce5..8fb0ba0 100644 --- a/src/device-types/HACover.h +++ b/src/device-types/HACover.h @@ -3,6 +3,10 @@ #include "HABaseDeviceType.h" +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) +#include +#endif + #ifndef EX_ARDUINOHA_COVER #define HACOVER_CALLBACK(name) void (*name)(CoverCommand cmd, HACover* sender) @@ -145,7 +149,26 @@ public: * @param callback */ inline void onCommand(HACOVER_CALLBACK(callback)) - { _commandCallback = callback; } + { + _commandCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _commandStdCallback = nullptr; +#endif + } + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /** + * Registers callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onCommand(const std::function& callback) + { + _commandCallback = nullptr; + _commandStdCallback = callback; + } +#endif protected: virtual void buildSerializer() override; @@ -204,6 +227,11 @@ private: /// The command callback that will be called when clicking the cover's button in the HA panel. HACOVER_CALLBACK(_commandCallback); + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /// The std::function callback that will be called when clicking the cover's button in the HA panel. + std::function _commandStdCallback; +#endif }; #endif diff --git a/src/device-types/HAFan.cpp b/src/device-types/HAFan.cpp index ee404a2..114aa74 100644 --- a/src/device-types/HAFan.cpp +++ b/src/device-types/HAFan.cpp @@ -17,6 +17,10 @@ HAFan::HAFan(const char* uniqueId, const uint8_t features) : _currentSpeed(0), _stateCallback(nullptr), _speedCallback(nullptr) +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + , _stateStdCallback(), + _speedStdCallback() +#endif { } @@ -171,23 +175,52 @@ void HAFan::handleStateCommand(const uint8_t* cmd, const uint16_t length) { (void)cmd; - if (!_stateCallback) { + const bool hasStateCallback = + _stateCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_stateStdCallback) +#endif + ; + + if (!hasStateCallback) { return; } bool state = length == strlen_P(HAStateOn); - _stateCallback(state, this); + if (_stateCallback) { + _stateCallback(state, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_stateStdCallback) { + _stateStdCallback(state, this); + } +#endif } void HAFan::handleSpeedCommand(const uint8_t* cmd, const uint16_t length) { - if (!_speedCallback) { + const bool hasSpeedCallback = + _speedCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_speedStdCallback) +#endif + ; + + if (!hasSpeedCallback) { return; } const HANumeric& number = HANumeric::fromStr(cmd, length); if (number.isUInt16()) { - _speedCallback(number.toUInt16(), this); + const uint16_t speed = number.toUInt16(); + if (_speedCallback) { + _speedCallback(speed, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_speedStdCallback) { + _speedStdCallback(speed, this); + } +#endif } } diff --git a/src/device-types/HAFan.h b/src/device-types/HAFan.h index 8ed750e..f62d21d 100644 --- a/src/device-types/HAFan.h +++ b/src/device-types/HAFan.h @@ -4,6 +4,10 @@ #include "HABaseDeviceType.h" #include "../utils/HANumeric.h" +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) +#include +#endif + #ifndef EX_ARDUINOHA_FAN #define HAFAN_STATE_CALLBACK(name) void (*name)(bool state, HAFan* sender) @@ -156,7 +160,12 @@ public: * @note In non-optimistic mode, the state must be reported back to HA using the HAFan::setState method. */ inline void onStateCommand(HAFAN_STATE_CALLBACK(callback)) - { _stateCallback = callback; } + { + _stateCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _stateStdCallback = nullptr; +#endif + } /** * Registers callback that will be called each time the speed command from HA is received. @@ -166,7 +175,38 @@ public: * @note In non-optimistic mode, the speed must be reported back to HA using the HAFan::setSpeed method. */ inline void onSpeedCommand(HAFAN_SPEED_CALLBACK(callback)) - { _speedCallback = callback; } + { + _speedCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _speedStdCallback = nullptr; +#endif + } + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /** + * Registers state callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onStateCommand(const std::function& callback) + { + _stateCallback = nullptr; + _stateStdCallback = callback; + } + + /** + * Registers speed callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onSpeedCommand(const std::function& callback) + { + _speedCallback = nullptr; + _speedStdCallback = callback; + } +#endif protected: virtual void buildSerializer() override; @@ -239,6 +279,14 @@ private: /// The callback that will be called when the speed command is received from the HA. HAFAN_SPEED_CALLBACK(_speedCallback); + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /// The std::function callback that will be called when the state command is received from the HA. + std::function _stateStdCallback; + + /// The std::function callback that will be called when the speed command is received from the HA. + std::function _speedStdCallback; +#endif }; #endif diff --git a/src/device-types/HAHVAC.cpp b/src/device-types/HAHVAC.cpp index 76eb422..c242459 100644 --- a/src/device-types/HAHVAC.cpp +++ b/src/device-types/HAHVAC.cpp @@ -42,6 +42,14 @@ HAHVAC::HAHVAC( _modeCallback(nullptr), _targetTemperature(), _targetTemperatureCallback(nullptr) +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + , _auxStdCallback(), + _powerStdCallback(), + _fanModeStdCallback(), + _swingModeStdCallback(), + _modeStdCallback(), + _targetTemperatureStdCallback() +#endif { if (_features & FanFeature) { _fanModesSerializer = new HASerializerArray(4); @@ -652,87 +660,207 @@ void HAHVAC::handleAuxStateCommand(const uint8_t* cmd, const uint16_t length) { (void)cmd; - if (!_auxCallback) { + const bool hasAuxCallback = + _auxCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_auxStdCallback) +#endif + ; + + if (!hasAuxCallback) { return; } bool state = length == strlen_P(HAStateOn); - _auxCallback(state, this); + if (_auxCallback) { + _auxCallback(state, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_auxStdCallback) { + _auxStdCallback(state, this); + } +#endif } void HAHVAC::handlePowerCommand(const uint8_t* cmd, const uint16_t length) { (void)cmd; - if (!_powerCallback) { + const bool hasPowerCallback = + _powerCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_powerStdCallback) +#endif + ; + + if (!hasPowerCallback) { return; } bool state = length == strlen_P(HAStateOn); - _powerCallback(state, this); + if (_powerCallback) { + _powerCallback(state, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_powerStdCallback) { + _powerStdCallback(state, this); + } +#endif } void HAHVAC::handleFanModeCommand(const uint8_t* cmd, const uint16_t length) { - if (!_fanModeCallback) { + const bool hasFanModeCallback = + _fanModeCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_fanModeStdCallback) +#endif + ; + + if (!hasFanModeCallback) { return; } + FanMode mode = UnknownFanMode; + bool handled = false; if (memcmp_P(cmd, HAFanModeAuto, length) == 0) { - _fanModeCallback(AutoFanMode, this); + mode = AutoFanMode; + handled = true; } else if (memcmp_P(cmd, HAFanModeLow, length) == 0) { - _fanModeCallback(LowFanMode, this); + mode = LowFanMode; + handled = true; } else if (memcmp_P(cmd, HAFanModeMedium, length) == 0) { - _fanModeCallback(MediumFanMode, this); + mode = MediumFanMode; + handled = true; } else if (memcmp_P(cmd, HAFanModeHigh, length) == 0) { - _fanModeCallback(HighFanMode, this); + mode = HighFanMode; + handled = true; } + + if (!handled) { + return; + } + + if (_fanModeCallback) { + _fanModeCallback(mode, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_fanModeStdCallback) { + _fanModeStdCallback(mode, this); + } +#endif } void HAHVAC::handleSwingModeCommand(const uint8_t* cmd, const uint16_t length) { - if (!_swingModeCallback) { + const bool hasSwingModeCallback = + _swingModeCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_swingModeStdCallback) +#endif + ; + + if (!hasSwingModeCallback) { return; } + SwingMode mode = UnknownSwingMode; + bool handled = false; if (memcmp_P(cmd, HASwingModeOn, length) == 0) { - _swingModeCallback(OnSwingMode, this); + mode = OnSwingMode; + handled = true; } else if (memcmp_P(cmd, HASwingModeOff, length) == 0) { - _swingModeCallback(OffSwingMode, this); + mode = OffSwingMode; + handled = true; } + + if (!handled) { + return; + } + + if (_swingModeCallback) { + _swingModeCallback(mode, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_swingModeStdCallback) { + _swingModeStdCallback(mode, this); + } +#endif } void HAHVAC::handleModeCommand(const uint8_t* cmd, const uint16_t length) { - if (!_modeCallback) { + const bool hasModeCallback = + _modeCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_modeStdCallback) +#endif + ; + + if (!hasModeCallback) { return; } + Mode mode = UnknownMode; + bool handled = false; if (memcmp_P(cmd, HAModeAuto, length) == 0) { - _modeCallback(AutoMode, this); + mode = AutoMode; + handled = true; } else if (memcmp_P(cmd, HAModeOff, length) == 0) { - _modeCallback(OffMode, this); + mode = OffMode; + handled = true; } else if (memcmp_P(cmd, HAModeCool, length) == 0) { - _modeCallback(CoolMode, this); + mode = CoolMode; + handled = true; } else if (memcmp_P(cmd, HAModeHeat, length) == 0) { - _modeCallback(HeatMode, this); + mode = HeatMode; + handled = true; } else if (memcmp_P(cmd, HAModeDry, length) == 0) { - _modeCallback(DryMode, this); + mode = DryMode; + handled = true; } else if (memcmp_P(cmd, HAModeFanOnly, length) == 0) { - _modeCallback(FanOnlyMode, this); + mode = FanOnlyMode; + handled = true; } + + if (!handled) { + return; + } + + if (_modeCallback) { + _modeCallback(mode, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_modeStdCallback) { + _modeStdCallback(mode, this); + } +#endif } void HAHVAC::handleTargetTemperatureCommand(const uint8_t* cmd, const uint16_t length) { - if (!_targetTemperatureCallback) { + const bool hasTargetTemperatureCallback = + _targetTemperatureCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_targetTemperatureStdCallback) +#endif + ; + + if (!hasTargetTemperatureCallback) { return; } HANumeric number = HANumeric::fromStr(cmd, length); if (number.isSet()) { number.setPrecision(_precision); - _targetTemperatureCallback(number, this); + if (_targetTemperatureCallback) { + _targetTemperatureCallback(number, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_targetTemperatureStdCallback) { + _targetTemperatureStdCallback(number, this); + } +#endif } } diff --git a/src/device-types/HAHVAC.h b/src/device-types/HAHVAC.h index 1957ac6..b124de1 100644 --- a/src/device-types/HAHVAC.h +++ b/src/device-types/HAHVAC.h @@ -4,6 +4,10 @@ #include "HABaseDeviceType.h" #include "../utils/HANumeric.h" +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) +#include +#endif + #ifndef EX_ARDUINOHA_HVAC #define _SET_CURRENT_TEMPERATURE_OVERLOAD(type) \ @@ -447,7 +451,12 @@ public: * @note The aux state must be reported back to HA using the HAHVAC::setAuxState method. */ inline void onAuxStateCommand(HAHVAC_CALLBACK_BOOL(callback)) - { _auxCallback = callback; } + { + _auxCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _auxStdCallback = nullptr; +#endif + } /** * Registers callback that will be called each time the power command from HA is received. @@ -456,7 +465,12 @@ public: * @param callback */ inline void onPowerCommand(HAHVAC_CALLBACK_BOOL(callback)) - { _powerCallback = callback; } + { + _powerCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _powerStdCallback = nullptr; +#endif + } /** * Registers callback that will be called each time the fan mode command from HA is received. @@ -466,7 +480,12 @@ public: * @note The fan mode must be reported back to HA using the HAHVAC::setFanMode method. */ inline void onFanModeCommand(HAHVAC_CALLBACK_FAN_MODE(callback)) - { _fanModeCallback = callback; } + { + _fanModeCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _fanModeStdCallback = nullptr; +#endif + } /** * Registers callback that will be called each time the swing mode command from HA is received. @@ -476,7 +495,12 @@ public: * @note The swing mode must be reported back to HA using the HAHVAC::setSwingMode method. */ inline void onSwingModeCommand(HAHVAC_CALLBACK_SWING_MODE(callback)) - { _swingModeCallback = callback; } + { + _swingModeCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _swingModeStdCallback = nullptr; +#endif + } /** * Registers callback that will be called each time the HVAC mode command from HA is received. @@ -486,7 +510,12 @@ public: * @note The mode must be reported back to HA using the HAHVAC::setMode method. */ inline void onModeCommand(HAHVAC_CALLBACK_MODE(callback)) - { _modeCallback = callback; } + { + _modeCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _modeStdCallback = nullptr; +#endif + } /** * Registers callback that will be called each time the target temperature is set via HA panel. @@ -496,7 +525,74 @@ public: * @note The target temperature must be reported back to HA using the HAHVAC::setTargetTemperature method. */ inline void onTargetTemperatureCommand(HAHVAC_CALLBACK_TARGET_TEMP(callback)) - { _targetTemperatureCallback = callback; } + { + _targetTemperatureCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _targetTemperatureStdCallback = nullptr; +#endif + } + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /** + * Registers aux state callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + */ + inline void onAuxStateCommand(const std::function& callback) + { + _auxCallback = nullptr; + _auxStdCallback = callback; + } + + /** + * Registers power callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + */ + inline void onPowerCommand(const std::function& callback) + { + _powerCallback = nullptr; + _powerStdCallback = callback; + } + + /** + * Registers fan mode callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + */ + inline void onFanModeCommand(const std::function& callback) + { + _fanModeCallback = nullptr; + _fanModeStdCallback = callback; + } + + /** + * Registers swing mode callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + */ + inline void onSwingModeCommand(const std::function& callback) + { + _swingModeCallback = nullptr; + _swingModeStdCallback = callback; + } + + /** + * Registers HVAC mode callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + */ + inline void onModeCommand(const std::function& callback) + { + _modeCallback = nullptr; + _modeStdCallback = callback; + } + + /** + * Registers target temperature callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + */ + inline void onTargetTemperatureCommand(const std::function& callback) + { + _targetTemperatureCallback = nullptr; + _targetTemperatureStdCallback = callback; + } +#endif protected: virtual void buildSerializer() override; @@ -698,6 +794,26 @@ private: /// Callback that will be called when the target temperature is changed via the HA panel. HAHVAC_CALLBACK_TARGET_TEMP(_targetTemperatureCallback); + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /// The std::function callback that will be called when the aux state command is received from the HA. + std::function _auxStdCallback; + + /// The std::function callback that will be called when the power command is received from the HA. + std::function _powerStdCallback; + + /// The std::function callback that will be called when the fan mode command is received from the HA. + std::function _fanModeStdCallback; + + /// The std::function callback that will be called when the swing mode command is received from the HA. + std::function _swingModeStdCallback; + + /// The std::function callback that will be called when the mode command is received from the HA. + std::function _modeStdCallback; + + /// The std::function callback that will be called when the target temperature is changed via the HA panel. + std::function _targetTemperatureStdCallback; +#endif }; #endif diff --git a/src/device-types/HALight.cpp b/src/device-types/HALight.cpp index 03f6dfe..9a29c8a 100644 --- a/src/device-types/HALight.cpp +++ b/src/device-types/HALight.cpp @@ -62,6 +62,12 @@ HALight::HALight(const char* uniqueId, const uint8_t features) : _brightnessCallback(nullptr), _colorTemperatureCallback(nullptr), _rgbColorCallback(nullptr) +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + , _stateStdCallback(), + _brightnessStdCallback(), + _colorTemperatureStdCallback(), + _rgbColorStdCallback() +#endif { } @@ -321,23 +327,52 @@ void HALight::handleStateCommand(const uint8_t* cmd, const uint16_t length) { (void)cmd; - if (!_stateCallback) { + const bool hasStateCallback = + _stateCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_stateStdCallback) +#endif + ; + + if (!hasStateCallback) { return; } bool state = length == strlen_P(HAStateOn); - _stateCallback(state, this); + if (_stateCallback) { + _stateCallback(state, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_stateStdCallback) { + _stateStdCallback(state, this); + } +#endif } void HALight::handleBrightnessCommand(const uint8_t* cmd, const uint16_t length) { - if (!_brightnessCallback) { + const bool hasBrightnessCallback = + _brightnessCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_brightnessStdCallback) +#endif + ; + + if (!hasBrightnessCallback) { return; } const HANumeric& number = HANumeric::fromStr(cmd, length); if (number.isUInt8()) { - _brightnessCallback(number.toUInt8(), this); + const uint8_t brightness = number.toUInt8(); + if (_brightnessCallback) { + _brightnessCallback(brightness, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_brightnessStdCallback) { + _brightnessStdCallback(brightness, this); + } +#endif } } @@ -346,19 +381,41 @@ void HALight::handleColorTemperatureCommand( const uint16_t length ) { - if (!_colorTemperatureCallback) { + const bool hasColorTemperatureCallback = + _colorTemperatureCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_colorTemperatureStdCallback) +#endif + ; + + if (!hasColorTemperatureCallback) { return; } const HANumeric& number = HANumeric::fromStr(cmd, length); if (number.isUInt16()) { - _colorTemperatureCallback(number.toUInt16(), this); + const uint16_t temperature = number.toUInt16(); + if (_colorTemperatureCallback) { + _colorTemperatureCallback(temperature, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_colorTemperatureStdCallback) { + _colorTemperatureStdCallback(temperature, this); + } +#endif } } void HALight::handleRGBCommand(const uint8_t* cmd, const uint16_t length) { - if (!_rgbColorCallback) { + const bool hasRgbCallback = + _rgbColorCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_rgbColorStdCallback) +#endif + ; + + if (!hasRgbCallback) { return; } @@ -366,7 +423,14 @@ void HALight::handleRGBCommand(const uint8_t* cmd, const uint16_t length) color.fromBuffer(cmd, length); if (color.isSet) { - _rgbColorCallback(color, this); + if (_rgbColorCallback) { + _rgbColorCallback(color, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_rgbColorStdCallback) { + _rgbColorStdCallback(color, this); + } +#endif } } diff --git a/src/device-types/HALight.h b/src/device-types/HALight.h index 021d32f..c97cc47 100644 --- a/src/device-types/HALight.h +++ b/src/device-types/HALight.h @@ -4,6 +4,10 @@ #include "HABaseDeviceType.h" #include "../utils/HANumeric.h" +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) +#include +#endif + #ifndef EX_ARDUINOHA_LIGHT #define HALIGHT_STATE_CALLBACK(name) void (*name)(bool state, HALight* sender) @@ -265,7 +269,12 @@ public: * @note In non-optimistic mode, the state must be reported back to HA using the HALight::setState method. */ inline void onStateCommand(HALIGHT_STATE_CALLBACK(callback)) - { _stateCallback = callback; } + { + _stateCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _stateStdCallback = nullptr; +#endif + } /** * Registers callback that will be called each time the brightness command from HA is received. @@ -275,7 +284,12 @@ public: * @note In non-optimistic mode, the brightness must be reported back to HA using the HALight::setBrightness method. */ inline void onBrightnessCommand(HALIGHT_BRIGHTNESS_CALLBACK(callback)) - { _brightnessCallback = callback; } + { + _brightnessCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _brightnessStdCallback = nullptr; +#endif + } /** * Registers callback that will be called each time the color temperature command from HA is received. @@ -285,7 +299,12 @@ public: * @note In non-optimistic mode, the color temperature must be reported back to HA using the HALight::setColorTemperature method. */ inline void onColorTemperatureCommand(HALIGHT_COLOR_TEMP_CALLBACK(callback)) - { _colorTemperatureCallback = callback; } + { + _colorTemperatureCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _colorTemperatureStdCallback = nullptr; +#endif + } /** * Registers callback that will be called each time the RGB color command from HA is received. @@ -295,7 +314,62 @@ public: * @note In non-optimistic mode, the color must be reported back to HA using the HALight::setRGBColor method. */ inline void onRGBColorCommand(HALIGHT_RGB_COLOR_CALLBACK(callback)) - { _rgbColorCallback = callback; } + { + _rgbColorCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _rgbColorStdCallback = nullptr; +#endif + } + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /** + * Registers state callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onStateCommand(const std::function& callback) + { + _stateCallback = nullptr; + _stateStdCallback = callback; + } + + /** + * Registers brightness callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onBrightnessCommand(const std::function& callback) + { + _brightnessCallback = nullptr; + _brightnessStdCallback = callback; + } + + /** + * Registers color temperature callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onColorTemperatureCommand(const std::function& callback) + { + _colorTemperatureCallback = nullptr; + _colorTemperatureStdCallback = callback; + } + + /** + * Registers RGB color callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onRGBColorCommand(const std::function& callback) + { + _rgbColorCallback = nullptr; + _rgbColorStdCallback = callback; + } +#endif protected: virtual void buildSerializer() override; @@ -415,6 +489,20 @@ private: /// The callback that will be called when the RGB command is received from the HA. HALIGHT_RGB_COLOR_CALLBACK(_rgbColorCallback); + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /// The std::function callback that will be called when the state command is received from the HA. + std::function _stateStdCallback; + + /// The std::function callback that will be called when the brightness command is received from the HA. + std::function _brightnessStdCallback; + + /// The std::function callback that will be called when the color temperature command is received from the HA. + std::function _colorTemperatureStdCallback; + + /// The std::function callback that will be called when the RGB command is received from the HA. + std::function _rgbColorStdCallback; +#endif }; #endif diff --git a/src/device-types/HALock.cpp b/src/device-types/HALock.cpp index d107458..8dbe510 100644 --- a/src/device-types/HALock.cpp +++ b/src/device-types/HALock.cpp @@ -11,6 +11,9 @@ HALock::HALock(const char* uniqueId) : _optimistic(false), _currentState(StateUnknown), _commandCallback(nullptr) +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + , _commandStdCallback() +#endif { } @@ -109,17 +112,42 @@ bool HALock::publishState(const LockState state) void HALock::handleCommand(const uint8_t* cmd, const uint16_t length) { - if (!_commandCallback) { + const bool hasCommandCallback = + _commandCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_commandStdCallback) +#endif + ; + + if (!hasCommandCallback) { return; } + LockCommand command; + bool handled = false; if (memcmp_P(cmd, HALockCommand, length) == 0) { - _commandCallback(CommandLock, this); + command = CommandLock; + handled = true; } else if (memcmp_P(cmd, HAUnlockCommand, length) == 0) { - _commandCallback(CommandUnlock, this); + command = CommandUnlock; + handled = true; } else if (memcmp_P(cmd, HAOpenCommand, length) == 0) { - _commandCallback(CommandOpen, this); + command = CommandOpen; + handled = true; } + + if (!handled) { + return; + } + + if (_commandCallback) { + _commandCallback(command, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_commandStdCallback) { + _commandStdCallback(command, this); + } +#endif } #endif diff --git a/src/device-types/HALock.h b/src/device-types/HALock.h index d8d036f..4ad47cf 100644 --- a/src/device-types/HALock.h +++ b/src/device-types/HALock.h @@ -3,6 +3,10 @@ #include "HABaseDeviceType.h" +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) +#include +#endif + #ifndef EX_ARDUINOHA_LOCK #define HALOCK_CALLBACK(name) void (*name)(LockCommand command, HALock* sender) @@ -100,7 +104,26 @@ public: * @param callback */ inline void onCommand(HALOCK_CALLBACK(callback)) - { _commandCallback = callback; } + { + _commandCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _commandStdCallback = nullptr; +#endif + } + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /** + * Registers callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onCommand(const std::function& callback) + { + _commandCallback = nullptr; + _commandStdCallback = callback; + } +#endif protected: virtual void buildSerializer() override; @@ -142,6 +165,11 @@ private: /// The callback that will be called when lock/unlock/open command is received from the HA. HALOCK_CALLBACK(_commandCallback); + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /// The std::function callback that will be called when lock/unlock/open command is received from the HA. + std::function _commandStdCallback; +#endif }; #endif diff --git a/src/device-types/HANumber.cpp b/src/device-types/HANumber.cpp index 2c41345..abab620 100644 --- a/src/device-types/HANumber.cpp +++ b/src/device-types/HANumber.cpp @@ -8,6 +8,7 @@ HANumber::HANumber(const char* uniqueId, const NumberPrecision precision) : HABaseDeviceType(AHATOFSTR(HAComponentNumber), uniqueId), _precision(precision), _class(nullptr), + _entityCategory(nullptr), _icon(nullptr), _retain(false), _optimistic(false), @@ -42,11 +43,12 @@ void HANumber::buildSerializer() return; } - _serializer = new HASerializer(this, 16); // 16 - max properties nb + _serializer = new HASerializer(this, 17); // 17 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + _serializer->set(AHATOFSTR(HAStateEntityCategory), _entityCategory); _serializer->set(AHATOFSTR(HAIconProperty), _icon); _serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), _unitOfMeasurement); _serializer->set( @@ -165,17 +167,38 @@ bool HANumber::publishState(const HANumeric& state) void HANumber::handleCommand(const uint8_t* cmd, const uint16_t length) { - if (!_commandCallback) { + const bool hasCommandCallback = + _commandCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_commandStdCallback) +#endif + ; + + if (!hasCommandCallback) { return; } if (memcmp_P(cmd, HAStateNone, length) == 0) { - _commandCallback(HANumeric(), this); + if (_commandCallback) { + _commandCallback(HANumeric(), this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_commandStdCallback) { + _commandStdCallback(HANumeric(), this); + } +#endif } else { HANumeric number = HANumeric::fromStr(cmd, length); if (number.isSet()) { number.setPrecision(_precision); - _commandCallback(number, this); + if (_commandCallback) { + _commandCallback(number, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_commandStdCallback) { + _commandStdCallback(number, this); + } +#endif } } } diff --git a/src/device-types/HANumber.h b/src/device-types/HANumber.h index e20d6fe..6bd26bc 100644 --- a/src/device-types/HANumber.h +++ b/src/device-types/HANumber.h @@ -4,6 +4,10 @@ #include "HABaseDeviceType.h" #include "../utils/HANumeric.h" +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) +#include +#endif + #ifndef EX_ARDUINOHA_NUMBER #define _SET_STATE_OVERLOAD(type) \ @@ -106,6 +110,15 @@ public: inline void setDeviceClass(const char* deviceClass) { _class = deviceClass; } + /** + * Sets the entity category for the number. + * See: https://www.home-assistant.io/integrations/number.mqtt/#entity_category + * + * @param entityCategory The category name. + */ + inline void setEntityCategory(const char* entityCategory) + { _entityCategory = entityCategory; } + /** * Sets icon of the number. * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). @@ -184,7 +197,26 @@ public: * @note In non-optimistic mode, the number must be reported back to HA using the HANumber::setState method. */ inline void onCommand(HANUMBER_CALLBACK(callback)) - { _commandCallback = callback; } + { + _commandCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _commandStdCallback = nullptr; +#endif + } + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /** + * Registers callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onCommand(const std::function& callback) + { + _commandCallback = nullptr; + _commandStdCallback = callback; + } +#endif protected: virtual void buildSerializer() override; @@ -228,6 +260,9 @@ private: /// The device class. It can be nullptr. const char* _class; + /// The entity category for the number. It can be nullptr. + const char* _entityCategory; + /// The icon of the number. It can be nullptr. const char* _icon; @@ -257,6 +292,11 @@ private: /// The callback that will be called when the command is received from the HA. HANUMBER_CALLBACK(_commandCallback); + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /// The std::function callback that will be called when the command is received from the HA. + std::function _commandStdCallback; +#endif }; #endif diff --git a/src/device-types/HAScene.cpp b/src/device-types/HAScene.cpp index c579c68..d431220 100644 --- a/src/device-types/HAScene.cpp +++ b/src/device-types/HAScene.cpp @@ -9,6 +9,9 @@ HAScene::HAScene(const char* uniqueId) : _icon(nullptr), _retain(false), _commandCallback(nullptr) +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + , _commandStdCallback() +#endif { } @@ -65,12 +68,26 @@ void HAScene::onMqttMessage( (void)payload; (void)length; - if (_commandCallback && HASerializer::compareDataTopics( + const bool hasCommandCallback = + _commandCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_commandStdCallback) +#endif + ; + + if (hasCommandCallback && HASerializer::compareDataTopics( topic, uniqueId(), AHATOFSTR(HACommandTopic) )) { - _commandCallback(this); + if (_commandCallback) { + _commandCallback(this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_commandStdCallback) { + _commandStdCallback(this); + } +#endif } } diff --git a/src/device-types/HAScene.h b/src/device-types/HAScene.h index b5105a1..6d51986 100644 --- a/src/device-types/HAScene.h +++ b/src/device-types/HAScene.h @@ -3,6 +3,10 @@ #include "HABaseDeviceType.h" +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) +#include +#endif + #ifndef EX_ARDUINOHA_SCENE #define HASCENE_CALLBACK(name) void (*name)(HAScene* sender) @@ -47,7 +51,26 @@ public: * @param callback */ inline void onCommand(HASCENE_CALLBACK(callback)) - { _commandCallback = callback; } + { + _commandCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _commandStdCallback = nullptr; +#endif + } + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /** + * Registers callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onCommand(const std::function& callback) + { + _commandCallback = nullptr; + _commandStdCallback = callback; + } +#endif protected: virtual void buildSerializer() override; @@ -67,6 +90,11 @@ private: /// The command callback that will be called when scene is activated from the HA panel. HASCENE_CALLBACK(_commandCallback); + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /// The std::function callback that will be called when scene is activated from the HA panel. + std::function _commandStdCallback; +#endif }; #endif diff --git a/src/device-types/HASelect.cpp b/src/device-types/HASelect.cpp index b4bff01..3f26e17 100644 --- a/src/device-types/HASelect.cpp +++ b/src/device-types/HASelect.cpp @@ -12,6 +12,9 @@ HASelect::HASelect(const char* uniqueId) : _retain(false), _optimistic(false), _commandCallback(nullptr) +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + , _commandStdCallback() +#endif { } @@ -151,7 +154,14 @@ void HASelect::onMqttMessage( const uint16_t length ) { - if (_commandCallback && HASerializer::compareDataTopics( + const bool hasCommandCallback = + _commandCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_commandStdCallback) +#endif + ; + + if (hasCommandCallback && HASerializer::compareDataTopics( topic, uniqueId(), AHATOFSTR(HACommandTopic) @@ -161,7 +171,14 @@ void HASelect::onMqttMessage( for (uint8_t i = 0; i < optionsNb; i++) { if (memcmp(payload, options[i], length) == 0) { - _commandCallback(i, this); + if (_commandCallback) { + _commandCallback(i, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_commandStdCallback) { + _commandStdCallback(i, this); + } +#endif return; } } diff --git a/src/device-types/HASelect.h b/src/device-types/HASelect.h index a6c0f99..63f885c 100644 --- a/src/device-types/HASelect.h +++ b/src/device-types/HASelect.h @@ -3,6 +3,10 @@ #include "HABaseDeviceType.h" +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) +#include +#endif + #ifndef EX_ARDUINOHA_SELECT class HASerializerArray; @@ -109,7 +113,26 @@ public: * @note In non-optimistic mode, the selected option must be reported back to HA using the HASelect::setState method. */ inline void onCommand(HASELECT_CALLBACK(callback)) - { _commandCallback = callback; } + { + _commandCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _commandStdCallback = nullptr; +#endif + } + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /** + * Registers callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onCommand(const std::function& callback) + { + _commandCallback = nullptr; + _commandStdCallback = callback; + } +#endif #ifdef ARDUINOHA_TEST inline HASerializerArray* getOptions() const @@ -156,6 +179,11 @@ private: /// The command callback that will be called when option is changed via the HA panel. HASELECT_CALLBACK(_commandCallback); + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /// The std::function callback that will be called when option is changed via the HA panel. + std::function _commandStdCallback; +#endif }; #endif diff --git a/src/device-types/HASensor.cpp b/src/device-types/HASensor.cpp index 0d2efe9..7f16fc5 100644 --- a/src/device-types/HASensor.cpp +++ b/src/device-types/HASensor.cpp @@ -9,6 +9,7 @@ HASensor::HASensor(const char* uniqueId, const uint16_t features) : _features(features), _deviceClass(nullptr), _stateClass(nullptr), + _entityCategory(nullptr), _forceUpdate(false), _icon(nullptr), _unitOfMeasurement(nullptr), @@ -46,12 +47,13 @@ void HASensor::buildSerializer() return; } - _serializer = new HASerializer(this, 13); // 13 - max properties nb + _serializer = new HASerializer(this, 14); // 14 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _deviceClass); _serializer->set(AHATOFSTR(HAStateClassProperty), _stateClass); + _serializer->set(AHATOFSTR(HAStateEntityCategory), _entityCategory); _serializer->set(AHATOFSTR(HAIconProperty), _icon); _serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), _unitOfMeasurement); diff --git a/src/device-types/HASensor.h b/src/device-types/HASensor.h index 6dbd281..39414a6 100644 --- a/src/device-types/HASensor.h +++ b/src/device-types/HASensor.h @@ -73,6 +73,15 @@ public: inline void setStateClass(const char* stateClass) { _stateClass = stateClass; } + /** + * Sets the entity category for the sensor. + * See: https://www.home-assistant.io/integrations/sensor.mqtt/#entity_category + * + * @param entityCategory The category name. + */ + inline void setEntityCategory(const char* entityCategory) + { _entityCategory = entityCategory; } + /** * Forces HA panel to process each incoming value (MQTT message). * It's useful if you want to have meaningful value graphs in history. @@ -113,6 +122,9 @@ private: /// The state class for the long term stats. It can be nullptr. See: https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics const char* _stateClass; + /// The entity category for the sensor. It can be nullptr. + const char* _entityCategory; + /// The force update flag for the HA panel. bool _forceUpdate; diff --git a/src/device-types/HASwitch.cpp b/src/device-types/HASwitch.cpp index 85da4f3..2ba796f 100644 --- a/src/device-types/HASwitch.cpp +++ b/src/device-types/HASwitch.cpp @@ -90,13 +90,27 @@ void HASwitch::onMqttMessage( { (void)payload; - if (_commandCallback && HASerializer::compareDataTopics( + const bool hasCommandCallback = + _commandCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_commandStdCallback) +#endif + ; + + if (hasCommandCallback && HASerializer::compareDataTopics( topic, uniqueId(), AHATOFSTR(HACommandTopic) )) { bool state = length == strlen_P(HAStateOn); - _commandCallback(state, this); + if (_commandCallback) { + _commandCallback(state, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_commandStdCallback) { + _commandStdCallback(state, this); + } +#endif } } diff --git a/src/device-types/HASwitch.h b/src/device-types/HASwitch.h index 7179b28..0067fac 100644 --- a/src/device-types/HASwitch.h +++ b/src/device-types/HASwitch.h @@ -3,6 +3,10 @@ #include "HABaseDeviceType.h" +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) +#include +#endif + #ifndef EX_ARDUINOHA_SWITCH #define HASWITCH_CALLBACK(name) void (*name)(bool state, HASwitch* sender) @@ -107,7 +111,26 @@ public: * @note In non-optimistic mode, the state must be reported back to HA using the HASwitch::setState method. */ inline void onCommand(HASWITCH_CALLBACK(callback)) - { _commandCallback = callback; } + { + _commandCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _commandStdCallback = nullptr; +#endif + } + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /** + * Registers callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onCommand(const std::function& callback) + { + _commandCallback = nullptr; + _commandStdCallback = callback; + } +#endif protected: virtual void buildSerializer() override; @@ -144,6 +167,11 @@ private: /// The callback that will be called when switch command is received from the HA. HASWITCH_CALLBACK(_commandCallback); + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /// The std::function callback that will be called when switch command is received from the HA. + std::function _commandStdCallback; +#endif }; #endif diff --git a/src/device-types/HAText.cpp b/src/device-types/HAText.cpp new file mode 100644 index 0000000..d5d14ee --- /dev/null +++ b/src/device-types/HAText.cpp @@ -0,0 +1,167 @@ +#include "HAText.h" +#ifndef EX_ARDUINOHA_TEXT + +#include "../HAMqtt.h" +#include "../utils/HASerializer.h" + +HAText::HAText(const char* uniqueId) : + HABaseDeviceType(AHATOFSTR(HAComponentText), uniqueId), + _icon(nullptr), + _retain(false), + _optimistic(false), + _mode(ModeText), + _min(), + _max(), + _pattern(nullptr), + _currentState(nullptr), + _commandCallback(nullptr) +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + , _commandStdCallback() +#endif +{ + +} + +bool HAText::setState(const char* state, const bool force) +{ + if (!state) { + return false; + } + + if ( + !force && + _currentState && + strcmp(state, _currentState) == 0 + ) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +void HAText::buildSerializer() +{ + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 14); // 14 - max properties nb + _serializer->set(AHATOFSTR(HANameProperty), _name); + _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAIconProperty), _icon); + _serializer->set( + AHATOFSTR(HAModeProperty), + getModeProperty(), + HASerializer::ProgmemPropertyValue + ); + _serializer->set(AHATOFSTR(HAPatternProperty), _pattern); + + if (_min.isSet()) { + _serializer->set( + AHATOFSTR(HAMinProperty), + &_min, + HASerializer::NumberPropertyType + ); + } + + if (_max.isSet()) { + _serializer->set( + AHATOFSTR(HAMaxProperty), + &_max, + HASerializer::NumberPropertyType + ); + } + + if (_retain) { + _serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + _serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + _serializer->set(HASerializer::WithDevice); + _serializer->set(HASerializer::WithAvailability); + _serializer->topic(AHATOFSTR(HAStateTopic)); + _serializer->topic(AHATOFSTR(HACommandTopic)); +} + +void HAText::onMqttConnected() +{ + if (!uniqueId()) { + return; + } + + publishConfig(); + publishAvailability(); + + if (!_retain && _currentState) { + publishState(_currentState); + } + + subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); +} + +void HAText::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length +) +{ + const bool hasCommandCallback = + _commandCallback +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + || static_cast(_commandStdCallback) +#endif + ; + + if (hasCommandCallback && HASerializer::compareDataTopics( + topic, + uniqueId(), + AHATOFSTR(HACommandTopic) + )) { + char value[length + 1]; + value[length] = 0; + memcpy(value, payload, length); + if (_commandCallback) { + _commandCallback(value, this); + } +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + if (_commandStdCallback) { + _commandStdCallback(value, this); + } +#endif + } +} + +bool HAText::publishState(const char* state) +{ + return publishOnDataTopic( + AHATOFSTR(HAStateTopic), + state, + true + ); +} + +const __FlashStringHelper* HAText::getModeProperty() const +{ + return _mode == ModePassword + ? AHATOFSTR(HAModePassword) + : nullptr; +} + +#endif diff --git a/src/device-types/HAText.h b/src/device-types/HAText.h new file mode 100644 index 0000000..9643408 --- /dev/null +++ b/src/device-types/HAText.h @@ -0,0 +1,210 @@ +#ifndef AHA_HATEXT_H +#define AHA_HATEXT_H + +#include "HABaseDeviceType.h" +#include "../utils/HANumeric.h" + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) +#include +#endif + +#ifndef EX_ARDUINOHA_TEXT + +#define HATEXT_CALLBACK(name) void (*name)(const char* value, HAText* sender) + +/** + * HAText adds a text input in the Home Assistant panel. + * + * @note + * You can find more information about this entity in the Home Assistant documentation: + * https://www.home-assistant.io/integrations/text.mqtt/ + */ +class HAText : public HABaseDeviceType +{ +public: + /// Represents mode of the text input. + enum Mode { + ModeText = 0, + ModePassword + }; + + /** + * @param uniqueId The unique ID of the text entity. It needs to be unique in a scope of your device. + */ + HAText(const char* uniqueId); + + /** + * Changes state of the text and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state New state of the text. + * @param force Forces to update state without comparing it to previous known state. + * @returns Returns `true` if MQTT message has been published successfully. + */ + bool setState(const char* state, const bool force = false); + + /** + * Sets current state of the text without publishing it to Home Assistant. + * This method may be useful if you want to change state before connection + * with MQTT broker is acquired. + * + * @param state New state of the text. + */ + inline void setCurrentState(const char* state) + { _currentState = state; } + + /** + * Returns last known state of the text. + */ + inline const char* getCurrentState() const + { return _currentState; } + + /** + * Sets icon of the text. + * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). + * + * @param icon The icon name. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * Sets retain flag for the text command. + * If set to `true` the command produced by Home Assistant will be retained. + * + * @param retain + */ + inline void setRetain(const bool retain) + { _retain = retain; } + + /** + * Sets optimistic flag for the text state. + * In this mode the text state doesn't need to be reported back to the HA panel when a command is received. + * By default the optimistic mode is disabled. + * + * @param optimistic The optimistic mode (`true` - enabled, `false` - disabled). + */ + inline void setOptimistic(const bool optimistic) + { _optimistic = optimistic; } + + /** + * Sets mode of the text input. + * By default it's `HAText::ModeText`. + * + * @param mode Mode to set. + */ + inline void setMode(const Mode mode) + { _mode = mode; } + + /** + * Sets minimum accepted length of the command payload. + * + * @param min Minimum number of characters. + */ + inline void setMin(const uint16_t min) + { _min = HANumeric(min, PrecisionP0); } + + /** + * Sets maximum accepted length of the command payload. + * + * @param max Maximum number of characters. + */ + inline void setMax(const uint16_t max) + { _max = HANumeric(max, PrecisionP0); } + + /** + * Sets regex pattern for command payload validation in Home Assistant. + * + * @param pattern Regex pattern. + */ + inline void setPattern(const char* pattern) + { _pattern = pattern; } + + /** + * Registers callback that will be called each time text command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same text entity. + * + * @param callback + * @note In non-optimistic mode, the state should be reported back to HA using HAText::setState. + */ + inline void onCommand(HATEXT_CALLBACK(callback)) + { + _commandCallback = callback; +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + _commandStdCallback = nullptr; +#endif + } + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /** + * Registers callback using std::function. + * It allows passing capturing lambdas and std::bind expressions. + * + * @param callback + */ + inline void onCommand(const std::function& callback) + { + _commandCallback = nullptr; + _commandStdCallback = callback; + } +#endif + +protected: + virtual void buildSerializer() override; + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t length + ) override; + +private: + /** + * Publishes the MQTT message with the given state. + * + * @param state The state to publish. + * @returns Returns `true` if the MQTT message has been published successfully. + */ + bool publishState(const char* state); + + /** + * Returns progmem string representing mode of the text. + */ + const __FlashStringHelper* getModeProperty() const; + + /// The icon of the text. It can be nullptr. + const char* _icon; + + /// The retain flag for the HA commands. + bool _retain; + + /// The optimistic mode of the text (`true` - enabled, `false` - disabled). + bool _optimistic; + + /// Controls how the text should be displayed in the UI. + Mode _mode; + + /// The minimal number of characters accepted by Home Assistant. + HANumeric _min; + + /// The maximum number of characters accepted by Home Assistant. + HANumeric _max; + + /// Regular expression pattern accepted by Home Assistant. It can be nullptr. + const char* _pattern; + + /// The current state of the text. It can be nullptr if state wasn't set. + const char* _currentState; + + /// The callback that will be called when command is received from the HA. + HATEXT_CALLBACK(_commandCallback); + +#if defined(ARDUINOHA_ENABLE_STDFUNCTION) + /// The std::function callback that will be called when command is received from the HA. + std::function _commandStdCallback; +#endif +}; + +#endif +#endif diff --git a/src/mocks/PubSubClientMock.cpp b/src/mocks/PubSubClientMock.cpp index 4562eea..cfca05f 100644 --- a/src/mocks/PubSubClientMock.cpp +++ b/src/mocks/PubSubClientMock.cpp @@ -9,6 +9,7 @@ PubSubClientMock::PubSubClientMock() : _keepAlive(15), _bufferSize(256), _state(-1), + _connectCallsNb(0), _flushedMessagesNb(0), _subscriptions(nullptr), _subscriptionsNb(0), @@ -56,6 +57,7 @@ bool PubSubClientMock::connect( (void)willQos; (void)cleanSession; + _connectCallsNb++; _connection.connected = true; _connection.id = id; _connection.user = user; diff --git a/src/mocks/PubSubClientMock.h b/src/mocks/PubSubClientMock.h index 0781ff1..0593e12 100644 --- a/src/mocks/PubSubClientMock.h +++ b/src/mocks/PubSubClientMock.h @@ -145,6 +145,9 @@ public: inline int16_t state() const { return _state; } + inline uint16_t getConnectCallsNb() const + { return _connectCallsNb; } + inline uint8_t getFlushedMessagesNb() const { return _flushedMessagesNb; } @@ -175,6 +178,7 @@ private: uint16_t _keepAlive; uint16_t _bufferSize; int16_t _state; + uint16_t _connectCallsNb; uint8_t _flushedMessagesNb; MqttSubscription** _subscriptions; uint8_t _subscriptionsNb; diff --git a/src/utils/HADictionary.cpp b/src/utils/HADictionary.cpp index 266d21d..5e13814 100644 --- a/src/utils/HADictionary.cpp +++ b/src/utils/HADictionary.cpp @@ -14,6 +14,7 @@ const char HAComponentSelect[] PROGMEM = {"select"}; const char HAComponentSensor[] PROGMEM = {"sensor"}; const char HAComponentSwitch[] PROGMEM = {"switch"}; const char HAComponentTag[] PROGMEM = {"tag"}; +const char HAComponentText[] PROGMEM = {"text"}; const char HAComponentScene[] PROGMEM = {"scene"}; const char HAComponentFan[] PROGMEM = {"fan"}; const char HAComponentLight[] PROGMEM = {"light"}; @@ -42,6 +43,7 @@ const char HAUniqueIdProperty[] PROGMEM = {"uniq_id"}; const char HAObjectIdProperty[] PROGMEM = {"obj_id"}; const char HADeviceProperty[] PROGMEM = {"dev"}; const char HADeviceClassProperty[] PROGMEM = {"dev_cla"}; +const char HAStateEntityCategory[] PROGMEM = {"ent_cat"}; const char HAStateClassProperty[] PROGMEM = {"stat_cla"}; const char HAIconProperty[] PROGMEM = {"ic"}; const char HARetainProperty[] PROGMEM = {"ret"}; @@ -59,6 +61,7 @@ const char HAMinProperty[] PROGMEM = {"min"}; const char HAMaxProperty[] PROGMEM = {"max"}; const char HAStepProperty[] PROGMEM = {"step"}; const char HAModeProperty[] PROGMEM = {"mode"}; +const char HAPatternProperty[] PROGMEM = {"pattern"}; const char HACommandTemplateProperty[] PROGMEM = {"cmd_tpl"}; const char HASpeedRangeMaxProperty[] PROGMEM = {"spd_rng_max"}; const char HASpeedRangeMinProperty[] PROGMEM = {"spd_rng_min"}; @@ -121,6 +124,7 @@ const char HANotHome[] PROGMEM = {"not_home"}; const char HATrigger[] PROGMEM = {"trigger"}; const char HAModeBox[] PROGMEM = {"box"}; const char HAModeSlider[] PROGMEM = {"slider"}; +const char HAModePassword[] PROGMEM = {"password"}; // covers const char HAClosedState[] PROGMEM = {"closed"}; diff --git a/src/utils/HADictionary.h b/src/utils/HADictionary.h index 609f38d..23e1b91 100644 --- a/src/utils/HADictionary.h +++ b/src/utils/HADictionary.h @@ -14,6 +14,7 @@ extern const char HAComponentSelect[]; extern const char HAComponentSensor[]; extern const char HAComponentSwitch[]; extern const char HAComponentTag[]; +extern const char HAComponentText[]; extern const char HAComponentScene[]; extern const char HAComponentFan[]; extern const char HAComponentLight[]; @@ -42,6 +43,7 @@ extern const char HAUniqueIdProperty[]; extern const char HAObjectIdProperty[]; extern const char HADeviceProperty[]; extern const char HADeviceClassProperty[]; +extern const char HAStateEntityCategory[]; extern const char HAStateClassProperty[]; extern const char HAIconProperty[]; extern const char HARetainProperty[]; @@ -59,6 +61,7 @@ extern const char HAMinProperty[]; extern const char HAMaxProperty[]; extern const char HAStepProperty[]; extern const char HAModeProperty[]; +extern const char HAPatternProperty[]; extern const char HACommandTemplateProperty[]; extern const char HASpeedRangeMaxProperty[]; extern const char HASpeedRangeMinProperty[]; @@ -121,6 +124,7 @@ extern const char HANotHome[]; extern const char HATrigger[]; extern const char HAModeBox[]; extern const char HAModeSlider[]; +extern const char HAModePassword[]; // covers extern const char HAClosedState[]; diff --git a/tests/BinarySensorTest/BinarySensorTest.ino b/tests/BinarySensorTest/BinarySensorTest.ino index f6a4823..7a6357b 100644 --- a/tests/BinarySensorTest/BinarySensorTest.ino +++ b/tests/BinarySensorTest/BinarySensorTest.ino @@ -140,6 +140,26 @@ AHA_TEST(BinarySensorTest, device_class) { ) } +AHA_TEST(BinarySensorTest, entity_category_setter) { + initMqttTest(testDeviceId) + + HABinarySensor sensor(testUniqueId); + sensor.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + sensor, + ( + "{" + "\"uniq_id\":\"uniqueSensor\"," + "\"ent_cat\":\"diagnostic\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueSensor/stat_t\"" + "}" + ) + ) +} + AHA_TEST(BinarySensorTest, icon_setter) { initMqttTest(testDeviceId) diff --git a/tests/ButtonTest/ButtonTest.ino b/tests/ButtonTest/ButtonTest.ino index 4f49a07..80c3879 100644 --- a/tests/ButtonTest/ButtonTest.ino +++ b/tests/ButtonTest/ButtonTest.ino @@ -169,6 +169,26 @@ AHA_TEST(ButtonTest, device_class) { ) } +AHA_TEST(ButtonTest, entity_category_setter) { + prepareTest + + HAButton button(testUniqueId); + button.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + button, + ( + "{" + "\"uniq_id\":\"uniqueButton\"," + "\"ent_cat\":\"diagnostic\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"cmd_t\":\"testData/testDevice/uniqueButton/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(ButtonTest, icon_setter) { prepareTest diff --git a/tests/Makefile b/tests/Makefile index ae6a663..5103877 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -2,6 +2,7 @@ tests: set -e; \ for i in *Test/Makefile; do \ echo '==== Making:' $$(dirname $$i); \ + $(MAKE) -C $$(dirname $$i) clean; \ $(MAKE) -C $$(dirname $$i) -j; \ done diff --git a/tests/MqttTest/MqttTest.ino b/tests/MqttTest/MqttTest.ino index b38dad8..4f1b309 100644 --- a/tests/MqttTest/MqttTest.ino +++ b/tests/MqttTest/MqttTest.ino @@ -29,6 +29,44 @@ AHA_TEST(MqttTest, maximum_number_of_device_types) { assertEqual(&deviceType, mqtt.getDevicesTypes()[0]); } +AHA_TEST(MqttTest, reconnect_interval_default_value) { + HADevice device(testDeviceId); + HAMqtt mqtt(nullptr, device); + + assertEqual((uint16_t)10000, mqtt.getReconnectInterval()); +} + +AHA_TEST(MqttTest, reconnect_interval_setter) { + HADevice device(testDeviceId); + HAMqtt mqtt(nullptr, device); + + mqtt.setReconnectInterval(5000); + assertEqual((uint16_t)5000, mqtt.getReconnectInterval()); + + mqtt.setReconnectInterval(0); // ignored + assertEqual((uint16_t)5000, mqtt.getReconnectInterval()); +} + +AHA_TEST(MqttTest, reconnect_interval_throttles_attempts) { + PubSubClientMock* mock = new PubSubClientMock(); + HADevice device(testDeviceId); + HAMqtt mqtt(mock, device); + mqtt.begin("testHost", "testUser", "testPass"); + mqtt.setReconnectInterval(50); + + assertEqual((uint16_t)0, mock->getConnectCallsNb()); + mqtt.loop(); + assertEqual((uint16_t)1, mock->getConnectCallsNb()); + + mock->disconnect(); + mqtt.loop(); + assertEqual((uint16_t)1, mock->getConnectCallsNb()); + + delay(60); + mqtt.loop(); + assertEqual((uint16_t)2, mock->getConnectCallsNb()); +} + void setup() { delay(1000); diff --git a/tests/NumberTest/NumberTest.ino b/tests/NumberTest/NumberTest.ino index b516faf..108afeb 100644 --- a/tests/NumberTest/NumberTest.ino +++ b/tests/NumberTest/NumberTest.ino @@ -377,6 +377,27 @@ AHA_TEST(NumberTest, device_class) { ) } +AHA_TEST(NumberTest, entity_category_setter) { + prepareTest + + HANumber number(testUniqueId); + number.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + number, + ( + "{" + "\"uniq_id\":\"uniqueNumber\"," + "\"ent_cat\":\"diagnostic\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueNumber/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueNumber/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(NumberTest, icon_setter) { prepareTest diff --git a/tests/SensorTest/SensorTest.ino b/tests/SensorTest/SensorTest.ino index b4592ab..910187e 100644 --- a/tests/SensorTest/SensorTest.ino +++ b/tests/SensorTest/SensorTest.ino @@ -150,6 +150,26 @@ AHA_TEST(SensorTest, state_class_setter) { ) } +AHA_TEST(SensorTest, entity_category_setter) { + initMqttTest(testDeviceId) + + HASensor sensor(testUniqueId); + sensor.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + sensor, + ( + "{" + "\"uniq_id\":\"uniqueSensor\"," + "\"ent_cat\":\"diagnostic\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueSensor/stat_t\"" + "}" + ) + ) +} + AHA_TEST(SensorTest, force_update_setter) { initMqttTest(testDeviceId) diff --git a/tests/StdFunctionCallbacksTest/Makefile b/tests/StdFunctionCallbacksTest/Makefile new file mode 100644 index 0000000..5ec7675 --- /dev/null +++ b/tests/StdFunctionCallbacksTest/Makefile @@ -0,0 +1,5 @@ +APP_NAME := StdFunctionCallbacksTest +ARDUINO_LIBS := AUnit arduino-home-assistant +EXTRA_CPPFLAGS := -D ARDUINOHA_TEST +EXTRA_CXXFLAGS := -g +include ../../../EpoxyDuino/EpoxyDuino.mk diff --git a/tests/StdFunctionCallbacksTest/StdFunctionCallbacksTest.ino b/tests/StdFunctionCallbacksTest/StdFunctionCallbacksTest.ino new file mode 100644 index 0000000..5e35c95 --- /dev/null +++ b/tests/StdFunctionCallbacksTest/StdFunctionCallbacksTest.ino @@ -0,0 +1,369 @@ +#include +#include +#include + +#define prepareTest \ + initMqttTest(testDeviceId) + +using aunit::TestRunner; + +static const char* testDeviceId = "testDevice"; + +AHA_TEST(StdFunctionCallbacksTest, button_command_callback) { + prepareTest + + HAButton button("button"); + bool called = false; + HAButton* caller = nullptr; + + button.onCommand([&](HAButton* sender) { + called = true; + caller = sender; + }); + + mock->fakeMessage(F("testData/testDevice/button/cmd_t"), F("PRESS")); + + assertTrue(called); + assertEqual(&button, caller); +} + +AHA_TEST(StdFunctionCallbacksTest, switch_command_callback) { + prepareTest + + HASwitch testSwitch("switch"); + bool called = false; + bool state = false; + HASwitch* caller = nullptr; + + testSwitch.onCommand([&](bool cmdState, HASwitch* sender) { + called = true; + state = cmdState; + caller = sender; + }); + + mock->fakeMessage(F("testData/testDevice/switch/cmd_t"), F("ON")); + + assertTrue(called); + assertTrue(state); + assertEqual(&testSwitch, caller); +} + +AHA_TEST(StdFunctionCallbacksTest, number_command_callback) { + prepareTest + + HANumber number("number"); + bool called = false; + HANumeric value; + HANumber* caller = nullptr; + + number.onCommand([&](HANumeric cmdValue, HANumber* sender) { + called = true; + value = cmdValue; + caller = sender; + }); + + mock->fakeMessage(F("testData/testDevice/number/cmd_t"), F("1234")); + + assertTrue(called); + assertTrue(HANumeric(1234, 0) == value); + assertEqual(&number, caller); +} + +AHA_TEST(StdFunctionCallbacksTest, text_command_callback) { + prepareTest + + HAText text("text"); + bool called = false; + bool valueMatched = false; + HAText* caller = nullptr; + + text.onCommand([&](const char* value, HAText* sender) { + called = true; + valueMatched = strcmp(value, "hello") == 0; + caller = sender; + }); + + mock->fakeMessage(F("testData/testDevice/text/cmd_t"), F("hello")); + + assertTrue(called); + assertTrue(valueMatched); + assertEqual(&text, caller); +} + +AHA_TEST(StdFunctionCallbacksTest, select_command_callback) { + prepareTest + + HASelect select("select"); + select.setOptions("Option A;Option B"); + bool called = false; + int8_t selectedIndex = -1; + HASelect* caller = nullptr; + + select.onCommand([&](int8_t index, HASelect* sender) { + called = true; + selectedIndex = index; + caller = sender; + }); + + mock->fakeMessage(F("testData/testDevice/select/cmd_t"), F("Option B")); + + assertTrue(called); + assertEqual((int8_t)1, selectedIndex); + assertEqual(&select, caller); +} + +AHA_TEST(StdFunctionCallbacksTest, scene_command_callback) { + prepareTest + + HAScene scene("scene"); + bool called = false; + HAScene* caller = nullptr; + + scene.onCommand([&](HAScene* sender) { + called = true; + caller = sender; + }); + + mock->fakeMessage(F("testData/testDevice/scene/cmd_t"), F("ON")); + + assertTrue(called); + assertEqual(&scene, caller); +} + +AHA_TEST(StdFunctionCallbacksTest, cover_command_callback) { + prepareTest + + HACover cover("cover"); + bool called = false; + HACover::CoverCommand command = static_cast(0); + HACover* caller = nullptr; + + cover.onCommand([&](HACover::CoverCommand cmd, HACover* sender) { + called = true; + command = cmd; + caller = sender; + }); + + mock->fakeMessage(F("testData/testDevice/cover/cmd_t"), F("OPEN")); + + assertTrue(called); + assertEqual(HACover::CommandOpen, command); + assertEqual(&cover, caller); +} + +AHA_TEST(StdFunctionCallbacksTest, lock_command_callback) { + prepareTest + + HALock lock("lock"); + bool called = false; + HALock::LockCommand command = static_cast(0); + HALock* caller = nullptr; + + lock.onCommand([&](HALock::LockCommand cmd, HALock* sender) { + called = true; + command = cmd; + caller = sender; + }); + + mock->fakeMessage(F("testData/testDevice/lock/cmd_t"), F("UNLOCK")); + + assertTrue(called); + assertEqual(HALock::CommandUnlock, command); + assertEqual(&lock, caller); +} + +AHA_TEST(StdFunctionCallbacksTest, fan_command_callbacks) { + prepareTest + + HAFan fan("fan", HAFan::SpeedsFeature); + bool stateCalled = false; + bool state = false; + HAFan* stateCaller = nullptr; + bool speedCalled = false; + uint16_t speed = 0; + HAFan* speedCaller = nullptr; + + fan.onStateCommand([&](bool cmdState, HAFan* sender) { + stateCalled = true; + state = cmdState; + stateCaller = sender; + }); + fan.onSpeedCommand([&](uint16_t cmdSpeed, HAFan* sender) { + speedCalled = true; + speed = cmdSpeed; + speedCaller = sender; + }); + + mock->fakeMessage(F("testData/testDevice/fan/cmd_t"), F("ON")); + mock->fakeMessage(F("testData/testDevice/fan/pct_cmd_t"), F("50")); + + assertTrue(stateCalled); + assertTrue(state); + assertEqual(&fan, stateCaller); + assertTrue(speedCalled); + assertEqual((uint16_t)50, speed); + assertEqual(&fan, speedCaller); +} + +AHA_TEST(StdFunctionCallbacksTest, light_command_callbacks) { + prepareTest + + HALight light( + "light", + HALight::BrightnessFeature | HALight::ColorTemperatureFeature | HALight::RGBFeature + ); + bool stateCalled = false; + bool state = false; + HALight* stateCaller = nullptr; + bool brightnessCalled = false; + uint8_t brightness = 0; + HALight* brightnessCaller = nullptr; + bool colorTempCalled = false; + uint16_t colorTemperature = 0; + HALight* colorTempCaller = nullptr; + bool rgbCalled = false; + HALight::RGBColor rgbColor; + HALight* rgbCaller = nullptr; + + light.onStateCommand([&](bool cmdState, HALight* sender) { + stateCalled = true; + state = cmdState; + stateCaller = sender; + }); + light.onBrightnessCommand([&](uint8_t cmdBrightness, HALight* sender) { + brightnessCalled = true; + brightness = cmdBrightness; + brightnessCaller = sender; + }); + light.onColorTemperatureCommand([&](uint16_t cmdTemperature, HALight* sender) { + colorTempCalled = true; + colorTemperature = cmdTemperature; + colorTempCaller = sender; + }); + light.onRGBColorCommand([&](HALight::RGBColor cmdColor, HALight* sender) { + rgbCalled = true; + rgbColor = cmdColor; + rgbCaller = sender; + }); + + mock->fakeMessage(F("testData/testDevice/light/cmd_t"), F("ON")); + mock->fakeMessage(F("testData/testDevice/light/bri_cmd_t"), F("42")); + mock->fakeMessage(F("testData/testDevice/light/clr_temp_cmd_t"), F("250")); + mock->fakeMessage(F("testData/testDevice/light/rgb_cmd_t"), F("1,2,3")); + + assertTrue(stateCalled); + assertTrue(state); + assertEqual(&light, stateCaller); + assertTrue(brightnessCalled); + assertEqual((uint8_t)42, brightness); + assertEqual(&light, brightnessCaller); + assertTrue(colorTempCalled); + assertEqual((uint16_t)250, colorTemperature); + assertEqual(&light, colorTempCaller); + assertTrue(rgbCalled); + assertTrue(rgbColor.isSet); + assertTrue(rgbColor == HALight::RGBColor(1, 2, 3)); + assertEqual(&light, rgbCaller); +} + +AHA_TEST(StdFunctionCallbacksTest, hvac_command_callbacks) { + prepareTest + + HAHVAC hvac( + "hvac", + HAHVAC::AuxHeatingFeature | + HAHVAC::PowerFeature | + HAHVAC::FanFeature | + HAHVAC::SwingFeature | + HAHVAC::ModesFeature | + HAHVAC::TargetTemperatureFeature + ); + bool auxCalled = false; + bool auxState = false; + HAHVAC* auxCaller = nullptr; + bool powerCalled = false; + bool powerState = true; + HAHVAC* powerCaller = nullptr; + bool fanModeCalled = false; + HAHVAC::FanMode fanMode = HAHVAC::UnknownFanMode; + HAHVAC* fanModeCaller = nullptr; + bool swingModeCalled = false; + HAHVAC::SwingMode swingMode = HAHVAC::UnknownSwingMode; + HAHVAC* swingModeCaller = nullptr; + bool modeCalled = false; + HAHVAC::Mode mode = HAHVAC::UnknownMode; + HAHVAC* modeCaller = nullptr; + bool targetTempCalled = false; + HANumeric targetTemp; + HAHVAC* targetTempCaller = nullptr; + + hvac.onAuxStateCommand([&](bool state, HAHVAC* sender) { + auxCalled = true; + auxState = state; + auxCaller = sender; + }); + hvac.onPowerCommand([&](bool state, HAHVAC* sender) { + powerCalled = true; + powerState = state; + powerCaller = sender; + }); + hvac.onFanModeCommand([&](HAHVAC::FanMode cmdMode, HAHVAC* sender) { + fanModeCalled = true; + fanMode = cmdMode; + fanModeCaller = sender; + }); + hvac.onSwingModeCommand([&](HAHVAC::SwingMode cmdMode, HAHVAC* sender) { + swingModeCalled = true; + swingMode = cmdMode; + swingModeCaller = sender; + }); + hvac.onModeCommand([&](HAHVAC::Mode cmdMode, HAHVAC* sender) { + modeCalled = true; + mode = cmdMode; + modeCaller = sender; + }); + hvac.onTargetTemperatureCommand([&](HANumeric cmdTemperature, HAHVAC* sender) { + targetTempCalled = true; + targetTemp = cmdTemperature; + targetTempCaller = sender; + }); + + mock->fakeMessage(F("testData/testDevice/hvac/aux_cmd_t"), F("ON")); + mock->fakeMessage(F("testData/testDevice/hvac/pow_cmd_t"), F("OFF")); + mock->fakeMessage(F("testData/testDevice/hvac/fan_mode_cmd_t"), F("high")); + mock->fakeMessage(F("testData/testDevice/hvac/swing_mode_cmd_t"), F("on")); + mock->fakeMessage(F("testData/testDevice/hvac/mode_cmd_t"), F("heat")); + mock->fakeMessage(F("testData/testDevice/hvac/temp_cmd_t"), F("215")); + + assertTrue(auxCalled); + assertTrue(auxState); + assertEqual(&hvac, auxCaller); + assertTrue(powerCalled); + assertFalse(powerState); + assertEqual(&hvac, powerCaller); + assertTrue(fanModeCalled); + assertEqual(HAHVAC::HighFanMode, fanMode); + assertEqual(&hvac, fanModeCaller); + assertTrue(swingModeCalled); + assertEqual(HAHVAC::OnSwingMode, swingMode); + assertEqual(&hvac, swingModeCaller); + assertTrue(modeCalled); + assertEqual(HAHVAC::HeatMode, mode); + assertEqual(&hvac, modeCaller); + assertTrue(targetTempCalled); + assertTrue(HANumeric(21.5f, 1) == targetTemp); + assertEqual(&hvac, targetTempCaller); +} + +void setup() +{ + delay(1000); + Serial.begin(115200); + while (!Serial); +} + +void loop() +{ + TestRunner::run(); + delay(1); +} diff --git a/tests/TextTest/Makefile b/tests/TextTest/Makefile new file mode 100644 index 0000000..828f723 --- /dev/null +++ b/tests/TextTest/Makefile @@ -0,0 +1,5 @@ +APP_NAME := TextTest +ARDUINO_LIBS := AUnit arduino-home-assistant +EXTRA_CPPFLAGS := "-D ARDUINOHA_TEST" +EXTRA_CXXFLAGS := -g +include ../../../EpoxyDuino/EpoxyDuino.mk diff --git a/tests/TextTest/TextTest.ino b/tests/TextTest/TextTest.ino new file mode 100644 index 0000000..382e66f --- /dev/null +++ b/tests/TextTest/TextTest.ino @@ -0,0 +1,351 @@ +#include +#include + +#define prepareTest \ + initMqttTest(testDeviceId) \ + lastCommandCallbackCall.reset(); + +#define assertCommandCallbackCalled(expectedValue, callerPtr) \ + assertTrue(lastCommandCallbackCall.called); \ + assertEqual(expectedValue, lastCommandCallbackCall.value); \ + assertEqual(callerPtr, lastCommandCallbackCall.caller); + +#define assertCommandCallbackNotCalled() \ + assertFalse(lastCommandCallbackCall.called); + +using aunit::TestRunner; + +struct CommandCallback { + bool called = false; + char value[64] = {0}; + HAText* caller = nullptr; + + void reset() { + called = false; + value[0] = 0; + caller = nullptr; + } +}; + +static const char* testDeviceId = "testDevice"; +static const char* testUniqueId = "uniqueText"; +static CommandCallback lastCommandCallbackCall; + +const char ConfigTopic[] PROGMEM = {"homeassistant/text/testDevice/uniqueText/config"}; +const char StateTopic[] PROGMEM = {"testData/testDevice/uniqueText/stat_t"}; +const char CommandTopic[] PROGMEM = {"testData/testDevice/uniqueText/cmd_t"}; + +void onCommandReceived(const char* value, HAText* caller) +{ + lastCommandCallbackCall.called = true; + strncpy( + lastCommandCallbackCall.value, + value, + sizeof(lastCommandCallbackCall.value) - 1 + ); + lastCommandCallbackCall.value[sizeof(lastCommandCallbackCall.value) - 1] = 0; + lastCommandCallbackCall.caller = caller; +} + +AHA_TEST(TextTest, invalid_unique_id) { + prepareTest + + HAText text(nullptr); + text.buildSerializerTest(); + HASerializer* serializer = text.getSerializer(); + + assertTrue(serializer == nullptr); +} + +AHA_TEST(TextTest, default_params) { + prepareTest + + HAText text(testUniqueId); + assertEntityConfig( + mock, + text, + ( + "{" + "\"uniq_id\":\"uniqueText\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueText/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueText/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(TextTest, extended_unique_id) { + prepareTest + + device.enableExtendedUniqueIds(); + HAText text(testUniqueId); + assertEntityConfig( + mock, + text, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueText\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueText/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueText/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(TextTest, command_subscription) { + prepareTest + + HAText text(testUniqueId); + mqtt.loop(); + + assertEqual(1, mock->getSubscriptionsNb()); + assertEqual(AHATOFSTR(CommandTopic), mock->getSubscriptions()[0]->topic); +} + +AHA_TEST(TextTest, availability) { + prepareTest + + HAText text(testUniqueId); + text.setAvailability(true); + mqtt.loop(); + + // availability is published after config in HAText + assertMqttMessage( + 1, + F("testData/testDevice/uniqueText/avty_t"), + "online", + true + ) +} + +AHA_TEST(TextTest, publish_last_known_state) { + prepareTest + + HAText text(testUniqueId); + text.setCurrentState("initial"); + mqtt.loop(); + + assertEqual(2, mock->getFlushedMessagesNb()); + assertMqttMessage(1, AHATOFSTR(StateTopic), "initial", true) +} + +AHA_TEST(TextTest, publish_nothing_if_retained) { + prepareTest + + HAText text(testUniqueId); + text.setRetain(true); + text.setCurrentState("initial"); + mqtt.loop(); + + assertEqual(1, mock->getFlushedMessagesNb()); // only config should be pushed +} + +AHA_TEST(TextTest, name_setter) { + prepareTest + + HAText text(testUniqueId); + text.setName("testName"); + + assertEntityConfig( + mock, + text, + ( + "{" + "\"name\":\"testName\"," + "\"uniq_id\":\"uniqueText\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueText/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueText/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(TextTest, object_id_setter) { + prepareTest + + HAText text(testUniqueId); + text.setObjectId("testId"); + + assertEntityConfig( + mock, + text, + ( + "{" + "\"obj_id\":\"testId\"," + "\"uniq_id\":\"uniqueText\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueText/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueText/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(TextTest, icon_setter) { + prepareTest + + HAText text(testUniqueId); + text.setIcon("testIcon"); + + assertEntityConfig( + mock, + text, + ( + "{" + "\"uniq_id\":\"uniqueText\"," + "\"ic\":\"testIcon\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueText/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueText/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(TextTest, retain_setter) { + prepareTest + + HAText text(testUniqueId); + text.setRetain(true); + + assertEntityConfig( + mock, + text, + ( + "{" + "\"uniq_id\":\"uniqueText\"," + "\"ret\":true," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueText/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueText/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(TextTest, optimistic_setter) { + prepareTest + + HAText text(testUniqueId); + text.setOptimistic(true); + + assertEntityConfig( + mock, + text, + ( + "{" + "\"uniq_id\":\"uniqueText\"," + "\"opt\":true," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueText/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueText/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(TextTest, mode_setter_password) { + prepareTest + + HAText text(testUniqueId); + text.setMode(HAText::ModePassword); + + assertEntityConfig( + mock, + text, + ( + "{" + "\"uniq_id\":\"uniqueText\"," + "\"mode\":\"password\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueText/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueText/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(TextTest, min_max_pattern_setters) { + prepareTest + + HAText text(testUniqueId); + text.setMin(2); + text.setMax(40); + text.setPattern("^[a-zA-Z0-9]+$"); + + assertEntityConfig( + mock, + text, + ( + "{" + "\"uniq_id\":\"uniqueText\"," + "\"pattern\":\"^[a-zA-Z0-9]+$\"," + "\"min\":2," + "\"max\":40," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueText/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueText/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(TextTest, publish_state) { + prepareTest + + mock->connectDummy(); + HAText text(testUniqueId); + + assertTrue(text.setState("new-value")); + assertSingleMqttMessage(AHATOFSTR(StateTopic), "new-value", true) +} + +AHA_TEST(TextTest, publish_state_debounce) { + prepareTest + + mock->connectDummy(); + HAText text(testUniqueId); + text.setCurrentState("new-value"); + + assertTrue(text.setState("new-value")); + assertEqual(0, mock->getFlushedMessagesNb()); +} + +AHA_TEST(TextTest, command_callback) { + prepareTest + + HAText text(testUniqueId); + text.onCommand(onCommandReceived); + mock->fakeMessage(AHATOFSTR(CommandTopic), F("hello")); + + assertCommandCallbackCalled("hello", &text) +} + +AHA_TEST(TextTest, different_text_command) { + prepareTest + + HAText text(testUniqueId); + text.onCommand(onCommandReceived); + mock->fakeMessage( + F("testData/testDevice/uniqueTextDifferent/cmd_t"), + F("hello") + ); + + assertCommandCallbackNotCalled() +} + +void setup() +{ + delay(1000); + Serial.begin(115200); + while (!Serial); +} + +void loop() +{ + TestRunner::run(); + delay(1); +}