Add fork-driven MQTT and entity feature upgrades.

This integrates configurable MQTT reconnect behavior, the new Text entity, default lambda callback support across command-capable entities, and entity_category support for button/sensor/number families with test coverage.
This commit is contained in:
2026-04-16 15:38:26 +10:00
parent 1d333ab229
commit 2ebdcf302b
44 changed files with 2221 additions and 78 deletions
+1
View File
@@ -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"
+8
View File
@@ -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 <Arduino.h>
+9 -1
View File
@@ -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;
}
+19 -2
View File
@@ -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;
+3 -1
View File
@@ -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()) {
+12
View File
@@ -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;
+19 -3
View File
@@ -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<bool>(_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
}
}
+41 -1
View File
@@ -3,6 +3,10 @@
#include "HABaseDeviceType.h"
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
#include <functional>
#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<void(HAButton*)>& 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<void(HAButton*)> _commandStdCallback;
#endif
};
#endif
+32 -4
View File
@@ -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<bool>(_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
+29 -1
View File
@@ -3,6 +3,10 @@
#include "HABaseDeviceType.h"
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
#include <functional>
#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<void(CoverCommand, HACover*)>& 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<void(CoverCommand, HACover*)> _commandStdCallback;
#endif
};
#endif
+37 -4
View File
@@ -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<bool>(_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<bool>(_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
}
}
+50 -2
View File
@@ -4,6 +4,10 @@
#include "HABaseDeviceType.h"
#include "../utils/HANumeric.h"
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
#include <functional>
#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<void(bool, HAFan*)>& 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<void(uint16_t, HAFan*)>& 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<void(bool, HAFan*)> _stateStdCallback;
/// The std::function callback that will be called when the speed command is received from the HA.
std::function<void(uint16_t, HAFan*)> _speedStdCallback;
#endif
};
#endif
+149 -21
View File
@@ -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<bool>(_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<bool>(_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<bool>(_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<bool>(_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<bool>(_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<bool>(_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
}
}
+122 -6
View File
@@ -4,6 +4,10 @@
#include "HABaseDeviceType.h"
#include "../utils/HANumeric.h"
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
#include <functional>
#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<void(bool, HAHVAC*)>& 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<void(bool, HAHVAC*)>& 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<void(FanMode, HAHVAC*)>& 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<void(SwingMode, HAHVAC*)>& 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<void(Mode, HAHVAC*)>& 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<void(HANumeric, HAHVAC*)>& 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<void(bool, HAHVAC*)> _auxStdCallback;
/// The std::function callback that will be called when the power command is received from the HA.
std::function<void(bool, HAHVAC*)> _powerStdCallback;
/// The std::function callback that will be called when the fan mode command is received from the HA.
std::function<void(FanMode, HAHVAC*)> _fanModeStdCallback;
/// The std::function callback that will be called when the swing mode command is received from the HA.
std::function<void(SwingMode, HAHVAC*)> _swingModeStdCallback;
/// The std::function callback that will be called when the mode command is received from the HA.
std::function<void(Mode, HAHVAC*)> _modeStdCallback;
/// The std::function callback that will be called when the target temperature is changed via the HA panel.
std::function<void(HANumeric, HAHVAC*)> _targetTemperatureStdCallback;
#endif
};
#endif
+72 -8
View File
@@ -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<bool>(_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<bool>(_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<bool>(_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<bool>(_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
}
}
+92 -4
View File
@@ -4,6 +4,10 @@
#include "HABaseDeviceType.h"
#include "../utils/HANumeric.h"
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
#include <functional>
#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<void(bool, HALight*)>& 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<void(uint8_t, HALight*)>& 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<void(uint16_t, HALight*)>& 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<void(HALight::RGBColor, HALight*)>& 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<void(bool, HALight*)> _stateStdCallback;
/// The std::function callback that will be called when the brightness command is received from the HA.
std::function<void(uint8_t, HALight*)> _brightnessStdCallback;
/// The std::function callback that will be called when the color temperature command is received from the HA.
std::function<void(uint16_t, HALight*)> _colorTemperatureStdCallback;
/// The std::function callback that will be called when the RGB command is received from the HA.
std::function<void(HALight::RGBColor, HALight*)> _rgbColorStdCallback;
#endif
};
#endif
+32 -4
View File
@@ -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<bool>(_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
+29 -1
View File
@@ -3,6 +3,10 @@
#include "HABaseDeviceType.h"
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
#include <functional>
#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<void(LockCommand, HALock*)>& 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<void(LockCommand, HALock*)> _commandStdCallback;
#endif
};
#endif
+27 -4
View File
@@ -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<bool>(_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
}
}
}
+41 -1
View File
@@ -4,6 +4,10 @@
#include "HABaseDeviceType.h"
#include "../utils/HANumeric.h"
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
#include <functional>
#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<void(HANumeric, HANumber*)>& 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<void(HANumeric, HANumber*)> _commandStdCallback;
#endif
};
#endif
+19 -2
View File
@@ -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<bool>(_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
}
}
+29 -1
View File
@@ -3,6 +3,10 @@
#include "HABaseDeviceType.h"
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
#include <functional>
#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<void(HAScene*)>& 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<void(HAScene*)> _commandStdCallback;
#endif
};
#endif
+19 -2
View File
@@ -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<bool>(_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;
}
}
+29 -1
View File
@@ -3,6 +3,10 @@
#include "HABaseDeviceType.h"
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
#include <functional>
#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<void(int8_t, HASelect*)>& 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<void(int8_t, HASelect*)> _commandStdCallback;
#endif
};
#endif
+3 -1
View File
@@ -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);
+12
View File
@@ -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;
+16 -2
View File
@@ -90,13 +90,27 @@ void HASwitch::onMqttMessage(
{
(void)payload;
if (_commandCallback && HASerializer::compareDataTopics(
const bool hasCommandCallback =
_commandCallback
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
|| static_cast<bool>(_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
}
}
+29 -1
View File
@@ -3,6 +3,10 @@
#include "HABaseDeviceType.h"
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
#include <functional>
#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<void(bool, HASwitch*)>& 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<void(bool, HASwitch*)> _commandStdCallback;
#endif
};
#endif
+167
View File
@@ -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<bool>(_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
+210
View File
@@ -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 <functional>
#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<void(const char*, HAText*)>& 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<void(const char*, HAText*)> _commandStdCallback;
#endif
};
#endif
#endif
+2
View File
@@ -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;
+4
View File
@@ -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;
+4
View File
@@ -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"};
+4
View File
@@ -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[];
@@ -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)
+20
View File
@@ -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
+1
View File
@@ -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
+38
View File
@@ -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);
+21
View File
@@ -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
+20
View File
@@ -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)
+5
View File
@@ -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
@@ -0,0 +1,369 @@
#include <AUnit.h>
#include <ArduinoHA.h>
#include <string.h>
#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<HACover::CoverCommand>(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<HALock::LockCommand>(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);
}
+5
View File
@@ -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
+351
View File
@@ -0,0 +1,351 @@
#include <AUnit.h>
#include <ArduinoHA.h>
#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);
}