This commit is contained in:
Stephan Mühl
2023-03-22 12:15:18 +01:00
committed by GitHub
parent 3e12414a87
commit adb5102869
203 changed files with 35010 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
#ifndef AHA_ARDUINOHA_H
#define AHA_ARDUINOHA_H
#include "HADevice.h"
#include "HAMqtt.h"
#include "device-types/HABinarySensor.h"
#include "device-types/HAButton.h"
//#include "device-types/HACamera.h"
//#include "device-types/HACover.h"
//#include "device-types/HADeviceTracker.h"
//#include "device-types/HADeviceTrigger.h"
//#include "device-types/HAFan.h"
//#include "device-types/HAHVAC.h"
#include "device-types/HALight.h"
//#include "device-types/HALock.h"
#include "device-types/HANumber.h"
//#include "device-types/HAScene.h"
#include "device-types/HASelect.h"
#include "device-types/HASensor.h"
#include "device-types/HASensorNumber.h"
#include "device-types/HASwitch.h"
//#include "device-types/HATagScanner.h"
#include "utils/HAUtils.h"
#include "utils/HANumeric.h"
#ifdef ARDUINOHA_TEST
#include "mocks/AUnitHelpers.h"
#include "mocks/PubSubClientMock.h"
#include "utils/HADictionary.h"
#include "utils/HASerializer.h"
#endif
#endif

View File

@@ -0,0 +1,35 @@
// Turns on debug information of the ArduinoHA core.
// Please note that you need to initialize serial interface manually
// by calling Serial.begin([baudRate]) before initializing ArduinoHA.
//#define ARDUINOHA_DEBUG
// These macros allow to exclude some parts of the library to save more resources.
#define EX_ARDUINOHA_BINARY_SENSOR
//#define EX_ARDUINOHA_BUTTON
#define EX_ARDUINOHA_CAMERA
#define EX_ARDUINOHA_COVER
#define EX_ARDUINOHA_DEVICE_TRACKER
#define EX_ARDUINOHA_DEVICE_TRIGGER
#define EX_ARDUINOHA_FAN
#define EX_ARDUINOHA_HVAC
//#define EX_ARDUINOHA_LIGHT
#define EX_ARDUINOHA_LOCK
#define EX_ARDUINOHA_NUMBER
#define EX_ARDUINOHA_SCENE
//#define EX_ARDUINOHA_SELECT
// #define EX_ARDUINOHA_SENSOR
// #define EX_ARDUINOHA_SWITCH
#define EX_ARDUINOHA_TAG_SCANNER
#if defined(ARDUINOHA_DEBUG)
#include <Arduino.h>
#define ARDUINOHA_DEBUG_PRINTLN(x) Serial.println(x);
#define ARDUINOHA_DEBUG_PRINT(x) Serial.print(x);
#else
#define ARDUINOHA_DEBUG_INIT()
#define ARDUINOHA_DEBUG_PRINTLN(x)
#define ARDUINOHA_DEBUG_PRINT(x)
#endif
#define AHATOFSTR(x) reinterpret_cast<const __FlashStringHelper*>(x)
#define AHAFROMFSTR(x) reinterpret_cast<const char*>(x)

View File

@@ -0,0 +1,146 @@
#include "ArduinoHADefines.h"
#include "HADevice.h"
#include "HAMqtt.h"
#include "utils/HAUtils.h"
#include "utils/HASerializer.h"
#define HADEVICE_INIT \
_ownsUniqueId(false), \
_serializer(new HASerializer(nullptr, 5)), \
_availabilityTopic(nullptr), \
_sharedAvailability(false), \
_available(true) // device will be available by default
HADevice::HADevice() :
_uniqueId(nullptr),
HADEVICE_INIT
{
}
HADevice::HADevice(const char* uniqueId) :
_uniqueId(uniqueId),
HADEVICE_INIT
{
_serializer->set(AHATOFSTR(HADeviceIdentifiersProperty), _uniqueId);
}
HADevice::HADevice(const byte* uniqueId, const uint16_t length) :
_uniqueId(HAUtils::byteArrayToStr(uniqueId, length)),
HADEVICE_INIT
{
_ownsUniqueId = true;
_serializer->set(AHATOFSTR(HADeviceIdentifiersProperty), _uniqueId);
}
HADevice::~HADevice()
{
delete _serializer;
if (_availabilityTopic) {
delete _availabilityTopic;
}
if (_ownsUniqueId) {
delete[] _uniqueId;
}
}
bool HADevice::setUniqueId(const byte* uniqueId, const uint16_t length)
{
if (_uniqueId) {
return false; // unique ID cannot be changed at runtime once it's set
}
_uniqueId = HAUtils::byteArrayToStr(uniqueId, length);
_ownsUniqueId = true;
_serializer->set(AHATOFSTR(HADeviceIdentifiersProperty), _uniqueId);
return true;
}
void HADevice::setManufacturer(const char* manufacturer)
{
_serializer->set(AHATOFSTR(HADeviceManufacturerProperty), manufacturer);
}
void HADevice::setModel(const char* model)
{
_serializer->set(AHATOFSTR(HADeviceModelProperty), model);
}
void HADevice::setName(const char* name)
{
_serializer->set(AHATOFSTR(HANameProperty), name);
}
void HADevice::setSoftwareVersion(const char* softwareVersion)
{
_serializer->set(
AHATOFSTR(HADeviceSoftwareVersionProperty),
softwareVersion
);
}
void HADevice::setAvailability(bool online)
{
_available = online;
publishAvailability();
}
bool HADevice::enableSharedAvailability()
{
if (_sharedAvailability) {
return true; // already enabled
}
const uint16_t topicLength = HASerializer::calculateDataTopicLength(
nullptr,
AHATOFSTR(HAAvailabilityTopic)
);
if (topicLength == 0) {
return false;
}
_availabilityTopic = new char[topicLength];
if (HASerializer::generateDataTopic(
_availabilityTopic,
nullptr,
AHATOFSTR(HAAvailabilityTopic)
) > 0) {
_sharedAvailability = true;
return true;
}
return false;
}
void HADevice::enableLastWill()
{
HAMqtt* mqtt = HAMqtt::instance();
if (!mqtt || !_availabilityTopic) {
return;
}
mqtt->setLastWill(
_availabilityTopic,
"offline",
true
);
}
void HADevice::publishAvailability() const
{
HAMqtt* mqtt = HAMqtt::instance();
if (!_availabilityTopic || !mqtt) {
return;
}
const char* payload = _available ? HAOnline : HAOffline;
const uint16_t length = strlen_P(payload);
if (mqtt->beginPublish(_availabilityTopic, length, true)) {
mqtt->writePayload(AHATOFSTR(payload));
mqtt->endPublish();
}
}

View File

@@ -0,0 +1,160 @@
#ifndef AHA_HADEVICE_H
#define AHA_HADEVICE_H
#include <Arduino.h>
class HASerializer;
/**
* This class represents your device that's going to be registered in the Home Assistant devices registry.
* Each entity (HABinarySensor, HASensor, etc.) that you use will be owned by this device.
*/
class HADevice
{
public:
/**
* Constructs HADevice without the unique ID.
*
* @note You will need to set the ID using HADevice::setUniqueId method. Otherwise none of the entities will work.
*/
HADevice();
/**
* Constructs HADevice with the given unique ID (string).
* Keep the unique ID short to save the memory.
*
* @param uniqueId String with the null terminator.
*/
HADevice(const char* uniqueId);
/**
* Constructs HADevice using the given byte array as the unique ID.
* It works in the same way as HADevice::setUniqueId method.
*
* @param uniqueId Bytes array that's going to be converted into the string.
* @param length Number of bytes in the array.
*/
HADevice(const byte* uniqueId, const uint16_t length);
/**
* Deletes HASerializer and the availability topic if the shared availability was enabled.
*/
~HADevice();
/**
* Returns pointer to the unique ID. It can be nullptr if the device has no ID assigned.
*/
inline const char* getUniqueId() const
{ return _uniqueId; }
/**
* Returns the instance of the HASerializer used by the device.
* This method is used by all entities to serialize device's representation.
*/
inline const HASerializer* getSerializer() const
{ return _serializer; }
/**
* Returns true if the shared availability is enabled for the device.
*/
inline bool isSharedAvailabilityEnabled() const
{ return _sharedAvailability; }
/**
* Returns availability topic generated by the HADevice::enableSharedAvailability method.
* It can be nullptr if the shared availability is not enabled.
*/
inline const char* getAvailabilityTopic() const
{ return _availabilityTopic; }
/**
* Returns online/offline state of the device.
*/
inline bool isAvailable() const
{ return _available; }
/**
* Sets unique ID of the device based on the given byte array.
* Each byte is converted into a hex string representation, so the final length of the unique ID will be twice as given.
*
* @param uniqueId Bytes array that's going to be converted into the string.
* @param length Number of bytes in the array.
* @note The unique ID can be set only once (via constructor or using this method).
*/
bool setUniqueId(const byte* uniqueId, const uint16_t length);
/**
* Sets the "manufacturer" property that's going to be displayed in the Home Assistant.
*
* @param manufacturer Any string. Keep it short to save the memory.
*/
void setManufacturer(const char* manufacturer);
/**
* Sets the "model" property that's going to be displayed in the Home Assistant.
*
* @param model Any string. Keep it short to save the memory.
*/
void setModel(const char* model);
/**
* Sets the "name" property that's going to be displayed in the Home Assistant.
*
* @param name Any string. Keep it short to save the memory.
*/
void setName(const char* name);
/**
* Sets the "software version" property that's going to be displayed in the Home Assistant.
*
* @param softwareVersion Any string. Keep it short to save the memory.
*/
void setSoftwareVersion(const char* softwareVersion);
/**
* Sets device's availability and publishes MQTT message on the availability topic.
* If the device is not connected to an MQTT broker or the shared availability is not enabled then nothing happens.
*
* @param online Set to true if the device should be displayed as available in the HA panel.
*/
void setAvailability(bool online);
/**
* Enables the shared availability feature.
*/
bool enableSharedAvailability();
/**
* Enables MQTT LWT feature.
* Please note that the shared availability needs to be enabled first.
*/
void enableLastWill();
/**
* Publishes current availability of the device on the availability topic.
* If the device is not connected to an MQTT broker or the shared availability is not enabled then nothing happens.
* This method is called by the HAMqtt when the connection to an MQTT broker is acquired.
*/
void publishAvailability() const;
private:
/// The unique ID of the device. It can be a memory allocated by HADevice::setUniqueId method.
const char* _uniqueId;
/// Specifies whether HADevice class owns the _uniqueId pointer.
bool _ownsUniqueId;
/// JSON serializer of the HADevice class. It's allocated in the constructor.
HASerializer* _serializer;
/// The availability topic allocated by HADevice::enableSharedAvailability method.
char* _availabilityTopic;
/// Specifies whether the shared availability is enabled.
bool _sharedAvailability;
/// Specifies whether the device is available (online / offline).
bool _available;
};
#endif

View File

@@ -0,0 +1,318 @@
#include "HAMqtt.h"
#ifndef ARDUINOHA_TEST
#include <PubSubClient.h>
#endif
#include "HADevice.h"
#include "device-types/HABaseDeviceType.h"
#include "mocks/PubSubClientMock.h"
#define HAMQTT_INIT \
_device(device), \
_messageCallback(nullptr), \
_connectedCallback(nullptr), \
_initialized(false), \
_discoveryPrefix(DefaultDiscoveryPrefix), \
_dataPrefix(DefaultDataPrefix), \
_username(nullptr), \
_password(nullptr), \
_lastConnectionAttemptAt(0), \
_devicesTypesNb(0), \
_maxDevicesTypesNb(maxDevicesTypesNb), \
_devicesTypes(new HABaseDeviceType *[maxDevicesTypesNb]), \
_lastWillTopic(nullptr), \
_lastWillMessage(nullptr), \
_lastWillRetain(false)
static const char *DefaultDiscoveryPrefix = "homeassistant";
static const char *DefaultDataPrefix = "SHA";
HAMqtt *HAMqtt::_instance = nullptr;
void onMessageReceived(char *topic, uint8_t *payload, unsigned int length)
{
if (HAMqtt::instance() == nullptr || length > UINT16_MAX)
{
return;
}
HAMqtt::instance()->processMessage(topic, payload, static_cast<uint16_t>(length));
}
#ifdef ARDUINOHA_TEST
HAMqtt::HAMqtt(
PubSubClientMock *pubSub,
HADevice &device,
uint8_t maxDevicesTypesNb) : _mqtt(pubSub),
HAMQTT_INIT
{
_instance = this;
}
#else
HAMqtt::HAMqtt(
Client &netClient,
HADevice &device,
uint8_t maxDevicesTypesNb) : _mqtt(new PubSubClient(netClient)),
HAMQTT_INIT
{
_instance = this;
}
#endif
HAMqtt::~HAMqtt()
{
delete[] _devicesTypes;
if (_mqtt)
{
delete _mqtt;
}
_instance = nullptr;
}
bool HAMqtt::begin(
const IPAddress serverIp,
const uint16_t serverPort,
const char *username,
const char *password,
const char *clientID)
{
ARDUINOHA_DEBUG_PRINT(F("AHA: init server "))
ARDUINOHA_DEBUG_PRINT(serverIp)
ARDUINOHA_DEBUG_PRINT(F(":"))
ARDUINOHA_DEBUG_PRINTLN(serverPort)
if (_initialized)
{
ARDUINOHA_DEBUG_PRINTLN(F("AHA: already initialized"))
return false;
}
_username = username;
_password = password;
_initialized = true;
_clientID = clientID;
_mqtt->setServer(serverIp, serverPort);
_mqtt->setCallback(onMessageReceived);
return true;
}
bool HAMqtt::begin(
const IPAddress serverIp,
const char *username,
const char *password,
const char *userid)
{
return begin(serverIp, HAMQTT_DEFAULT_PORT, username, password, userid);
}
bool HAMqtt::begin(
const char *serverHostname,
const uint16_t serverPort,
const char *username,
const char *password,
const char *clientID)
{
ARDUINOHA_DEBUG_PRINT(F("AHA: init server "))
ARDUINOHA_DEBUG_PRINT(serverHostname)
ARDUINOHA_DEBUG_PRINT(F(":"))
ARDUINOHA_DEBUG_PRINTLN(serverPort)
if (_initialized)
{
ARDUINOHA_DEBUG_PRINTLN(F("AHA: already initialized"))
return false;
}
_username = username;
_password = password;
_initialized = true;
_clientID = clientID;
_mqtt->setServer(serverHostname, serverPort);
_mqtt->setCallback(onMessageReceived);
return true;
}
bool HAMqtt::begin(
const char *serverHostname,
const char *username,
const char *password,
const char *userid)
{
return begin(serverHostname, HAMQTT_DEFAULT_PORT, username, password, userid);
}
bool HAMqtt::disconnect()
{
if (!_initialized)
{
return false;
}
ARDUINOHA_DEBUG_PRINTLN(F("AHA: disconnecting"))
_initialized = false;
_lastConnectionAttemptAt = 0;
_mqtt->disconnect();
return true;
}
void HAMqtt::disableHA()
{
noHA = true;
}
void HAMqtt::loop()
{
if (_initialized && !_mqtt->loop())
{
connectToServer();
}
}
bool HAMqtt::isConnected() const
{
return _mqtt->connected();
}
void HAMqtt::addDeviceType(HABaseDeviceType *deviceType)
{
if (_devicesTypesNb + 1 >= _maxDevicesTypesNb)
{
return;
}
_devicesTypes[_devicesTypesNb++] = deviceType;
}
bool HAMqtt::publish(const char *topic, const char *payload, bool retained)
{
if (!isConnected())
{
return false;
}
ARDUINOHA_DEBUG_PRINT(F("AHA: publishing "))
ARDUINOHA_DEBUG_PRINT(topic)
ARDUINOHA_DEBUG_PRINT(F(", len: "))
ARDUINOHA_DEBUG_PRINTLN(strlen(payload))
_mqtt->beginPublish(topic, strlen(payload), retained);
_mqtt->write((const uint8_t *)(payload), strlen(payload));
return _mqtt->endPublish();
}
bool HAMqtt::beginPublish(
const char *topic,
uint16_t payloadLength,
bool retained)
{
ARDUINOHA_DEBUG_PRINT(F("AHA: begin publish "))
ARDUINOHA_DEBUG_PRINT(topic)
ARDUINOHA_DEBUG_PRINT(F(", len: "))
ARDUINOHA_DEBUG_PRINTLN(payloadLength)
return _mqtt->beginPublish(topic, payloadLength, retained);
}
void HAMqtt::writePayload(const char *data, const uint16_t length)
{
writePayload(reinterpret_cast<const uint8_t *>(data), length);
}
void HAMqtt::writePayload(const uint8_t *data, const uint16_t length)
{
_mqtt->write(data, length);
}
void HAMqtt::writePayload(const __FlashStringHelper *src)
{
_mqtt->print(src);
}
bool HAMqtt::endPublish()
{
return _mqtt->endPublish();
}
bool HAMqtt::subscribe(const char *topic)
{
ARDUINOHA_DEBUG_PRINT(F("AHA: subscribing "))
ARDUINOHA_DEBUG_PRINTLN(topic)
return _mqtt->subscribe(topic);
}
void HAMqtt::processMessage(const char *topic, const uint8_t *payload, uint16_t length)
{
ARDUINOHA_DEBUG_PRINT(F("AHA: received call "))
ARDUINOHA_DEBUG_PRINT(topic)
ARDUINOHA_DEBUG_PRINT(F(", len: "))
ARDUINOHA_DEBUG_PRINTLN(length)
if (_messageCallback)
{
_messageCallback(topic, payload, length);
}
if (!noHA)
{
for (uint8_t i = 0; i < _devicesTypesNb; i++)
{
_devicesTypes[i]->onMqttMessage(topic, payload, length);
}
}
}
void HAMqtt::connectToServer()
{
if (_lastConnectionAttemptAt > 0 &&
(millis() - _lastConnectionAttemptAt) < ReconnectInterval)
{
return;
}
_lastConnectionAttemptAt = millis();
_mqtt->connect(
_clientID,
_username,
_password,
_lastWillTopic,
0,
_lastWillRetain,
_lastWillMessage,
true);
if (isConnected())
{
ARDUINOHA_DEBUG_PRINTLN(F("AHA: connected"))
if (_connectedCallback)
{
_connectedCallback();
}
if (!noHA)
onConnectedLogic();
}
else
{
ARDUINOHA_DEBUG_PRINTLN(F("AHA: failed to connect"))
}
}
void HAMqtt::onConnectedLogic()
{
_device.publishAvailability();
for (uint8_t i = 0; i < _devicesTypesNb; i++)
{
_devicesTypes[i]->onMqttConnected();
}
}

View File

@@ -0,0 +1,413 @@
#ifndef AHA_HAMQTT_H
#define AHA_HAMQTT_H
#include <Arduino.h>
#include <Client.h>
#include <IPAddress.h>
#include "ArduinoHADefines.h"
#define HAMQTT_CALLBACK(name) void (*name)()
#define HAMQTT_MESSAGE_CALLBACK(name) void (*name)(const char *topic, const uint8_t *payload, uint16_t length)
#define HAMQTT_DEFAULT_PORT 1883
#ifdef ARDUINOHA_TEST
class PubSubClientMock;
#else
class PubSubClient;
#endif
class HADevice;
class HABaseDeviceType;
#if defined(ARDUINO_API_VERSION)
using namespace arduino;
#endif
/**
* This class is a wrapper for the PubSub API.
* It's a central point of the library where instances of all device types are stored.
*/
class HAMqtt
{
public:
/**
* Returns existing instance (singleton) of the HAMqtt class.
* It may be a null pointer if the HAMqtt object was never constructed or it was destroyed.
*/
inline static HAMqtt *instance()
{
return _instance;
}
#ifdef ARDUINOHA_TEST
explicit HAMqtt(
PubSubClientMock *pubSub,
HADevice &device,
const uint8_t maxDevicesTypesNb = 6);
#else
/**
* Creates a new instance of the HAMqtt class.
* Please note that only one instance of the class can be initialized at the same time.
*
* @param netClient The EthernetClient or WiFiClient that's going to be used for the network communication.
* @param device An instance of the HADevice class representing your device.
* @param maxDevicesTypesNb The maximum number of device types (sensors, switches, etc.) that you're going to implement.
*/
explicit HAMqtt(
Client &netClient,
HADevice &device,
const uint8_t maxDevicesTypesNb = 6);
#endif
/**
* Removes singleton of the HAMqtt class.
*/
~HAMqtt();
/**
* Sets the prefix of the Home Assistant discovery topics.
* It needs to match the prefix set in the HA admin panel.
* The default prefix is "homeassistant".
*
* @param prefix The discovery topics' prefix.
*/
inline void setDiscoveryPrefix(const char *prefix)
{
_discoveryPrefix = prefix;
}
/**
* Returns the discovery topics' prefix.
*/
inline const char *getDiscoveryPrefix() const
{
return _discoveryPrefix;
}
/**
* Sets prefix of the data topics.
* It may be useful if you want to pass MQTT traffic through a bridge.
* The default prefix is "aha".
*
* @param prefix The data topics' prefix.
*/
inline void setDataPrefix(const char *prefix)
{
_dataPrefix = prefix;
}
/**
* Returns the data topics' prefix.
*/
inline const char *getDataPrefix() const
{
return _dataPrefix;
}
/**
* Returns instance of the device assigned to the HAMqtt class.
* It's the same object (pointer) that was passed to the HAMqtt constructor.
*/
inline HADevice const *getDevice() const
{
return &_device;
}
/**
* Registers a new callback method that will be called when the device receives an MQTT message.
* Please note that the callback is also fired by internal MQTT messages used by the library.
* You should always verify the topic of the received message.
*
* @param callback Callback method.
*/
inline void onMessage(HAMQTT_MESSAGE_CALLBACK(callback))
{
_messageCallback = callback;
}
/**
* Registers a new callback method that will be called each time a connection to the MQTT broker is acquired.
* The callback is also fired after reconnecting to the broker.
* You can use this method to register topics' subscriptions.
*
* @param callback Callback method.
*/
inline void onConnected(HAMQTT_CALLBACK(callback))
{
_connectedCallback = callback;
}
/**
* Sets parameters of the MQTT connection using the IP address and port.
* The library will try to connect to the broker in first loop cycle.
* Please note that the library automatically reconnects to the broker if connection is lost.
*
* @param serverIp IP address of the MQTT broker.
* @param serverPort Port of the MQTT broker.
* @param username Username for authentication. It can be nullptr if the anonymous connection needs to be performed.
* @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed.
*/
bool begin(
const IPAddress serverIp,
const uint16_t serverPort = HAMQTT_DEFAULT_PORT,
const char *username = nullptr,
const char *password = nullptr,
const char *clientID = nullptr);
/**
* Sets parameters of the MQTT connection using the IP address and the default port (1883).
* The library will try to connect to the broker in first loop cycle.
* Please note that the library automatically reconnects to the broker if connection is lost.
*
* @param serverIp IP address of the MQTT broker.
* @param username Username for authentication. It can be nullptr if the anonymous connection needs to be performed.
* @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed.
*/
bool begin(
const IPAddress serverIp,
const char *username,
const char *password,
const char *clientID);
/**
* Sets parameters of the MQTT connection using the hostname and port.
* The library will try to connect to the broker in first loop cycle.
* Please note that the library automatically reconnects to the broker if connection is lost.
*
* @param serverHostname Hostname of the MQTT broker.
* @param serverPort Port of the MQTT broker.
* @param username Username for authentication. It can be nullptr if the anonymous connection needs to be performed.
* @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed.
*/
bool begin(
const char *serverHostname,
const uint16_t serverPort = HAMQTT_DEFAULT_PORT,
const char *username = nullptr,
const char *password = nullptr,
const char *clientID = nullptr);
/**
* Sets parameters of the MQTT connection using the hostname and the default port (1883).
* The library will try to connect to the broker in first loop cycle.
* Please note that the library automatically reconnects to the broker if connection is lost.
*
* @param serverHostname Hostname of the MQTT broker.
* @param username Username for authentication. It can be nullptr if the anonymous connection needs to be performed.
* @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed.
*/
bool begin(
const char *serverHostname,
const char *username,
const char *password,
const char *clientID);
/**
* Closes the MQTT connection.
*/
bool disconnect();
void disableHA();
/**
* This method should be called periodically inside the main loop of the firmware.
* It's safe to call this method in some interval (like 5ms).
*/
void loop();
/**
* Returns true if connection to the MQTT broker is established.
*/
bool isConnected() const;
/**
* Adds a new device's type to the MQTT.
* Each time the connection with MQTT broker is acquired, the HAMqtt class
* calls "onMqttConnected" method in all devices' types instances.
*
* @note The HAMqtt class doesn't take ownership of the given pointer.
* @param deviceType Instance of the device's type (HASwitch, HABinarySensor, etc.).
*/
void addDeviceType(HABaseDeviceType *deviceType);
/**
* Publishes the MQTT message with given topic and payload.
* Message won't be published if the connection with the MQTT broker is not established.
* In this case method returns false.
*
* @param topic The topic to publish.
* @param payload The payload to publish (it may be empty const char).
* @param retained Specifies whether message should be retained.
*/
bool publish(const char *topic, const char *payload, bool retained = false);
/**
* Begins publishing of a message with the given properties.
* When this method returns true the payload can be written using HAMqtt::writePayload method.
*
* @param topic Topic of the published message.
* @param payloadLength Length of the payload (bytes) that's going to be published.
* @param retained Specifies whether the published message should be retained.
*/
bool beginPublish(const char *topic, uint16_t payloadLength, bool retained = false);
/**
* Writes given string to the TCP stream.
* Please note that before writing any data the HAMqtt::beginPublish method
* needs to be called.
*
* @param data The string to publish.
* @param length Length of the data (bytes).
*/
void writePayload(const char *data, const uint16_t length);
/**
* Writes given data to the TCP stream.
* Please note that before writing any data the HAMqtt::beginPublish method
* needs to be called.
*
* @param data The data to publish.
* @param length Length of the data (bytes).
*/
void writePayload(const uint8_t *data, const uint16_t length);
/**
* Writes given progmem data to the TCP stream.
* Please note that before writing any data the HAMqtt::beginPublish method
* needs to be called.
*
* @param data Progmem data to publish.
*/
void writePayload(const __FlashStringHelper *data);
/**
* Finishes publishing of a message.
* After calling this method the message will be processed by the broker.
*/
bool endPublish();
/**
* Subscribes to the given topic.
* Whenever a new message is received the onMqttMessage callback in all
* devices types is called.
*
* Please note that you need to subscribe topic each time the connection
* with the broker is acquired.
*
* @param topic Topic to subscribe.
*/
bool subscribe(const char *topic);
/**
* Enables the last will message that will be produced when the device disconnects from the broker.
* If you want to change availability of the device in Home Assistant panel
* please use enableLastWill() method from the HADevice class instead.
*
* @param lastWillTopic The topic to publish.
* @param lastWillMessage The message (payload) to publish.
* @param lastWillRetain Specifies whether the published message should be retained.
*/
inline void setLastWill(
const char *lastWillTopic,
const char *lastWillMessage,
bool lastWillRetain)
{
_lastWillTopic = lastWillTopic;
_lastWillMessage = lastWillMessage;
_lastWillRetain = lastWillRetain;
}
/**
* Processes MQTT message received from the broker (subscription).
*
* @note Do not use this method on your own. It's only for the internal purpose.
* @param topic Topic of the message.
* @param payload Content of the message.
* @param length Length of the message.
*/
void processMessage(const char *topic, const uint8_t *payload, uint16_t length);
#ifdef ARDUINOHA_TEST
inline uint8_t getDevicesTypesNb() const
{
return _devicesTypesNb;
}
inline HABaseDeviceType **getDevicesTypes() const
{
return _devicesTypes;
}
#endif
private:
/// Interval between MQTT reconnects (milliseconds).
static const uint16_t ReconnectInterval = 5000;
/// Living instance of the HAMqtt class. It can be nullptr.
static HAMqtt *_instance;
/**
* Attempts to connect to the MQTT broker.
* The method uses properties passed to the "begin" method.
*/
void connectToServer();
bool noHA = false;
/**
* This method is called each time the connection with MQTT broker is acquired.
*/
void onConnectedLogic();
#ifdef ARDUINOHA_TEST
PubSubClientMock *_mqtt;
#else
/// Instance of the PubSubClient class. It's initialized in the constructor.
PubSubClient *_mqtt;
#endif
/// Instance of the HADevice passed to the constructor.
const HADevice &_device;
/// The callback method that will be called when an MQTT message is received.
HAMQTT_MESSAGE_CALLBACK(_messageCallback);
/// The callback method that will be called when the MQTT connection is acquired.
HAMQTT_CALLBACK(_connectedCallback);
/// Specifies whether the HAMqtt::begin method was ever called.
bool _initialized;
/// Teh discovery prefix that's used for the configuration messages.
const char *_discoveryPrefix;
/// The data prefix that's used for publishing data messages.
const char *_dataPrefix;
/// The username used for the authentication. It's set in the HAMqtt::begin method.
const char *_username;
/// The username used for the authentication. It's set in the HAMqtt::begin method.
const char *_clientID;
/// The password used for the authentication. It's set in the HAMqtt::begin method.
const char *_password;
/// Time of the last connection attemps (milliseconds since boot).
uint32_t _lastConnectionAttemptAt;
/// The amount of registered devices types.
uint8_t _devicesTypesNb;
/// The maximum amount of devices types that can be registered.
uint8_t _maxDevicesTypesNb;
/// Pointers of all registered devices types (array of pointers).
HABaseDeviceType **_devicesTypes;
/// The last will topic set by HAMqtt::setLastWill
const char *_lastWillTopic;
/// The last will message set by HAMqtt::setLastWill
const char *_lastWillMessage;
/// The last will retain set by HAMqtt::setLastWill
bool _lastWillRetain;
};
#endif

View File

@@ -0,0 +1,205 @@
#include "HABaseDeviceType.h"
#include "../HAMqtt.h"
#include "../HADevice.h"
#include "../utils/HAUtils.h"
#include "../utils/HASerializer.h"
HABaseDeviceType::HABaseDeviceType(
const __FlashStringHelper* componentName,
const char* uniqueId
) :
_componentName(componentName),
_uniqueId(uniqueId),
_name(nullptr),
_serializer(nullptr),
_availability(AvailabilityDefault)
{
if (mqtt()) {
mqtt()->addDeviceType(this);
}
}
void HABaseDeviceType::setAvailability(bool online)
{
_availability = (online ? AvailabilityOnline : AvailabilityOffline);
publishAvailability();
}
HAMqtt* HABaseDeviceType::mqtt()
{
return HAMqtt::instance();
}
void HABaseDeviceType::subscribeTopic(
const char* uniqueId,
const __FlashStringHelper* topic
)
{
const uint16_t topicLength = HASerializer::calculateDataTopicLength(
uniqueId,
topic
);
if (topicLength == 0) {
return;
}
char fullTopic[topicLength];
if (!HASerializer::generateDataTopic(
fullTopic,
uniqueId,
topic
)) {
return;
}
HAMqtt::instance()->subscribe(fullTopic);
}
void HABaseDeviceType::onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
)
{
(void)topic;
(void)payload;
(void)length;
}
void HABaseDeviceType::destroySerializer()
{
if (_serializer) {
delete _serializer;
_serializer = nullptr;
}
}
void HABaseDeviceType::publishConfig()
{
buildSerializer();
if (_serializer == nullptr) {
return;
}
const uint16_t topicLength = HASerializer::calculateConfigTopicLength(
componentName(),
uniqueId()
);
const uint16_t dataLength = _serializer->calculateSize();
if (topicLength > 0 && dataLength > 0) {
char topic[topicLength];
HASerializer::generateConfigTopic(
topic,
componentName(),
uniqueId()
);
if (mqtt()->beginPublish(topic, dataLength, true)) {
_serializer->flush();
mqtt()->endPublish();
}
}
destroySerializer();
}
void HABaseDeviceType::publishAvailability()
{
const HADevice* device = mqtt()->getDevice();
if (
!device ||
device->isSharedAvailabilityEnabled() ||
!isAvailabilityConfigured()
) {
return;
}
publishOnDataTopic(
AHATOFSTR(HAAvailabilityTopic),
_availability == AvailabilityOnline
? AHATOFSTR(HAOnline)
: AHATOFSTR(HAOffline),
true
);
}
bool HABaseDeviceType::publishOnDataTopic(
const __FlashStringHelper* topic,
const __FlashStringHelper* payload,
bool retained
)
{
if (!payload) {
return false;
}
return publishOnDataTopic(
topic,
reinterpret_cast<const uint8_t*>(payload),
strlen_P(AHAFROMFSTR(payload)),
retained,
true
);
}
bool HABaseDeviceType::publishOnDataTopic(
const __FlashStringHelper* topic,
const char* payload,
bool retained
)
{
if (!payload) {
return false;
}
return publishOnDataTopic(
topic,
reinterpret_cast<const uint8_t*>(payload),
strlen(payload),
retained
);
}
bool HABaseDeviceType::publishOnDataTopic(
const __FlashStringHelper* topic,
const uint8_t* payload,
const uint16_t length,
bool retained,
bool isProgmemData
)
{
if (!payload) {
return false;
}
const uint16_t topicLength = HASerializer::calculateDataTopicLength(
uniqueId(),
topic
);
if (topicLength == 0) {
return false;
}
char fullTopic[topicLength];
if (!HASerializer::generateDataTopic(
fullTopic,
uniqueId(),
topic
)) {
return false;
}
if (mqtt()->beginPublish(fullTopic, length, retained)) {
if (isProgmemData) {
mqtt()->writePayload(AHATOFSTR(payload));
} else {
mqtt()->writePayload(payload, length);
}
return mqtt()->endPublish();
}
return false;
}

View File

@@ -0,0 +1,227 @@
#ifndef AHA_HABASEDEVICETYPE_H
#define AHA_HABASEDEVICETYPE_H
#include <Arduino.h>
#include "../ArduinoHADefines.h"
class HAMqtt;
class HASerializer;
class HABaseDeviceType
{
public:
enum NumberPrecision {
/// No digits after the decimal point.
PrecisionP0 = 0,
/// One digit after the decimal point.
PrecisionP1,
/// Two digits after the decimal point.
PrecisionP2,
/// Three digits after the decimal point.
PrecisionP3
};
/**
* Creates a new device type instance and registers it in the HAMqtt class.
*
* @param componentName The name of the Home Assistant component (e.g. `binary_sensor`).
* You can find all available component names in the Home Assistant documentation.
* The component name needs to be stored in the flash memory.
* @param uniqueId The unique ID of the device type. It needs to be unique in a scope of the HADevice.
*/
HABaseDeviceType(
const __FlashStringHelper* componentName,
const char* uniqueId
);
/**
* Returns unique ID of the device type.
*/
inline const char* uniqueId() const
{ return _uniqueId; }
/**
* Returns component name defined by the device type.
* It's used for the MQTT discovery topic.
*/
inline const __FlashStringHelper* componentName() const
{ return _componentName; }
/**
* Returns `true` if the availability was configured for this device type.
*/
inline bool isAvailabilityConfigured() const
{ return (_availability != AvailabilityDefault); }
/**
* Returns online state of the device type.
*/
inline bool isOnline() const
{ return (_availability == AvailabilityOnline); }
/**
* Sets name of the device type that will be used as a label in the HA panel.
* Keep the name short to save the resources.
*
* @param name The device type name.
*/
inline void setName(const char* name)
{ _name = name; }
/**
* Returns name of the deviced type that was assigned via setName method.
* It can be nullptr if there is no name assigned.
*/
inline const char* getName() const
{ return _name; }
/**
* Sets availability of the device type.
* Setting the initial availability enables availability reporting for this device type.
* Please note that not all device types support this feature.
* Follow HA documentation of a specific device type to get more information.
*
* @param online Specifies whether the device type is online.
*/
virtual void setAvailability(bool online);
#ifdef ARDUINOHA_TEST
inline HASerializer* getSerializer() const
{ return _serializer; }
inline void buildSerializerTest()
{ buildSerializer(); }
#endif
protected:
/**
* Returns instance of the HAMqtt class.
*/
static HAMqtt* mqtt();
/**
* Subscribes to the given data topic.
*
* @param uniqueId THe unique ID of the device type assigned via the constructor.
* @param topic Topic to subscribe (progmem string).
*/
static void subscribeTopic(
const char* uniqueId,
const __FlashStringHelper* topic
);
/**
* This method should build serializer that will be used for publishing the configuration.
* The serializer is built each time the MQTT connection is acquired.
* Follow implementation of the existing device types to get better understanding of the logic.
*/
virtual void buildSerializer() { };
/**
* This method is called each time the MQTT connection is acquired.
* Each device type should publish its configuration and availability.
* It can be also used for subscribing to MQTT topics.
*/
virtual void onMqttConnected() = 0;
/**
* This method is called each time the device receives a MQTT message.
* It can be any MQTT message so the method should always verify the topic.
*
* @param topic The topic on which the message was produced.
* @param payload The payload of the message. It can be nullptr.
* @param length The length of the payload.
*/
virtual void onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
);
/**
* Destroys the existing serializer.
*/
void destroySerializer();
/**
* Publishes configuration of this device type on the HA discovery topic.
*/
void publishConfig();
/**
* Publishes current availability of the device type.
* The message is only produced if the availability is configured for this device type.
*/
void publishAvailability();
/**
* Publishes the given flash string on the data topic.
*
* @param topic The topic to publish on (progmem string).
* @param payload The message's payload (progmem string).
* @param retained Specifies whether the message should be retained.
*/
bool publishOnDataTopic(
const __FlashStringHelper* topic,
const __FlashStringHelper* payload,
bool retained = false
);
/**
* Publishes the given string on the data topic.
*
* @param topic The topic to publish on (progmem string).
* @param payload The message's payload.
* @param retained Specifies whether the message should be retained.
*/
bool publishOnDataTopic(
const __FlashStringHelper* topic,
const char* payload,
bool retained = false
);
/**
* Publishes the given data on the data topic.
*
* @param topic The topic to publish on (progmem string).
* @param payload The message's payload.
* @param length The length of the payload.
* @param retained Specifies whether the message should be retained.
* @param isProgmemData Specifies whether the given data is stored in the flash memory.
*/
bool publishOnDataTopic(
const __FlashStringHelper* topic,
const uint8_t* payload,
const uint16_t length,
bool retained = false,
bool isProgmemData = false
);
/// The component name that was assigned via the constructor.
const __FlashStringHelper* const _componentName;
/// The unique ID that was assigned via the constructor.
const char* _uniqueId;
/// The name that was set using setName method. It can be nullptr.
const char* _name;
/// HASerializer that belongs to this device type. It can be nullptr.
HASerializer* _serializer;
private:
enum Availability {
AvailabilityDefault = 0,
AvailabilityOnline,
AvailabilityOffline
};
/// The current availability of this device type. AvailabilityDefault means that the initial availability was never set.
Availability _availability;
friend class HAMqtt;
};
#endif

View File

@@ -0,0 +1,66 @@
#include "HABinarySensor.h"
#ifndef EX_ARDUINOHA_BINARY_SENSOR
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HABinarySensor::HABinarySensor(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentBinarySensor), uniqueId),
_class(nullptr),
_icon(nullptr),
_currentState(false)
{
}
bool HABinarySensor::setState(const bool state, const bool force)
{
if (!force && state == _currentState) {
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
}
void HABinarySensor::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 7); // 7 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
_serializer->set(HASerializer::WithDevice);
_serializer->set(HASerializer::WithAvailability);
_serializer->topic(AHATOFSTR(HAStateTopic));
}
void HABinarySensor::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
publishState(_currentState);
}
bool HABinarySensor::publishState(const bool state)
{
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
AHATOFSTR(state ? HAStateOn : HAStateOff),
true
);
}
#endif

View File

@@ -0,0 +1,90 @@
#ifndef AHA_HABINARYSENSOR_H
#define AHA_HABINARYSENSOR_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_BINARY_SENSOR
/**
* HABinarySensor represents a binary sensor that allows publishing on/off state to the Home Assistant panel.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/binary_sensor.mqtt/
*/
class HABinarySensor : public HABaseDeviceType
{
public:
/**
* @param uniqueId The unique ID of the button. It needs to be unique in a scope of your device.
*/
HABinarySensor(const char* uniqueId);
/**
* Changes state of the sensor and publish MQTT message.
* Please note that if a new value is the same as the previous one the MQTT message won't be published.
*
* @param state New state of the sensor (`true` - on, `false` - off).
* @param force Forces to update the state without comparing it to a previous known state.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool setState(const bool state, const bool force = false);
/**
* Sets the current state of the sensor without publishing it to Home Assistant.
* This method may be useful if you want to change the state before the connection with the MQTT broker is acquired.
*
* @param state New state of the sensor.
*/
inline void setCurrentState(const bool state)
{ _currentState = state; }
/**
* Returns the last known state of the sensor.
*/
inline bool getCurrentState() const
{ return _currentState; }
/**
* Sets class of the device.
* You can find list of available values here: https://www.home-assistant.io/integrations/binary_sensor/#device-class
*
* @param deviceClass The class name.
*/
inline void setDeviceClass(const char* deviceClass)
{ _class = deviceClass; }
/**
* Sets icon of the sensor.
* Any icon from MaterialDesignIcons.com (for example: `mdi:home`).
*
* @param icon The icon name.
*/
inline void setIcon(const char* icon)
{ _icon = icon; }
protected:
virtual void buildSerializer() override;
virtual void onMqttConnected() 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(bool state);
/// The device class. It can be nullptr.
const char* _class;
/// The icon of the sensor. It can be nullptr.
const char* _icon;
/// Current state of the sensor. By default it's false.
bool _currentState;
};
#endif
#endif

View File

@@ -0,0 +1,72 @@
#include "HAButton.h"
#ifndef EX_ARDUINOHA_BUTTON
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HAButton::HAButton(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentButton), uniqueId),
_class(nullptr),
_icon(nullptr),
_retain(false),
_commandCallback(nullptr)
{
}
void HAButton::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 8); // 8 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
// optional property
if (_retain) {
_serializer->set(
AHATOFSTR(HARetainProperty),
&_retain,
HASerializer::BoolPropertyType
);
}
_serializer->set(HASerializer::WithDevice);
_serializer->set(HASerializer::WithAvailability);
_serializer->topic(AHATOFSTR(HACommandTopic));
}
void HAButton::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic));
}
void HAButton::onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
)
{
(void)payload;
(void)length;
if (_commandCallback && HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HACommandTopic)
)) {
_commandCallback(this);
}
}
#endif

View File

@@ -0,0 +1,86 @@
#ifndef AHA_HABUTTON_H
#define AHA_HABUTTON_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_BUTTON
#define HABUTTON_CALLBACK(name) void (*name)(HAButton* sender)
/**
* HAButton represents a button that's displayed in the Home Assistant panel and
* triggers some logic on your Arduino/ESP device once clicked.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/button.mqtt/
*/
class HAButton : public HABaseDeviceType
{
public:
/**
* @param uniqueId The unique ID of the button. It needs to be unique in a scope of your device.
*/
HAButton(const char* uniqueId);
/**
* Sets class of the device.
* You can find list of available values here: https://www.home-assistant.io/integrations/button/#device-class
*
* @param deviceClass The class name.
*/
inline void setDeviceClass(const char* deviceClass)
{ _class = deviceClass; }
/**
* Sets icon of the button.
* 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 button's command.
* If set to `true` the command produced by Home Assistant will be retained.
*
* @param retain
*/
inline void setRetain(const bool retain)
{ _retain = retain; }
/**
* Registers callback that will be called each time the press command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same button.
*
* @param callback
*/
inline void onCommand(HABUTTON_CALLBACK(callback))
{ _commandCallback = callback; }
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:
/// The device class. It can be nullptr.
const char* _class;
/// The icon of the button. It can be nullptr.
const char* _icon;
/// The retain flag for the HA commands.
bool _retain;
/// The command callback that will be called once clicking the button in HA panel.
HABUTTON_CALLBACK(_commandCallback);
};
#endif
#endif

View File

@@ -0,0 +1,65 @@
#include "HACamera.h"
#ifndef EX_ARDUINOHA_CAMERA
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HACamera::HACamera(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentCamera), uniqueId),
_encoding(EncodingBinary),
_icon(nullptr)
{
}
bool HACamera::publishImage(const uint8_t* data, const uint16_t length)
{
if (!data) {
return false;
}
return publishOnDataTopic(AHATOFSTR(HATopic), data, length, true);
}
void HACamera::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 7); // 7 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
_serializer->set(
AHATOFSTR(HAEncodingProperty),
getEncodingProperty(),
HASerializer::ProgmemPropertyValue
);
_serializer->set(HASerializer::WithDevice);
_serializer->set(HASerializer::WithAvailability);
_serializer->topic(AHATOFSTR(HATopic));
}
void HACamera::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
}
const __FlashStringHelper* HACamera::getEncodingProperty() const
{
switch (_encoding) {
case EncodingBase64:
return AHATOFSTR(HAEncodingBase64);
default:
return nullptr;
}
}
#endif

View File

@@ -0,0 +1,77 @@
#ifndef AHA_HACAMERA_H
#define AHA_HACAMERA_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_CAMERA
/**
* HACamera allows to display an image in the Home Assistant panel.
* It can be used for publishing an image from the ESP32-Cam module or any other
* module that's equipped with a camera.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/camera.mqtt/
*/
class HACamera : public HABaseDeviceType
{
public:
enum ImageEncoding {
EncodingBinary = 1,
EncodingBase64
};
/**
* @param uniqueId The unique ID of the camera. It needs to be unique in a scope of your device.
*/
HACamera(const char* uniqueId);
/**
* Publishes MQTT message with the given image data as a message content.
* It updates image displayed in the Home Assistant panel.
*
* @param data Image data (raw binary data or base64)
* @param length The length of the data.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool publishImage(const uint8_t* data, const uint16_t length);
/**
* Sets encoding of the image content.
* Bu default Home Assistant expects raw binary data (e.g. JPEG binary data).
*
* @param encoding The image's data encoding.
*/
inline void setEncoding(const ImageEncoding encoding)
{ _encoding = encoding; }
/**
* Sets icon of the camera.
* Any icon from MaterialDesignIcons.com (for example: `mdi:home`).
*
* @param icon The icon name.
*/
inline void setIcon(const char* icon)
{ _icon = icon; }
protected:
virtual void buildSerializer() override;
virtual void onMqttConnected() override;
private:
/**
* Returns progmem string representing the encoding property.
*/
const __FlashStringHelper* getEncodingProperty() const;
/// The encoding of the image's data. By default it's `HACamera::EncodingBinary`.
ImageEncoding _encoding;
/// The icon of the camera. It can be nullptr.
const char* _icon;
};
#endif
#endif

View File

@@ -0,0 +1,183 @@
#include "HACover.h"
#ifndef EX_ARDUINOHA_COVER
#include "../HAMqtt.h"
#include "../utils/HAUtils.h"
#include "../utils/HANumeric.h"
#include "../utils/HASerializer.h"
HACover::HACover(const char* uniqueId, const Features features) :
HABaseDeviceType(AHATOFSTR(HAComponentCover), uniqueId),
_features(features),
_currentState(StateUnknown),
_currentPosition(DefaultPosition),
_class(nullptr),
_icon(nullptr),
_retain(false),
_optimistic(false),
_commandCallback(nullptr)
{
}
bool HACover::setState(const CoverState state, const bool force)
{
if (!force && _currentState == state) {
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
}
bool HACover::setPosition(const int16_t position, const bool force)
{
if (!force && _currentPosition == position) {
return true;
}
if (publishPosition(position)) {
_currentPosition = position;
return true;
}
return false;
}
void HACover::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 11); // 11 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
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));
if (_features & PositionFeature) {
_serializer->topic(AHATOFSTR(HAPositionTopic));
}
}
void HACover::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
if (!_retain) {
publishState(_currentState);
publishPosition(_currentPosition);
}
subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic));
}
void HACover::onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
)
{
if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HACommandTopic)
)) {
handleCommand(payload, length);
}
}
bool HACover::publishState(CoverState state)
{
if (state == StateUnknown) {
return false;
}
const __FlashStringHelper *stateStr = nullptr;
switch (state) {
case StateClosed:
stateStr = AHATOFSTR(HAClosedState);
break;
case StateClosing:
stateStr = AHATOFSTR(HAClosingState);
break;
case StateOpen:
stateStr = AHATOFSTR(HAOpenState);
break;
case StateOpening:
stateStr = AHATOFSTR(HAOpeningState);
break;
case StateStopped:
stateStr = AHATOFSTR(HAStoppedState);
break;
default:
return false;
}
return publishOnDataTopic(AHATOFSTR(HAStateTopic), stateStr, true);
}
bool HACover::publishPosition(int16_t position)
{
if (position == DefaultPosition || !(_features & PositionFeature)) {
return false;
}
char str[6 + 1] = {0}; // int16_t digits with null terminator
HANumeric(position, 0).toStr(str);
return publishOnDataTopic(AHATOFSTR(HAPositionTopic), str, true);
}
void HACover::handleCommand(const uint8_t* cmd, const uint16_t length)
{
if (!_commandCallback) {
return;
}
if (memcmp_P(cmd, HACloseCommand, length) == 0) {
_commandCallback(CommandClose, this);
} else if (memcmp_P(cmd, HAOpenCommand, length) == 0) {
_commandCallback(CommandOpen, this);
} else if (memcmp_P(cmd, HAStopCommand, length) == 0) {
_commandCallback(CommandStop, this);
}
}
#endif

View File

@@ -0,0 +1,210 @@
#ifndef AHA_HACOVER_H
#define AHA_HACOVER_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_COVER
#define HACOVER_CALLBACK(name) void (*name)(CoverCommand cmd, HACover* sender)
/**
* HACover allows to control a cover (such as blinds, a roller shutter or a garage door).
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/cover.mqtt/
*/
class HACover : public HABaseDeviceType
{
public:
static const int16_t DefaultPosition = -32768;
enum CoverState {
StateUnknown = 0,
StateClosed,
StateClosing,
StateOpen,
StateOpening,
StateStopped
};
enum CoverCommand {
CommandOpen,
CommandClose,
CommandStop
};
enum Features {
DefaultFeatures = 0,
PositionFeature = 1
};
/**
* @param uniqueId The unique ID of the cover. It needs to be unique in a scope of your device.
* @param features Features that should be enabled for the fan.
*/
HACover(const char* uniqueId, const Features features = DefaultFeatures);
/**
* Changes state of the cover 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 cover.
* @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 CoverState state, const bool force = false);
/**
* Changes the position of the cover 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 position The new position of the cover (0-100).
* @param force Forces to update the state without comparing it to a previous known state.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setPosition(const int16_t position, const bool force = false);
/**
* Sets the current state of the cover without publishing it to Home Assistant.
* This method may be useful if you want to change the state before the connection
* with the MQTT broker is acquired.
*
* @param state The new state of the cover.
*/
inline void setCurrentState(const CoverState state)
{ _currentState = state; }
/**
* Returns last known state of the cover.
* By default the state is set to CoverState::StateUnknown
*/
inline CoverState getCurrentState() const
{ return _currentState; }
/**
* Sets the current position of the cover without pushing the value to Home Assistant.
* This method may be useful if you want to change the position before the connection
* with the MQTT broker is acquired.
*
* @param position The new position of the cover (0-100).
*/
inline void setCurrentPosition(const int16_t position)
{ _currentPosition = position; }
/**
* Returns the last known position of the cover.
* By default position is set to HACover::DefaultPosition
*/
inline int16_t getCurrentPosition() const
{ return _currentPosition; }
/**
* Sets class of the device.
* You can find list of available values here: https://www.home-assistant.io/integrations/cover/
*
* @param deviceClass The class name.
*/
inline void setDeviceClass(const char* deviceClass)
{ _class = deviceClass; }
/**
* Sets icon of the cover.
* 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 cover's 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 cover state.
* In this mode the cover 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; }
/**
* Registers callback that will be called each time the command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same cover.
*
* @param callback
*/
inline void onCommand(HACOVER_CALLBACK(callback))
{ _commandCallback = callback; }
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 CoverState state);
/**
* Publishes the MQTT message with the given position.
*
* @param position The position to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishPosition(const int16_t position);
/**
* Parses the given command and executes the cover's callback with proper enum's property.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleCommand(const uint8_t* cmd, const uint16_t length);
/// Features enabled for the cover.
const uint8_t _features;
/// The current state of the cover. By default it's `HACover::StateUnknown`.
CoverState _currentState;
/// The current position of the cover. By default it's `HACover::DefaultPosition`.
int16_t _currentPosition;
/// The device class. It can be nullptr.
const char* _class;
/// The icon of the button. It can be nullptr.
const char* _icon;
/// The retain flag for the HA commands.
bool _retain;
/// The optimistic mode of the cover (`true` - enabled, `false` - disabled).
bool _optimistic;
/// The command callback that will be called when clicking the cover's button in the HA panel.
HACOVER_CALLBACK(_commandCallback);
};
#endif
#endif

View File

@@ -0,0 +1,104 @@
#include "HADeviceTracker.h"
#ifndef EX_ARDUINOHA_DEVICE_TRACKER
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HADeviceTracker::HADeviceTracker(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentDeviceTracker), uniqueId),
_icon(nullptr),
_sourceType(SourceTypeUnknown),
_currentState(StateUnknown)
{
}
bool HADeviceTracker::setState(const TrackerState state, const bool force)
{
if (!force && state == _currentState) {
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
}
void HADeviceTracker::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 7); // 7 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
_serializer->set(
AHATOFSTR(HASourceTypeProperty),
getSourceTypeProperty(),
HASerializer::ProgmemPropertyValue
);
_serializer->set(HASerializer::WithDevice);
_serializer->set(HASerializer::WithAvailability);
_serializer->topic(AHATOFSTR(HAStateTopic));
}
void HADeviceTracker::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
publishState(_currentState);
}
bool HADeviceTracker::publishState(const TrackerState state)
{
const __FlashStringHelper *stateStr = nullptr;
switch (state) {
case StateHome:
stateStr = AHATOFSTR(HAHome);
break;
case StateNotHome:
stateStr = AHATOFSTR(HANotHome);
break;
case StateNotAvailable:
stateStr = AHATOFSTR(HAOffline);
break;
default:
return false;
}
return publishOnDataTopic(AHATOFSTR(HAStateTopic), stateStr, true);
}
const __FlashStringHelper* HADeviceTracker::getSourceTypeProperty() const
{
switch (_sourceType) {
case SourceTypeGPS:
return AHATOFSTR(HAGPSType);
case SourceTypeRouter:
return AHATOFSTR(HARouterType);
case SourceTypeBluetooth:
return AHATOFSTR(HABluetoothType);
case SourceTypeBluetoothLE:
return AHATOFSTR(HABluetoothLEType);
default:
return nullptr;
}
}
#endif

View File

@@ -0,0 +1,114 @@
#ifndef AHA_HADEVICETRACKER_H
#define AHA_HADEVICETRACKER_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_DEVICE_TRACKER
/**
* HADeviceTracker allows to implement a custom device's tracker.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/device_tracker.mqtt/
*/
class HADeviceTracker : public HABaseDeviceType
{
public:
/// Available source types of the tracker.
enum SourceType {
SourceTypeUnknown = 0,
SourceTypeGPS,
SourceTypeRouter,
SourceTypeBluetooth,
SourceTypeBluetoothLE
};
/// Available states that can be reported to the HA panel.
enum TrackerState {
StateUnknown = 0,
StateHome,
StateNotHome,
StateNotAvailable
};
/**
* @param uniqueId The unique ID of the tracker. It needs to be unique in a scope of your device.
*/
HADeviceTracker(const char* uniqueId);
/**
* Changes the state of the tracker 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 The new state of the tracker.
* @param force Forces to update the state without comparing it to a previous known state.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setState(const TrackerState state, const bool force = false);
/**
* Sets the current state of the tracker without publishing it to Home Assistant.
* This method may be useful if you want to change the state before connection
* with MQTT broker is acquired.
*
* @param state The new state of the tracker.
*/
inline void setCurrentState(const TrackerState state)
{ _currentState = state; }
/**
* Returns the last known state of the tracker.
* If setState method wasn't called the initial value will be returned.
*/
inline TrackerState getState() const
{ return _currentState; }
/**
* Sets icon of the tracker.
* Any icon from MaterialDesignIcons.com (for example: `mdi:home`).
*
* @param icon The icon name.
*/
inline void setIcon(const char* icon)
{ _icon = icon; }
/**
* Sets the source type of the tracker.
*
* @param type The source type (gps, router, bluetooth, bluetooth LE).
*/
inline void setSourceType(const SourceType type)
{ _sourceType = type; }
protected:
virtual void buildSerializer() override;
virtual void onMqttConnected() 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(TrackerState state);
/**
* Returns progmem string representing source type of the tracker.
*/
const __FlashStringHelper* getSourceTypeProperty() const;
/// The icon of the tracker. It can be nullptr.
const char* _icon;
/// The source type of the tracker. By default it's `HADeviceTracker::SourceTypeUnknown`.
SourceType _sourceType;
/// The current state of the device's tracker. By default its `HADeviceTracker::StateUnknown`.
TrackerState _currentState;
};
#endif
#endif

View File

@@ -0,0 +1,209 @@
#include "HADeviceTrigger.h"
#ifndef EX_ARDUINOHA_DEVICE_TRIGGER
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HADeviceTrigger::HADeviceTrigger(const char* type, const char* subtype) :
HABaseDeviceType(AHATOFSTR(HAComponentDeviceAutomation), nullptr),
_type(type),
_subtype(subtype),
_isProgmemType(false),
_isProgmemSubtype(false)
{
buildUniqueId();
}
HADeviceTrigger::HADeviceTrigger(TriggerType type, const char* subtype) :
HABaseDeviceType(AHATOFSTR(HAComponentDeviceAutomation), nullptr),
_type(determineProgmemType(type)),
_subtype(subtype),
_isProgmemType(true),
_isProgmemSubtype(false)
{
buildUniqueId();
}
HADeviceTrigger::HADeviceTrigger(const char* type, TriggerSubtype subtype) :
HABaseDeviceType(AHATOFSTR(HAComponentDeviceAutomation), nullptr),
_type(type),
_subtype(determineProgmemSubtype(subtype)),
_isProgmemType(false),
_isProgmemSubtype(true)
{
buildUniqueId();
}
HADeviceTrigger::HADeviceTrigger(TriggerType type, TriggerSubtype subtype) :
HABaseDeviceType(AHATOFSTR(HAComponentDeviceAutomation), nullptr),
_type(determineProgmemType(type)),
_subtype(determineProgmemSubtype(subtype)),
_isProgmemType(true),
_isProgmemSubtype(true)
{
buildUniqueId();
}
HADeviceTrigger::~HADeviceTrigger()
{
if (_uniqueId) {
delete _uniqueId;
}
}
bool HADeviceTrigger::trigger()
{
if (!_type || !_subtype) {
return false;
}
return publishOnDataTopic(AHATOFSTR(HATopic), "");
}
void HADeviceTrigger::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 5); // 5 - max properties nb
_serializer->set(
AHATOFSTR(HAAutomationTypeProperty),
AHATOFSTR(HATrigger),
HASerializer::ProgmemPropertyValue
);
_serializer->set(
AHATOFSTR(HATypeProperty),
_type,
_isProgmemType
? HASerializer::ProgmemPropertyValue
: HASerializer::ConstCharPropertyValue
);
_serializer->set(
AHATOFSTR(HASubtypeProperty),
_subtype,
_isProgmemSubtype
? HASerializer::ProgmemPropertyValue
: HASerializer::ConstCharPropertyValue
);
_serializer->set(HASerializer::WithDevice);
_serializer->topic(AHATOFSTR(HATopic));
}
void HADeviceTrigger::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
}
uint16_t HADeviceTrigger::calculateIdSize() const
{
if (!_type || !_subtype) {
return 0;
}
const uint16_t typeSize = _isProgmemType ? strlen_P(_type) : strlen(_type);
const uint16_t subtypeSize = _isProgmemSubtype
? strlen_P(_subtype)
: strlen(_subtype);
// plus underscore separator and null terminator
return typeSize + subtypeSize + 2;
}
void HADeviceTrigger::buildUniqueId()
{
const uint16_t idSize = calculateIdSize();
if (idSize == 0) {
return;
}
char* id = new char[idSize];
if (_isProgmemType) {
strcpy_P(id, _type);
} else {
strcpy(id, _type);
}
strcat_P(id, HASerializerUnderscore);
if (_isProgmemSubtype) {
strcat_P(id, _subtype);
} else {
strcat(id, _subtype);
}
_uniqueId = id;
}
const char* HADeviceTrigger::determineProgmemType(TriggerType type) const
{
switch (type) {
case ButtonShortPressType:
return HAButtonShortPressType;
case ButtonShortReleaseType:
return HAButtonShortReleaseType;
case ButtonLongPressType:
return HAButtonLongPressType;
case ButtonLongReleaseType:
return HAButtonLongReleaseType;
case ButtonDoublePressType:
return HAButtonDoublePressType;
case ButtonTriplePressType:
return HAButtonTriplePressType;
case ButtonQuadruplePressType:
return HAButtonQuadruplePressType;
case ButtonQuintuplePressType:
return HAButtonQuintuplePressType;
default:
return nullptr;
}
}
const char* HADeviceTrigger::determineProgmemSubtype(
TriggerSubtype subtype
) const
{
switch (subtype) {
case TurnOnSubtype:
return HATurnOnSubtype;
case TurnOffSubtype:
return HATurnOffSubtype;
case Button1Subtype:
return HAButton1Subtype;
case Button2Subtype:
return HAButton2Subtype;
case Button3Subtype:
return HAButton3Subtype;
case Button4Subtype:
return HAButton4Subtype;
case Button5Subtype:
return HAButton5Subtype;
case Button6Subtype:
return HAButton6Subtype;
default:
return nullptr;
}
}
#endif

View File

@@ -0,0 +1,176 @@
#ifndef AHA_HADEVICETRIGGER_H
#define AHA_HADEVICETRIGGER_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_DEVICE_TRIGGER
/**
* HADeviceTrigger allows to a custom trigger that can be used in the Home Assistant automation.
* For example, it can be a wall switch that produces `press` and `long_press` actions.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/device_trigger.mqtt/
*/
class HADeviceTrigger : public HABaseDeviceType
{
public:
/// Built-in types of the trigger.
enum TriggerType {
ButtonShortPressType = 1,
ButtonShortReleaseType,
ButtonLongPressType,
ButtonLongReleaseType,
ButtonDoublePressType,
ButtonTriplePressType,
ButtonQuadruplePressType,
ButtonQuintuplePressType
};
/// Built-in subtypes of the trigger.
enum TriggerSubtype {
TurnOnSubtype = 1,
TurnOffSubtype,
Button1Subtype,
Button2Subtype,
Button3Subtype,
Button4Subtype,
Button5Subtype,
Button6Subtype
};
/**
* Creates the device trigger with a custom type and subtype.
* For example, it can be `click` as the type and `btn0` as the subtype.
* Please note that combination of the type and subtype needs to be unique in a scope of your device.
*
* @param type String representation of the type.
* @param subtype String representation of the subtype.
*/
HADeviceTrigger(const char* type, const char* subtype);
/**
* Creates the device trigger with a built-in type and a custom subtype.
* For example, it can be `HADeviceTrigger::ButtonShortPressType` as the type and `btn0` as the subtype.
* Please note that combination of the type and subtype needs to be unique in a scope of your device.
*
* @param type Built-in type of the trigger.
* @param subtype String representation of the subtype.
*/
HADeviceTrigger(TriggerType type, const char* subtype);
/**
* Creates the device trigger with a custom type and a built-in subtype.
* For example, it can be `click` as the type and `HADeviceTrigger::Button1Subtype` as the subtype.
* Please note that combination of the type and subtype needs to be unique in a scope of your device.
*
* @param type String representation of the subtype.
* @param subtype Built-in subtype of the trigger.
*/
HADeviceTrigger(const char* type, TriggerSubtype subtype);
/**
* Creates the device trigger with a built-in type and built-in subtype.
* For example, it can be `HADeviceTrigger::ButtonShortPressType` as the type and `HADeviceTrigger::Button1Subtype` as the subtype.
* Please note that combination of the type and subtype needs to be unique in a scope of your device.
*
* @param type Built-in type of the trigger.
* @param subtype Built-in subtype of the trigger.
*/
HADeviceTrigger(TriggerType type, TriggerSubtype subtype);
/**
* Frees memory allocated by the class.
*/
~HADeviceTrigger();
/**
* Publishes MQTT message with the trigger event.
* The published message is not retained.
*
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool trigger();
/**
* Returns the type of the trigger.
* If the built-in type is used the returned value points to the flash memory.
* Use `HADeviceTrigger::isProgmemType` to verify if the returned value is the progmem pointer.
*
* @returns Pointer to the type.
*/
inline const char* getType() const
{ return _type; }
/**
* Returns `true` if the built-in type was assigned to the trigger.
*/
inline bool isProgmemType() const
{ return _isProgmemType; }
/**
* Returns the subtype of the trigger.
* If the built-in subtype is used the returned value points to the flash memory.
* Use `HADeviceTrigger::isProgmemSubtype` to verify if the returned value is the progmem pointer.
*
* @returns Pointer to the subtype.
*/
inline const char* getSubtype() const
{ return _subtype; }
/**
* Returns `true` if the built-in subtype was assigned to the trigger.
*/
inline bool isProgmemSubtype() const
{ return _isProgmemSubtype; }
protected:
virtual void buildSerializer() override;
virtual void onMqttConnected() override;
private:
/**
* Calculates desired size of the unique ID based on the type and subtype that were passed to the constructor.
*/
uint16_t calculateIdSize() const;
/**
* Builds the unique ID of the device's type based on the type and subtype that were passed to the constructor.
*/
void buildUniqueId();
/**
* Returns pointer to the flash memory that represents the given type.
*
* @param subtype Built-in type enum's value.
* @returns Pointer to the flash memory if the given type is supported.
* For the unsupported type the nullptr is returned.
*/
const char* determineProgmemType(TriggerType type) const;
/**
* Returns pointer to the flash memory that represents the given subtype.
*
* @param subtype Built-in subtype enum's value.
* @returns Pointer to the flash memory if the given subtype is supported.
* For the unsupported subtype the nullptr is returned.
*/
const char* determineProgmemSubtype(TriggerSubtype subtype) const;
private:
/// Pointer to the trigger's type. It can be pointer to the flash memory.
const char* _type;
/// Pointer to the trigger's subtype. It can be pointer to the flash memory.
const char* _subtype;
/// Specifies whether the type points to the flash memory.
bool _isProgmemType;
/// Specifies whether the subtype points to the flash memory.
bool _isProgmemSubtype;
};
#endif
#endif

View File

@@ -0,0 +1,193 @@
#include "HAFan.h"
#ifndef EX_ARDUINOHA_FAN
#include "../HAMqtt.h"
#include "../utils/HAUtils.h"
#include "../utils/HASerializer.h"
HAFan::HAFan(const char* uniqueId, const uint8_t features) :
HABaseDeviceType(AHATOFSTR(HAComponentFan), uniqueId),
_features(features),
_icon(nullptr),
_retain(false),
_optimistic(false),
_speedRangeMax(),
_speedRangeMin(),
_currentState(false),
_currentSpeed(0),
_stateCallback(nullptr),
_speedCallback(nullptr)
{
}
bool HAFan::setState(const bool state, const bool force)
{
if (!force && state == _currentState) {
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
}
bool HAFan::setSpeed(const uint16_t speed, const bool force)
{
if (!force && speed == _currentSpeed) {
return true;
}
if (publishSpeed(speed)) {
_currentSpeed = speed;
return true;
}
return false;
}
void HAFan::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 13); // 13 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
if (_retain) {
_serializer->set(
AHATOFSTR(HARetainProperty),
&_retain,
HASerializer::BoolPropertyType
);
}
if (_optimistic) {
_serializer->set(
AHATOFSTR(HAOptimisticProperty),
&_optimistic,
HASerializer::BoolPropertyType
);
}
if (_features & SpeedsFeature) {
_serializer->topic(AHATOFSTR(HAPercentageStateTopic));
_serializer->topic(AHATOFSTR(HAPercentageCommandTopic));
if (_speedRangeMax.isSet()) {
_serializer->set(
AHATOFSTR(HASpeedRangeMaxProperty),
&_speedRangeMax,
HASerializer::NumberPropertyType
);
}
if (_speedRangeMin.isSet()) {
_serializer->set(
AHATOFSTR(HASpeedRangeMinProperty),
&_speedRangeMin,
HASerializer::NumberPropertyType
);
}
}
_serializer->set(HASerializer::WithDevice);
_serializer->set(HASerializer::WithAvailability);
_serializer->topic(AHATOFSTR(HAStateTopic));
_serializer->topic(AHATOFSTR(HACommandTopic));
}
void HAFan::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
if (!_retain) {
publishState(_currentState);
publishSpeed(_currentSpeed);
}
subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic));
if (_features & SpeedsFeature) {
subscribeTopic(uniqueId(), AHATOFSTR(HAPercentageCommandTopic));
}
}
void HAFan::onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
)
{
if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HACommandTopic)
)) {
handleStateCommand(payload, length);
} else if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HAPercentageCommandTopic)
)) {
handleSpeedCommand(payload, length);
}
}
bool HAFan::publishState(const bool state)
{
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
AHATOFSTR(state ? HAStateOn : HAStateOff),
true
);
}
bool HAFan::publishSpeed(const uint16_t speed)
{
if (!(_features & SpeedsFeature)) {
return false;
}
char str[5 + 1] = {0}; // uint16_t digits with null terminator
HANumeric(speed, 0).toStr(str);
return publishOnDataTopic(AHATOFSTR(HAPercentageStateTopic), str, true);
}
void HAFan::handleStateCommand(const uint8_t* cmd, const uint16_t length)
{
(void)cmd;
if (!_stateCallback) {
return;
}
bool state = length == strlen_P(HAStateOn);
_stateCallback(state, this);
}
void HAFan::handleSpeedCommand(const uint8_t* cmd, const uint16_t length)
{
if (!_speedCallback) {
return;
}
const HANumeric& number = HANumeric::fromStr(cmd, length);
if (number.isUInt16()) {
_speedCallback(number.toUInt16(), this);
}
}
#endif

View File

@@ -0,0 +1,245 @@
#ifndef AHA_HAFAN_H
#define AHA_HAFAN_H
#include "HABaseDeviceType.h"
#include "../utils/HANumeric.h"
#ifndef EX_ARDUINOHA_FAN
#define HAFAN_STATE_CALLBACK(name) void (*name)(bool state, HAFan* sender)
#define HAFAN_SPEED_CALLBACK(name) void (*name)(uint16_t speed, HAFan* sender)
/**
* HAFan allows adding a controllable fan in the Home Assistant panel.
* The library supports only the state and speed of the fan.
* If you want more features please open a new GitHub issue.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/fan.mqtt/
*/
class HAFan : public HABaseDeviceType
{
public:
enum Features {
DefaultFeatures = 0,
SpeedsFeature = 1
};
/**
* @param uniqueId The unique ID of the fan. It needs to be unique in a scope of your device.
* @param features Features that should be enabled for the fan.
*/
HAFan(const char* uniqueId, const uint8_t features = DefaultFeatures);
/**
* Changes state of the fan 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 fan.
* @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 bool state, const bool force = false);
/**
* Changes the speed of the fan 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 speed The new speed of the fan. It should be in range of min and max value.
* @param force Forces to update the value without comparing it to a previous known value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setSpeed(const uint16_t speed, const bool force = false);
/**
* Alias for `setState(true)`.
*/
inline bool turnOn()
{ return setState(true); }
/**
* Alias for `setState(false)`.
*/
inline bool turnOff()
{ return setState(false); }
/**
* Sets current state of the fan 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 fan.
*/
inline void setCurrentState(const bool state)
{ _currentState = state; }
/**
* Returns last known state of the fan.
* By default it's `false`.
*/
inline bool getCurrentState() const
{ return _currentState; }
/**
* Sets the current speed of the fan without pushing the value to Home Assistant.
* This method may be useful if you want to change the speed before the connection
* with the MQTT broker is acquired.
*
* @param speed The new speed of the fan. It should be in range of min and max value.
*/
inline void setCurrentSpeed(const uint16_t speed)
{ _currentSpeed = speed; }
/**
* Returns the last known speed of the fan.
* By default speed is set to `0`.
*/
inline uint16_t getCurrentSpeed() const
{ return _currentSpeed; }
/**
* Sets icon of the fan.
* 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 fan's 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 fan state.
* In this mode the fan 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 the maximum of numeric output range (representing 100%).
* The number of speeds within the speed_range / 100 will determine the percentage step.
* By default the maximum range is `100`.
*
* @param max The maximum of numeric output range.
*/
inline void setSpeedRangeMax(const uint16_t max)
{ _speedRangeMax.setBaseValue(max); }
/**
* Sets the minimum of numeric output range (off is not included, so speed_range_min - 1 represents 0 %).
* The number of speeds within the speed_range / 100 will determine the percentage step.
* By default the minimum range is `1`.
*
* @param min The minimum of numeric output range.
*/
inline void setSpeedRangeMin(const uint16_t min)
{ _speedRangeMin.setBaseValue(min); }
/**
* Registers callback that will be called each time the state command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same fan.
*
* @param callback
* @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; }
/**
* Registers callback that will be called each time the speed command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same fan.
*
* @param callback
* @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; }
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 bool state);
/**
* Publishes the MQTT message with the given speed.
*
* @param speed The speed to publish. It should be in range of min and max value.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishSpeed(const uint16_t speed);
/**
* Parses the given state command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleStateCommand(const uint8_t* cmd, const uint16_t length);
/**
* Parses the given speed command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleSpeedCommand(const uint8_t* cmd, const uint16_t length);
/// Features enabled for the fan.
const uint8_t _features;
/// The icon of the button. It can be nullptr.
const char* _icon;
/// The retain flag for the HA commands.
bool _retain;
/// The optimistic mode of the fan (`true` - enabled, `false` - disabled).
bool _optimistic;
/// The maximum of numeric output range.
HANumeric _speedRangeMax;
/// The minimum of numeric output range.
HANumeric _speedRangeMin;
/// The current state of the fan. By default it's `false`.
bool _currentState;
/// The current speed of the fan. By default it's `0`.
uint16_t _currentSpeed;
/// The callback that will be called when the state command is received from the HA.
HAFAN_STATE_CALLBACK(_stateCallback);
/// The callback that will be called when the speed command is received from the HA.
HAFAN_SPEED_CALLBACK(_speedCallback);
};
#endif
#endif

View File

@@ -0,0 +1,755 @@
#include "HAHVAC.h"
#ifndef EX_ARDUINOHA_HVAC
#include "../HAMqtt.h"
#include "../utils/HAUtils.h"
#include "../utils/HASerializer.h"
const uint8_t HAHVAC::DefaultFanModes = AutoFanMode | LowFanMode | MediumFanMode | HighFanMode;
const uint8_t HAHVAC::DefaultSwingModes = OnSwingMode | OffSwingMode;
const uint8_t HAHVAC::DefaultModes = AutoMode | OffMode | CoolMode | HeatMode | DryMode | FanOnlyMode;
HAHVAC::HAHVAC(
const char* uniqueId,
const uint16_t features,
const NumberPrecision precision
) :
HABaseDeviceType(AHATOFSTR(HAComponentClimate), uniqueId),
_features(features),
_precision(precision),
_icon(nullptr),
_retain(false),
_CURRENT_TEMPerature(),
_action(UnknownAction),
_temperatureUnit(DefaultUnit),
_minTemp(),
_maxTemp(),
_tempStep(),
_auxCallback(nullptr),
_auxState(false),
_powerCallback(nullptr),
_fanMode(UnknownFanMode),
_fanModes(DefaultFanModes),
_fanModesSerializer(nullptr),
_fanModeCallback(nullptr),
_swingMode(UnknownSwingMode),
_swingModes(DefaultSwingModes),
_swingModesSerializer(nullptr),
_swingModeCallback(nullptr),
_mode(UnknownMode),
_modes(DefaultModes),
_modesSerializer(nullptr),
_modeCallback(nullptr),
_targetTemperature(),
_targetTemperatureCallback(nullptr)
{
if (_features & FanFeature) {
_fanModesSerializer = new HASerializerArray(4);
}
if (_features & SwingFeature) {
_swingModesSerializer = new HASerializerArray(2);
}
if (_features & ModesFeature) {
_modesSerializer = new HASerializerArray(6);
}
}
HAHVAC::~HAHVAC()
{
if (_fanModesSerializer) {
delete _fanModesSerializer;
}
if (_swingModesSerializer) {
delete _swingModesSerializer;
}
if (_modesSerializer) {
delete _modesSerializer;
}
}
bool HAHVAC::setCurrentTemperature(const HANumeric& temperature, const bool force)
{
if (temperature.getPrecision() != _precision) {
return false;
}
if (!force && temperature == _CURRENT_TEMPerature) {
return true;
}
if (publishCurrentTemperature(temperature)) {
_CURRENT_TEMPerature = temperature;
return true;
}
return false;
}
bool HAHVAC::setAction(const Action action, const bool force)
{
if (!force && action == _action) {
return true;
}
if (publishAction(action)) {
_action = action;
return true;
}
return false;
}
bool HAHVAC::setAuxState(const bool state, const bool force)
{
if (!force && state == _auxState) {
return true;
}
if (publishAuxState(state)) {
_auxState = state;
return true;
}
return false;
}
bool HAHVAC::setFanMode(const FanMode mode, const bool force)
{
if (!force && mode == _fanMode) {
return true;
}
if (publishFanMode(mode)) {
_fanMode = mode;
return true;
}
return false;
}
bool HAHVAC::setSwingMode(const SwingMode mode, const bool force)
{
if (!force && mode == _swingMode) {
return true;
}
if (publishSwingMode(mode)) {
_swingMode = mode;
return true;
}
return false;
}
bool HAHVAC::setMode(const Mode mode, const bool force)
{
if (!force && mode == _mode) {
return true;
}
if (publishMode(mode)) {
_mode = mode;
return true;
}
return false;
}
bool HAHVAC::setTargetTemperature(const HANumeric& temperature, const bool force)
{
if (temperature.getPrecision() != _precision) {
return false;
}
if (!force && temperature == _targetTemperature) {
return true;
}
if (publishTargetTemperature(temperature)) {
_targetTemperature = temperature;
return true;
}
return false;
}
void HAHVAC::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 27); // 27 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
if (_retain) {
_serializer->set(
AHATOFSTR(HARetainProperty),
&_retain,
HASerializer::BoolPropertyType
);
}
if (_features & ActionFeature) {
_serializer->topic(AHATOFSTR(HAActionTopic));
}
if (_features & AuxHeatingFeature) {
_serializer->topic(AHATOFSTR(HAAuxCommandTopic));
_serializer->topic(AHATOFSTR(HAAuxStateTopic));
}
if (_features & PowerFeature) {
_serializer->topic(AHATOFSTR(HAPowerCommandTopic));
}
if (_features & FanFeature) {
_serializer->topic(AHATOFSTR(HAFanModeCommandTopic));
_serializer->topic(AHATOFSTR(HAFanModeStateTopic));
if (_fanModes != DefaultFanModes) {
_fanModesSerializer->clear();
if (_fanModes & AutoFanMode) {
_fanModesSerializer->add(HAFanModeAuto);
}
if (_fanModes & LowFanMode) {
_fanModesSerializer->add(HAFanModeLow);
}
if (_fanModes & MediumFanMode) {
_fanModesSerializer->add(HAFanModeMedium);
}
if (_fanModes & HighFanMode) {
_fanModesSerializer->add(HAFanModeHigh);
}
_serializer->set(
AHATOFSTR(HAFanModesProperty),
_fanModesSerializer,
HASerializer::ArrayPropertyType
);
}
}
if (_features & SwingFeature) {
_serializer->topic(AHATOFSTR(HASwingModeCommandTopic));
_serializer->topic(AHATOFSTR(HASwingModeStateTopic));
if (_swingModes != DefaultSwingModes) {
_swingModesSerializer->clear();
if (_swingModes & OnSwingMode) {
_swingModesSerializer->add(HASwingModeOn);
}
if (_swingModes & OffSwingMode) {
_swingModesSerializer->add(HASwingModeOff);
}
_serializer->set(
AHATOFSTR(HASwingModesProperty),
_swingModesSerializer,
HASerializer::ArrayPropertyType
);
}
}
if (_features & ModesFeature) {
_serializer->topic(AHATOFSTR(HAModeCommandTopic));
_serializer->topic(AHATOFSTR(HAModeStateTopic));
if (_modes != DefaultModes) {
_modesSerializer->clear();
if (_modes & AutoMode) {
_modesSerializer->add(HAModeAuto);
}
if (_modes & OffMode) {
_modesSerializer->add(HAModeOff);
}
if (_modes & CoolMode) {
_modesSerializer->add(HAModeCool);
}
if (_modes & HeatMode) {
_modesSerializer->add(HAModeHeat);
}
if (_modes & DryMode) {
_modesSerializer->add(HAModeDry);
}
if (_modes & FanOnlyMode) {
_modesSerializer->add(HAModeFanOnly);
}
_serializer->set(
AHATOFSTR(HAModesProperty),
_modesSerializer,
HASerializer::ArrayPropertyType
);
}
}
if (_features & TargetTemperatureFeature) {
_serializer->topic(AHATOFSTR(HATemperatureCommandTopic));
_serializer->topic(AHATOFSTR(HATemperatureStateTopic));
_serializer->set(
AHATOFSTR(HATemperatureCommandTemplateProperty),
getCommandWithFloatTemplate(),
HASerializer::ProgmemPropertyValue
);
}
if (_temperatureUnit != DefaultUnit) {
const __FlashStringHelper *unitStr = _temperatureUnit == CelsiusUnit
? AHATOFSTR(HATemperatureUnitC)
: AHATOFSTR(HATemperatureUnitF);
_serializer->set(
AHATOFSTR(HATemperatureUnitProperty),
unitStr,
HASerializer::ProgmemPropertyValue
);
}
if (_minTemp.isSet()) {
_serializer->set(
AHATOFSTR(HAMinTempProperty),
&_minTemp,
HASerializer::NumberPropertyType
);
}
if (_maxTemp.isSet()) {
_serializer->set(
AHATOFSTR(HAMaxTempProperty),
&_maxTemp,
HASerializer::NumberPropertyType
);
}
if (_tempStep.isSet()) {
_serializer->set(
AHATOFSTR(HATempStepProperty),
&_tempStep,
HASerializer::NumberPropertyType
);
}
_serializer->topic(AHATOFSTR(HACurrentTemperatureTopic));
_serializer->set(HASerializer::WithDevice);
_serializer->set(HASerializer::WithAvailability);
}
void HAHVAC::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
if (!_retain) {
publishCurrentTemperature(_CURRENT_TEMPerature);
publishAction(_action);
publishAuxState(_auxState);
publishFanMode(_fanMode);
publishSwingMode(_swingMode);
publishMode(_mode);
publishTargetTemperature(_targetTemperature);
}
if (_features & AuxHeatingFeature) {
subscribeTopic(uniqueId(), AHATOFSTR(HAAuxCommandTopic));
}
if (_features & PowerFeature) {
subscribeTopic(uniqueId(), AHATOFSTR(HAPowerCommandTopic));
}
if (_features & FanFeature) {
subscribeTopic(uniqueId(), AHATOFSTR(HAFanModeCommandTopic));
}
if (_features & SwingFeature) {
subscribeTopic(uniqueId(), AHATOFSTR(HASwingModeCommandTopic));
}
if (_features & ModesFeature) {
subscribeTopic(uniqueId(), AHATOFSTR(HAModeCommandTopic));
}
if (_features & TargetTemperatureFeature) {
subscribeTopic(uniqueId(), AHATOFSTR(HATemperatureCommandTopic));
}
}
void HAHVAC::onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
)
{
if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HAAuxCommandTopic)
)) {
handleAuxStateCommand(payload, length);
} else if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HAPowerCommandTopic)
)) {
handlePowerCommand(payload, length);
} else if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HAFanModeCommandTopic)
)) {
handleFanModeCommand(payload, length);
} else if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HASwingModeCommandTopic)
)) {
handleSwingModeCommand(payload, length);
} else if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HAModeCommandTopic)
)) {
handleModeCommand(payload, length);
} else if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HATemperatureCommandTopic)
)) {
handleTargetTemperatureCommand(payload, length);
}
}
bool HAHVAC::publishCurrentTemperature(const HANumeric& temperature)
{
if (!temperature.isSet()) {
return false;
}
uint8_t size = temperature.calculateSize();
if (size == 0) {
return false;
}
char str[size + 1]; // with null terminator
str[size] = 0;
temperature.toStr(str);
return publishOnDataTopic(
AHATOFSTR(HACurrentTemperatureTopic),
str,
true
);
}
bool HAHVAC::publishAction(const Action action)
{
if (action == UnknownAction || !(_features & ActionFeature)) {
return false;
}
const __FlashStringHelper *stateStr = nullptr;
switch (action) {
case OffAction:
stateStr = AHATOFSTR(HAActionOff);
break;
case HeatingAction:
stateStr = AHATOFSTR(HAActionHeating);
break;
case CoolingAction:
stateStr = AHATOFSTR(HAActionCooling);
break;
case DryingAction:
stateStr = AHATOFSTR(HAActionDrying);
break;
case IdleAction:
stateStr = AHATOFSTR(HAActionIdle);
break;
case FanAction:
stateStr = AHATOFSTR(HAActionFan);
break;
default:
return false;
}
return publishOnDataTopic(
AHATOFSTR(HAActionTopic),
stateStr,
true
);
}
bool HAHVAC::publishAuxState(const bool state)
{
if (!(_features & AuxHeatingFeature)) {
return false;
}
return publishOnDataTopic(
AHATOFSTR(HAAuxStateTopic),
AHATOFSTR(state ? HAStateOn : HAStateOff),
true
);
}
bool HAHVAC::publishFanMode(const FanMode mode)
{
if (mode == UnknownFanMode || !(_features & FanFeature)) {
return false;
}
const __FlashStringHelper *stateStr = nullptr;
switch (mode) {
case AutoFanMode:
stateStr = AHATOFSTR(HAFanModeAuto);
break;
case LowFanMode:
stateStr = AHATOFSTR(HAFanModeLow);
break;
case MediumFanMode:
stateStr = AHATOFSTR(HAFanModeMedium);
break;
case HighFanMode:
stateStr = AHATOFSTR(HAFanModeHigh);
break;
default:
return false;
}
return publishOnDataTopic(
AHATOFSTR(HAFanModeStateTopic),
stateStr,
true
);
}
bool HAHVAC::publishSwingMode(const SwingMode mode)
{
if (mode == UnknownSwingMode || !(_features & SwingFeature)) {
return false;
}
const __FlashStringHelper *stateStr = nullptr;
switch (mode) {
case OnSwingMode:
stateStr = AHATOFSTR(HASwingModeOn);
break;
case OffSwingMode:
stateStr = AHATOFSTR(HASwingModeOff);
break;
default:
return false;
}
return publishOnDataTopic(
AHATOFSTR(HASwingModeStateTopic),
stateStr,
true
);
}
bool HAHVAC::publishMode(const Mode mode)
{
if (mode == UnknownMode || !(_features & ModesFeature)) {
return false;
}
const __FlashStringHelper *stateStr = nullptr;
switch (mode) {
case AutoMode:
stateStr = AHATOFSTR(HAModeAuto);
break;
case OffMode:
stateStr = AHATOFSTR(HAModeOff);
break;
case CoolMode:
stateStr = AHATOFSTR(HAModeCool);
break;
case HeatMode:
stateStr = AHATOFSTR(HAModeHeat);
break;
case DryMode:
stateStr = AHATOFSTR(HAModeDry);
break;
case FanOnlyMode:
stateStr = AHATOFSTR(HAModeFanOnly);
break;
default:
return false;
}
return publishOnDataTopic(
AHATOFSTR(HAModeStateTopic),
stateStr,
true
);
}
bool HAHVAC::publishTargetTemperature(const HANumeric& temperature)
{
if (!temperature.isSet()) {
return false;
}
uint8_t size = temperature.calculateSize();
if (size == 0) {
return false;
}
char str[size + 1]; // with null terminator
str[size] = 0;
temperature.toStr(str);
return publishOnDataTopic(
AHATOFSTR(HATemperatureStateTopic),
str,
true
);
}
void HAHVAC::handleAuxStateCommand(const uint8_t* cmd, const uint16_t length)
{
(void)cmd;
if (!_auxCallback) {
return;
}
bool state = length == strlen_P(HAStateOn);
_auxCallback(state, this);
}
void HAHVAC::handlePowerCommand(const uint8_t* cmd, const uint16_t length)
{
(void)cmd;
if (!_powerCallback) {
return;
}
bool state = length == strlen_P(HAStateOn);
_powerCallback(state, this);
}
void HAHVAC::handleFanModeCommand(const uint8_t* cmd, const uint16_t length)
{
if (!_fanModeCallback) {
return;
}
if (memcmp_P(cmd, HAFanModeAuto, length) == 0) {
_fanModeCallback(AutoFanMode, this);
} else if (memcmp_P(cmd, HAFanModeLow, length) == 0) {
_fanModeCallback(LowFanMode, this);
} else if (memcmp_P(cmd, HAFanModeMedium, length) == 0) {
_fanModeCallback(MediumFanMode, this);
} else if (memcmp_P(cmd, HAFanModeHigh, length) == 0) {
_fanModeCallback(HighFanMode, this);
}
}
void HAHVAC::handleSwingModeCommand(const uint8_t* cmd, const uint16_t length)
{
if (!_swingModeCallback) {
return;
}
if (memcmp_P(cmd, HASwingModeOn, length) == 0) {
_swingModeCallback(OnSwingMode, this);
} else if (memcmp_P(cmd, HASwingModeOff, length) == 0) {
_swingModeCallback(OffSwingMode, this);
}
}
void HAHVAC::handleModeCommand(const uint8_t* cmd, const uint16_t length)
{
if (!_modeCallback) {
return;
}
if (memcmp_P(cmd, HAModeAuto, length) == 0) {
_modeCallback(AutoMode, this);
} else if (memcmp_P(cmd, HAModeOff, length) == 0) {
_modeCallback(OffMode, this);
} else if (memcmp_P(cmd, HAModeCool, length) == 0) {
_modeCallback(CoolMode, this);
} else if (memcmp_P(cmd, HAModeHeat, length) == 0) {
_modeCallback(HeatMode, this);
} else if (memcmp_P(cmd, HAModeDry, length) == 0) {
_modeCallback(DryMode, this);
} else if (memcmp_P(cmd, HAModeFanOnly, length) == 0) {
_modeCallback(FanOnlyMode, this);
}
}
void HAHVAC::handleTargetTemperatureCommand(const uint8_t* cmd, const uint16_t length)
{
if (!_targetTemperatureCallback) {
return;
}
HANumeric number = HANumeric::fromStr(cmd, length);
if (number.isSet()) {
number.setPrecision(_precision);
_targetTemperatureCallback(number, this);
}
}
const __FlashStringHelper* HAHVAC::getCommandWithFloatTemplate()
{
switch (_precision) {
case PrecisionP1:
return AHATOFSTR(HAValueTemplateFloatP1);
case PrecisionP2:
return AHATOFSTR(HAValueTemplateFloatP2);
case PrecisionP3:
return AHATOFSTR(HAValueTemplateFloatP3);
default:
return nullptr;
}
}
#endif

View File

@@ -0,0 +1,704 @@
#ifndef AHA_HAHVAC_H
#define AHA_HAHVAC_H
#include "HABaseDeviceType.h"
#include "../utils/HANumeric.h"
#ifndef EX_ARDUINOHA_HVAC
#define _SET_CURRENT_TEMPERATURE_OVERLOAD(type) \
/** @overload */ \
inline bool setCurrentTemperature(const type temperature, const bool force = false) \
{ return setCurrentTemperature(HANumeric(temperature, _precision), force); }
#define _SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(type) \
/** @overload */ \
inline void setCurrentCurrentTemperature(const type temperature) \
{ setCurrentCurrentTemperature(HANumeric(temperature, _precision)); }
#define _SET_TARGET_TEMPERATURE_OVERLOAD(type) \
/** @overload */ \
inline bool setTargetTemperature(const type temperature, const bool force = false) \
{ return setTargetTemperature(HANumeric(temperature, _precision), force); }
#define _SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(type) \
/** @overload */ \
inline void setCurrentTargetTemperature(const type temperature) \
{ setCurrentTargetTemperature(HANumeric(temperature, _precision)); }
#define HAHVAC_CALLBACK_BOOL(name) void (*name)(bool state, HAHVAC* sender)
#define HAHVAC_CALLBACK_TARGET_TEMP(name) void (*name)(HANumeric temperature, HAHVAC* sender)
#define HAHVAC_CALLBACK_FAN_MODE(name) void (*name)(FanMode mode, HAHVAC* sender)
#define HAHVAC_CALLBACK_SWING_MODE(name) void (*name)(SwingMode mode, HAHVAC* sender)
#define HAHVAC_CALLBACK_MODE(name) void (*name)(Mode mode, HAHVAC* sender)
class HASerializerArray;
/**
* HAHVAC lets you control your HVAC devices.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/climate.mqtt/
*/
class HAHVAC : public HABaseDeviceType
{
public:
static const uint8_t DefaultFanModes;
static const uint8_t DefaultSwingModes;
static const uint8_t DefaultModes;
/// The list of features available in the HVAC. They're used in the constructor.
enum Features {
DefaultFeatures = 0,
ActionFeature = 1,
AuxHeatingFeature = 2,
PowerFeature = 4,
FanFeature = 8,
SwingFeature = 16,
ModesFeature = 32,
TargetTemperatureFeature = 64
};
/// The list of available actions of the HVAC.
enum Action {
UnknownAction = 0,
OffAction,
HeatingAction,
CoolingAction,
DryingAction,
IdleAction,
FanAction
};
/// The list of available fan modes.
enum FanMode {
UnknownFanMode = 0,
AutoFanMode = 1,
LowFanMode = 2,
MediumFanMode = 4,
HighFanMode = 8
};
/// The list of available swing modes.
enum SwingMode {
UnknownSwingMode = 0,
OnSwingMode = 1,
OffSwingMode = 2
};
/// The list of available HVAC's modes.
enum Mode {
UnknownMode = 0,
AutoMode = 1,
OffMode = 2,
CoolMode = 4,
HeatMode = 8,
DryMode = 16,
FanOnlyMode = 32
};
/// Temperature units available in the HVAC.
enum TemperatureUnit {
DefaultUnit = 1,
CelsiusUnit,
FahrenheitUnit
};
/**
* @param uniqueId The unique ID of the HVAC. It needs to be unique in a scope of your device.
* @param features Features that should be enabled for the HVAC.
* @param precision The precision of temperatures reported by the HVAC.
*/
HAHVAC(
const char* uniqueId,
const uint16_t features = DefaultFeatures,
const NumberPrecision precision = PrecisionP1
);
/**
* Frees memory allocated for the arrays serialization.
*/
~HAHVAC();
/**
* Changes current temperature of the HVAC 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 temperature New current temperature.
* @param force Forces to update the temperature without comparing it to a previous known value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setCurrentTemperature(const HANumeric& temperature, const bool force = false);
_SET_CURRENT_TEMPERATURE_OVERLOAD(int8_t)
_SET_CURRENT_TEMPERATURE_OVERLOAD(int16_t)
_SET_CURRENT_TEMPERATURE_OVERLOAD(int32_t)
_SET_CURRENT_TEMPERATURE_OVERLOAD(uint8_t)
_SET_CURRENT_TEMPERATURE_OVERLOAD(uint16_t)
_SET_CURRENT_TEMPERATURE_OVERLOAD(uint32_t)
_SET_CURRENT_TEMPERATURE_OVERLOAD(float)
#ifdef __SAMD21G18A__
_SET_CURRENT_TEMPERATURE_OVERLOAD(int)
#endif
/**
* Changes action of the HVAC 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 action New action.
* @param force Forces to update the action without comparing it to a previous known value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setAction(const Action action, const bool force = false);
/**
* Changes state of the aux heating 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 The new state.
* @param force Forces to update the state without comparing it to a previous known value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setAuxState(const bool state, const bool force = false);
/**
* Changes mode of the fan of the HVAC 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 mode New fan's mode.
* @param force Forces to update the mode without comparing it to a previous known value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setFanMode(const FanMode mode, const bool force = false);
/**
* Changes swing mode of the HVAC 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 mode New swing mode.
* @param force Forces to update the mode without comparing it to a previous known value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setSwingMode(const SwingMode mode, const bool force = false);
/**
* Changes mode of the HVAC 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 mode New HVAC's mode.
* @param force Forces to update the mode without comparing it to a previous known value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setMode(const Mode mode, const bool force = false);
/**
* Changes target temperature of the HVAC 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 temperature Target temperature to set.
* @param force Forces to update the mode without comparing it to a previous known value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setTargetTemperature(const HANumeric& temperature, const bool force = false);
_SET_TARGET_TEMPERATURE_OVERLOAD(int8_t)
_SET_TARGET_TEMPERATURE_OVERLOAD(int16_t)
_SET_TARGET_TEMPERATURE_OVERLOAD(int32_t)
_SET_TARGET_TEMPERATURE_OVERLOAD(uint8_t)
_SET_TARGET_TEMPERATURE_OVERLOAD(uint16_t)
_SET_TARGET_TEMPERATURE_OVERLOAD(uint32_t)
_SET_TARGET_TEMPERATURE_OVERLOAD(float)
#ifdef __SAMD21G18A__
_SET_TARGET_TEMPERATURE_OVERLOAD(int)
#endif
/**
* Sets current temperature of the HVAC without publishing it to Home Assistant.
* This method may be useful if you want to change temperature before connection
* with MQTT broker is acquired.
*
* @param temperature New current temperature.
*/
inline void setCurrentCurrentTemperature(const HANumeric& temperature)
{ if (temperature.getPrecision() == _precision) { _CURRENT_TEMPerature = temperature; } }
_SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(int8_t)
_SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(int16_t)
_SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(int32_t)
_SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(uint8_t)
_SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(uint16_t)
_SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(uint32_t)
_SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(float)
#ifdef __SAMD21G18A__
_SET_CURRENT_CURRENT_TEMPERATURE_OVERLOAD(int)
#endif
/**
* Returns last known current temperature of the HVAC.
* If setCurrentTemperature method wasn't called the initial value will be returned.
*/
inline const HANumeric& getCurrentTemperature() const
{ return _CURRENT_TEMPerature; }
/**
* Sets action of the HVAC without publishing it to Home Assistant.
* This method may be useful if you want to change the action before connection
* with MQTT broker is acquired.
*
* @param action New action.
*/
inline void setCurrentAction(const Action action)
{ _action = action; }
/**
* Returns last known action of the HVAC.
* If setAction method wasn't called the initial value will be returned.
*/
inline Action getCurrentAction() const
{ return _action; }
/**
* Sets aux heating state without publishing it to Home Assistant.
* This method may be useful if you want to change the state before connection
* with MQTT broker is acquired.
*
* @param state The new state.
*/
inline void setCurrentAuxState(const bool state)
{ _auxState = state; }
/**
* Returns last known state of the aux heating.
* If setAuxState method wasn't called the initial value will be returned.
*/
inline bool getCurrentAuxState() const
{ return _auxState; }
/**
* Sets fan's mode of the HVAC without publishing it to Home Assistant.
* This method may be useful if you want to change the mode before connection
* with MQTT broker is acquired.
*
* @param mode New fan's mode.
*/
inline void setCurrentFanMode(const FanMode mode)
{ _fanMode = mode; }
/**
* Returns last known fan's mode of the HVAC.
* If setFanMode method wasn't called the initial value will be returned.
*/
inline FanMode getCurrentFanMode() const
{ return _fanMode; }
/**
* Sets available fan modes.
*
* @param modes The modes to set (for example: `HAHVAC::AutoFanMode | HAHVAC::HighFanMode`).
*/
inline void setFanModes(const uint8_t modes)
{ _fanModes = modes; }
/**
* Sets swing mode of the HVAC without publishing it to Home Assistant.
* This method may be useful if you want to change the mode before connection
* with MQTT broker is acquired.
*
* @param mode New swing mode.
*/
inline void setCurrentSwingMode(const SwingMode mode)
{ _swingMode = mode; }
/**
* Returns last known swing mode of the HVAC.
* If setSwingMode method wasn't called the initial value will be returned.
*/
inline SwingMode getCurrentSwingMode() const
{ return _swingMode; }
/**
* Sets available swing modes.
*
* @param modes The modes to set (for example: `HAHVAC::OnSwingMode`).
*/
inline void setSwingModes(const uint8_t modes)
{ _swingModes = modes; }
/**
* Sets mode of the HVAC without publishing it to Home Assistant.
* This method may be useful if you want to change the mode before connection
* with MQTT broker is acquired.
*
* @param mode New HVAC's mode.
*/
inline void setCurrentMode(const Mode mode)
{ _mode = mode; }
/**
* Returns last known mode of the HVAC.
* If setMode method wasn't called the initial value will be returned.
*/
inline Mode getCurrentMode() const
{ return _mode; }
/**
* Sets available HVAC's modes.
*
* @param modes The modes to set (for example: `HAHVAC::CoolMode | HAHVAC::HeatMode`).
*/
inline void setModes(const uint8_t modes)
{ _modes = modes; }
/**
* Sets target temperature of the HVAC without publishing it to Home Assistant.
* This method may be useful if you want to change the target before connection
* with MQTT broker is acquired.
*
* @param temperature Target temperature to set.
*/
inline void setCurrentTargetTemperature(const HANumeric& temperature)
{ if (temperature.getPrecision() == _precision) { _targetTemperature = temperature; } }
_SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(int8_t)
_SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(int16_t)
_SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(int32_t)
_SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(uint8_t)
_SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(uint16_t)
_SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(uint32_t)
_SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(float)
#ifdef __SAMD21G18A__
_SET_CURRENT_TARGET_TEMPERATURE_OVERLOAD(int)
#endif
/**
* Returns last known target temperature of the HVAC.
* If setTargetTemperature method wasn't called the initial value will be returned.
*/
inline const HANumeric& getCurrentTargetTemperature() const
{ return _targetTemperature; }
/**
* Sets icon of the HVAC.
* 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 HVAC's command.
* If set to `true` the command produced by Home Assistant will be retained.
*
* @param retain
*/
inline void setRetain(const bool retain)
{ _retain = retain; }
/**
* Changes the temperature unit.
*
* @param unit See the TemperatureUnit enum above.
*/
inline void setTemperatureUnit(TemperatureUnit unit)
{ _temperatureUnit = unit; }
/**
* Sets the minimum temperature that can be set from the Home Assistant panel.
*
* @param min The minimum value.
*/
inline void setMinTemp(const float min)
{ _minTemp = HANumeric(min, _precision); }
/**
* Sets the maximum temperature that can be set from the Home Assistant panel.
*
* @param min The maximum value.
*/
inline void setMaxTemp(const float max)
{ _maxTemp = HANumeric(max, _precision); }
/**
* Sets the step of the temperature that can be set from the Home Assistant panel.
*
* @param step The setp value. By default it's `1`.
*/
inline void setTempStep(const float step)
{ _tempStep = HANumeric(step, _precision); }
/**
* Registers callback that will be called each time the aux state command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same HVAC.
*
* @param callback
* @note The aux state must be reported back to HA using the HAHVAC::setAuxState method.
*/
inline void onAuxStateCommand(HAHVAC_CALLBACK_BOOL(callback))
{ _auxCallback = callback; }
/**
* Registers callback that will be called each time the power command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same HVAC.
*
* @param callback
*/
inline void onPowerCommand(HAHVAC_CALLBACK_BOOL(callback))
{ _powerCallback = callback; }
/**
* Registers callback that will be called each time the fan mode command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same HVAC.
*
* @param callback
* @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; }
/**
* Registers callback that will be called each time the swing mode command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same HVAC.
*
* @param callback
* @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; }
/**
* Registers callback that will be called each time the HVAC mode command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same HVAC.
*
* @param callback
* @note The mode must be reported back to HA using the HAHVAC::setMode method.
*/
inline void onModeCommand(HAHVAC_CALLBACK_MODE(callback))
{ _modeCallback = callback; }
/**
* Registers callback that will be called each time the target temperature is set via HA panel.
* Please note that it's not possible to register multiple callbacks for the same HVAC.
*
* @param callback
* @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; }
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 current temperature.
*
* @param temperature The temperature to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishCurrentTemperature(const HANumeric& temperature);
/**
* Publishes the MQTT message with the given action.
*
* @param action The action to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishAction(const Action action);
/**
* Publishes the MQTT message with the given aux heating state.
*
* @param state The state to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishAuxState(const bool state);
/**
* Publishes the MQTT message with the given fan mode.
*
* @param mode The mode to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishFanMode(const FanMode mode);
/**
* Publishes the MQTT message with the given swing mode.
*
* @param mode The mode to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishSwingMode(const SwingMode mode);
/**
* Publishes the MQTT message with the given mode.
*
* @param mode The mode to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishMode(const Mode mode);
/**
* Publishes the MQTT message with the given target temperature.
*
* @param temperature The temperature to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishTargetTemperature(const HANumeric& temperature);
/**
* Parses the given aux state command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleAuxStateCommand(const uint8_t* cmd, const uint16_t length);
/**
* Parses the given power command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handlePowerCommand(const uint8_t* cmd, const uint16_t length);
/**
* Parses the given fan mode command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleFanModeCommand(const uint8_t* cmd, const uint16_t length);
/**
* Parses the given swing mode command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleSwingModeCommand(const uint8_t* cmd, const uint16_t length);
/**
* Parses the given HVAC's mode command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleModeCommand(const uint8_t* cmd, const uint16_t length);
/**
* Parses the given HVAC's target temperature command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleTargetTemperatureCommand(const uint8_t* cmd, const uint16_t length);
/**
* Returns progmem string representing value template for the command
* that contains floating point numbers.
*/
const __FlashStringHelper* getCommandWithFloatTemplate();
/// Features enabled for the HVAC.
const uint16_t _features;
/// The precision of temperatures. By default it's `HANumber::PrecisionP1`.
const NumberPrecision _precision;
/// The icon of the button. It can be nullptr.
const char* _icon;
/// The retain flag for the HA commands.
bool _retain;
/// The current temperature of the HVAC. By default it's not set.
HANumeric _CURRENT_TEMPerature;
/// The current action of the HVAC. By default it's `HAHVAC::UnknownAction`.
Action _action;
/// The temperature unit for the HVAC. By default it's `HAHVAC::DefaultUnit`.
TemperatureUnit _temperatureUnit;
/// The minimum temperature that can be set.
HANumeric _minTemp;
/// The maximum temperature that can be set.
HANumeric _maxTemp;
/// The step of the temperature that can be set.
HANumeric _tempStep;
/// Callback that will be called when the aux state command is received from the HA.
HAHVAC_CALLBACK_BOOL(_auxCallback);
/// The state of the aux heating. By default it's `false`.
bool _auxState;
/// Callback that will be called when the power command is received from the HA.
HAHVAC_CALLBACK_BOOL(_powerCallback);
/// The current mode of the fan. By default it's `HAHVAC::UnknownFanMode`.
FanMode _fanMode;
/// The supported fan modes. By default it's `HAHVAC::DefaultFanModes`.
uint8_t _fanModes;
/// The serializer for the fan modes. It's `nullptr` if the fan feature is disabled.
HASerializerArray* _fanModesSerializer;
/// Callback that will be called when the fan mode command is received from the HA.
HAHVAC_CALLBACK_FAN_MODE(_fanModeCallback);
/// The current swing mode. By default it's `HAHVAC::UnknownSwingMode`.
SwingMode _swingMode;
/// The supported swing modes. By default it's `HAHVAC::DefaultSwingModes`.
uint8_t _swingModes;
/// The serializer for the swing modes. It's `nullptr` if the swing feature is disabled.
HASerializerArray* _swingModesSerializer;
/// Callback that will be called when the swing mode command is received from the HA.
HAHVAC_CALLBACK_SWING_MODE(_swingModeCallback);
/// The current mode. By default it's `HAHVAC::UnknownMode`.
Mode _mode;
/// The supported modes. By default it's `HAHVAC::DefaultModes`.
uint8_t _modes;
/// The serializer for the modes. It's `nullptr` if the modes feature is disabled.
HASerializerArray* _modesSerializer;
/// Callback that will be called when the mode command is received from the HA.
HAHVAC_CALLBACK_MODE(_modeCallback);
/// The target temperature of the HVAC. By default it's not set.
HANumeric _targetTemperature;
/// Callback that will be called when the target temperature is changed via the HA panel.
HAHVAC_CALLBACK_TARGET_TEMP(_targetTemperatureCallback);
};
#endif
#endif

View File

@@ -0,0 +1,372 @@
#include "HALight.h"
#ifndef EX_ARDUINOHA_LIGHT
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
const uint8_t HALight::RGBStringMaxLength = 3*4; // 4 characters per color
void HALight::RGBColor::fromBuffer(const uint8_t* data, const uint16_t length)
{
if (length > RGBStringMaxLength) {
return;
}
uint8_t firstCommaPos = 0;
uint8_t secondCommaPos = 0;
for (uint8_t i = 0; i < length; i++) {
if (data[i] == ',') {
if (firstCommaPos == 0) {
firstCommaPos = i;
} else if (secondCommaPos == 0) {
secondCommaPos = i;
}
}
}
if (firstCommaPos == 0 || secondCommaPos == 0) {
return;
}
const uint8_t redLen = firstCommaPos;
const uint8_t greenLen = secondCommaPos - firstCommaPos - 1; // minus comma
const uint8_t blueLen = length - redLen - greenLen - 2; // minus two commas
const HANumeric& r = HANumeric::fromStr(data, redLen);
const HANumeric& g = HANumeric::fromStr(&data[redLen + 1], greenLen);
const HANumeric& b = HANumeric::fromStr(&data[redLen + greenLen + 2], blueLen);
if (r.isUInt8() && g.isUInt8() && b.isUInt8()) {
red = r.toUInt8();
green = g.toUInt8();
blue = b.toUInt8();
isSet = true;
}
}
HALight::HALight(const char* uniqueId, const uint8_t features) :
HABaseDeviceType(AHATOFSTR(HAComponentLight), uniqueId),
_features(features),
_icon(nullptr),
_retain(false),
_optimistic(false),
_brightnessScale(),
_currentState(false),
_BRIGHTNESS(0),
_minMireds(),
_maxMireds(),
_currentColorTemperature(0),
_currentRGBColor(),
_stateCallback(nullptr),
_brightnessCallback(nullptr),
_colorTemperatureCallback(nullptr),
_rgbColorCallback(nullptr)
{
}
bool HALight::setState(const bool state, const bool force)
{
if (!force && state == _currentState) {
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
}
bool HALight::setBrightness(const uint8_t brightness, const bool force)
{
if (!force && brightness == _BRIGHTNESS) {
return true;
}
if (publishBrightness(brightness)) {
_BRIGHTNESS = brightness;
return true;
}
return false;
}
bool HALight::setColorTemperature(const uint16_t temperature, const bool force)
{
if (!force && temperature == _currentColorTemperature) {
return true;
}
if (publishColorTemperature(temperature)) {
_currentColorTemperature = temperature;
return true;
}
return false;
}
bool HALight::setRGBColor(const RGBColor& color, const bool force)
{
if (!force && color == _currentRGBColor) {
return true;
}
if (publishRGBColor(color)) {
_currentRGBColor = color;
return true;
}
return false;
}
void HALight::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 18); // 18 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
if (_retain) {
_serializer->set(
AHATOFSTR(HARetainProperty),
&_retain,
HASerializer::BoolPropertyType
);
}
if (_optimistic) {
_serializer->set(
AHATOFSTR(HAOptimisticProperty),
&_optimistic,
HASerializer::BoolPropertyType
);
}
if (_features & BrightnessFeature) {
_serializer->topic(AHATOFSTR(HABrightnessStateTopic));
_serializer->topic(AHATOFSTR(HABrightnessCommandTopic));
if (_brightnessScale.isSet()) {
_serializer->set(
AHATOFSTR(HABrightnessScaleProperty),
&_brightnessScale,
HASerializer::NumberPropertyType
);
}
}
if (_features & ColorTemperatureFeature) {
_serializer->topic(AHATOFSTR(HAColorTemperatureStateTopic));
_serializer->topic(AHATOFSTR(HAColorTemperatureCommandTopic));
if (_minMireds.isSet()) {
_serializer->set(
AHATOFSTR(HAMinMiredsProperty),
&_minMireds,
HASerializer::NumberPropertyType
);
}
if (_maxMireds.isSet()) {
_serializer->set(
AHATOFSTR(HAMaxMiredsProperty),
&_maxMireds,
HASerializer::NumberPropertyType
);
}
}
if (_features & RGBFeature) {
_serializer->topic(AHATOFSTR(HARGBCommandTopic));
_serializer->topic(AHATOFSTR(HARGBStateTopic));
}
_serializer->set(HASerializer::WithDevice);
_serializer->set(HASerializer::WithAvailability);
_serializer->topic(AHATOFSTR(HAStateTopic));
_serializer->topic(AHATOFSTR(HACommandTopic));
}
void HALight::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
if (!_retain) {
publishState(_currentState);
publishBrightness(_BRIGHTNESS);
publishColorTemperature(_currentColorTemperature);
publishRGBColor(_currentRGBColor);
}
subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic));
if (_features & BrightnessFeature) {
subscribeTopic(uniqueId(), AHATOFSTR(HABrightnessCommandTopic));
}
if (_features & ColorTemperatureFeature) {
subscribeTopic(uniqueId(), AHATOFSTR(HAColorTemperatureCommandTopic));
}
if (_features & RGBFeature) {
subscribeTopic(uniqueId(), AHATOFSTR(HARGBCommandTopic));
}
}
void HALight::onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
)
{
if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HACommandTopic)
)) {
handleStateCommand(payload, length);
} else if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HABrightnessCommandTopic)
)) {
handleBrightnessCommand(payload, length);
} else if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HAColorTemperatureCommandTopic)
)) {
handleColorTemperatureCommand(payload, length);
} else if (
HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HARGBCommandTopic)
)
) {
handleRGBCommand(payload, length);
}
}
bool HALight::publishState(const bool state)
{
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
AHATOFSTR(state ? HAStateOn : HAStateOff),
true
);
}
bool HALight::publishBrightness(const uint8_t brightness)
{
if (!(_features & BrightnessFeature)) {
return false;
}
char str[3 + 1] = {0}; // uint8_t digits with null terminator
HANumeric(brightness, 0).toStr(str);
return publishOnDataTopic(AHATOFSTR(HABrightnessStateTopic), str, true);
}
bool HALight::publishColorTemperature(const uint16_t temperature)
{
if (!(_features & ColorTemperatureFeature)) {
return false;
}
char str[5 + 1] = {0}; // uint16_t digits with null terminator
HANumeric(temperature, 0).toStr(str);
return publishOnDataTopic(AHATOFSTR(HAColorTemperatureStateTopic), str, true);
}
bool HALight::publishRGBColor(const RGBColor& color)
{
if (!(_features & RGBFeature) || !color.isSet) {
return false;
}
char str[RGBStringMaxLength] = {0};
uint16_t len = 0;
// append red color with comma
len += HANumeric(color.red, 0).toStr(&str[0]);
str[len++] = ',';
// append green color with comma
len += HANumeric(color.green, 0).toStr(&str[len]);
str[len++] = ',';
// append blue color
HANumeric(color.blue, 0).toStr(&str[len]);
return publishOnDataTopic(AHATOFSTR(HARGBStateTopic), str, true);
}
void HALight::handleStateCommand(const uint8_t* cmd, const uint16_t length)
{
(void)cmd;
if (!_stateCallback) {
return;
}
bool state = length == strlen_P(HAStateOn);
_stateCallback(state, this);
}
void HALight::handleBrightnessCommand(const uint8_t* cmd, const uint16_t length)
{
if (!_brightnessCallback) {
return;
}
const HANumeric& number = HANumeric::fromStr(cmd, length);
if (number.isUInt8()) {
_brightnessCallback(number.toUInt8(), this);
}
}
void HALight::handleColorTemperatureCommand(
const uint8_t* cmd,
const uint16_t length
)
{
if (!_colorTemperatureCallback) {
return;
}
const HANumeric& number = HANumeric::fromStr(cmd, length);
if (number.isUInt16()) {
_colorTemperatureCallback(number.toUInt16(), this);
}
}
void HALight::handleRGBCommand(const uint8_t* cmd, const uint16_t length)
{
if (!_rgbColorCallback) {
return;
}
RGBColor color;
color.fromBuffer(cmd, length);
if (color.isSet) {
_rgbColorCallback(color, this);
}
}
#endif

View File

@@ -0,0 +1,421 @@
#ifndef AHA_HALIGHT_H
#define AHA_HALIGHT_H
#include "HABaseDeviceType.h"
#include "../utils/HANumeric.h"
#ifndef EX_ARDUINOHA_LIGHT
#define HALIGHT_STATE_CALLBACK(name) void (*name)(bool state, HALight* sender)
#define HALIGHT_BRIGHTNESS_CALLBACK(name) void (*name)(uint8_t brightness, HALight* sender)
#define HALIGHT_COLOR_TEMP_CALLBACK(name) void (*name)(uint16_t temperature, HALight* sender)
#define HALIGHT_RGB_COLOR_CALLBACK(name) void (*name)(HALight::RGBColor color, HALight* sender)
/**
* HALight allows adding a controllable light in the Home Assistant panel.
* The library supports only the state, brightness, color temperature and RGB color.
* If you need more features please open a new GitHub issue.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/light.mqtt/
*/
class HALight : public HABaseDeviceType
{
public:
static const uint8_t RGBStringMaxLength;
enum Features {
DefaultFeatures = 0,
BrightnessFeature = 1,
ColorTemperatureFeature = 2,
RGBFeature = 4
};
struct RGBColor {
uint8_t red;
uint8_t green;
uint8_t blue;
bool isSet;
RGBColor() :
red(0), green(0), blue(0), isSet(false) { }
RGBColor(uint8_t r, uint8_t g, uint8_t b) :
red(r), green(g), blue(b), isSet(true) { }
void operator= (const RGBColor& a) {
red = a.red;
green = a.green;
blue = a.blue;
isSet = a.isSet;
}
bool operator== (const RGBColor& a) const {
return (
red == a.red &&
green == a.green &&
blue == a.blue
);
}
bool operator!= (const RGBColor& a) const {
return (
red != a.red ||
green != a.green ||
blue != a.blue
);
}
void fromBuffer(const uint8_t* data, const uint16_t length);
};
/**
* @param uniqueId The unique ID of the light. It needs to be unique in a scope of your device.
* @param features Features that should be enabled for the light.
* You can enable multiple features by using OR bitwise operator, for example:
* `HALight::BrightnessFeature | HALight::ColorTemperatureFeature`
*/
HALight(const char* uniqueId, const uint8_t features = DefaultFeatures);
/**
* Changes state of the light 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 light.
* @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 bool state, const bool force = false);
/**
* Changes the brightness of the light 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 brightness The new brightness of the light.
* @param force Forces to update the value without comparing it to a previous known value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setBrightness(const uint8_t brightness, const bool force = false);
/**
* Changes the color temperature of the light 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 temperature The new color temperature of the light.
* @param force Forces to update the value without comparing it to a previous known value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setColorTemperature(const uint16_t temperature, const bool force = false);
/**
* Changes the RGB color of the light and publishes MQTT message.
* Please note that if a new color is the same as previous one,
* the MQTT message won't be published.
*
* @param color The new RGB color of the light.
* @param force Forces to update the value without comparing it to a previous known value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setRGBColor(const RGBColor& color, const bool force = false);
/**
* Alias for `setState(true)`.
*/
inline bool turnOn()
{ return setState(true); }
/**
* Alias for `setState(false)`.
*/
inline bool turnOff()
{ return setState(false); }
/**
* Sets current state of the light 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 light.
*/
inline void setCurrentState(const bool state)
{ _currentState = state; }
/**
* Returns last known state of the light.
* By default it's `false`.
*/
inline bool getCurrentState() const
{ return _currentState; }
/**
* Sets the current brightness of the light without pushing the value to Home Assistant.
* This method may be useful if you want to change the brightness before the connection
* with the MQTT broker is acquired.
*
* @param brightness The new brightness of the light.
*/
inline void setBRIGHTNESS(const uint8_t brightness)
{ _BRIGHTNESS = brightness; }
/**
* Returns the last known brightness of the light.
* By default brightness is set to `0`.
*/
inline uint8_t getBRIGHTNESS() const
{ return _BRIGHTNESS; }
/**
* Sets the current color temperature of the light without pushing the value to Home Assistant.
* This method may be useful if you want to change the color temperature before the connection
* with the MQTT broker is acquired.
*
* @param colorTemp The new color temperature (mireds).
*/
inline void setCurrentColorTemperature(const uint16_t temperature)
{ _currentColorTemperature = temperature; }
/**
* Returns the last known color temperature of the light.
* By default temperature is set to `0`.
*/
inline uint16_t getCurrentColorTemperature() const
{ return _currentColorTemperature; }
/**
* Sets the current RGB color of the light without pushing the value to Home Assistant.
* This method may be useful if you want to change the color before the connection
* with the MQTT broker is acquired.
*
* @param color The new RGB color.
*/
inline void setCurrentRGBColor(const RGBColor& color)
{ _currentRGBColor = color; }
/**
* Returns the last known RGB color of the light.
* By default the RGB color is set to `0,0,0`.
*/
inline const RGBColor& getCurrentRGBColor() const
{ return _currentRGBColor; }
/**
* Sets icon of the light.
* 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 light's 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 light state.
* In this mode the light 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 the maximum brightness value that can be set via HA panel.
* By default it's `255`.
*
* @param scale The maximum value of the brightness.
*/
inline void setBrightnessScale(const uint8_t scale)
{ _brightnessScale.setBaseValue(scale); }
/**
* Sets the minimum color temperature (mireds) value that can be set via HA panel.
* By default it's `153`.
*
* @param mireds The minimum value of the brightness.
*/
inline void setMinMireds(const uint16_t mireds)
{ _minMireds.setBaseValue(mireds); }
/**
* Sets the maximum color temperature (mireds) value that can be set via HA panel.
* By default it's `500`.
*
* @param mireds The maximum value of the brightness.
*/
inline void setMaxMireds(const uint16_t mireds)
{ _maxMireds.setBaseValue(mireds); }
/**
* Registers callback that will be called each time the state command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same light.
*
* @param callback
* @note In non-optimistic mode, the state must be reported back to HA using the HALight::setState method.
*/
inline void onStateCommand(HALIGHT_STATE_CALLBACK(callback))
{ _stateCallback = callback; }
/**
* Registers callback that will be called each time the brightness command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same light.
*
* @param callback
* @note In non-optimistic mode, the brightness must be reported back to HA using the HALight::setBrightness method.
*/
inline void onBrightnessCommand(HALIGHT_BRIGHTNESS_CALLBACK(callback))
{ _brightnessCallback = callback; }
/**
* Registers callback that will be called each time the color temperature command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same light.
*
* @param callback
* @note In non-optimistic mode, the color temperature must be reported back to HA using the HALight::setColorTemperature method.
*/
inline void onColorTemperatureCommand(HALIGHT_COLOR_TEMP_CALLBACK(callback))
{ _colorTemperatureCallback = callback; }
/**
* Registers callback that will be called each time the RGB color command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same light.
*
* @param callback
* @note In non-optimistic mode, the color must be reported back to HA using the HALight::setRGBColor method.
*/
inline void onRGBColorCommand(HALIGHT_RGB_COLOR_CALLBACK(callback))
{ _rgbColorCallback = callback; }
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 bool state);
/**
* Publishes the MQTT message with the given brightness.
*
* @param brightness The brightness to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishBrightness(const uint8_t brightness);
/**
* Publishes the MQTT message with the given color temperature (mireds).
*
* @param temperature The color temperature to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishColorTemperature(const uint16_t temperature);
/**
* Publishes the MQTT message with the given RGB color.
*
* @param color The color to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishRGBColor(const RGBColor& color);
/**
* Parses the given state command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleStateCommand(const uint8_t* cmd, const uint16_t length);
/**
* Parses the given brightness command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleBrightnessCommand(const uint8_t* cmd, const uint16_t length);
/**
* Parses the given color temperature command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleColorTemperatureCommand(const uint8_t* cmd, const uint16_t length);
/**
* Parses the given RGB color command and executes the callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleRGBCommand(const uint8_t* cmd, const uint16_t length);
/// Features enabled for the light.
const uint8_t _features;
/// The icon of the button. It can be nullptr.
const char* _icon;
/// The retain flag for the HA commands.
bool _retain;
/// The optimistic mode of the light (`true` - enabled, `false` - disabled).
bool _optimistic;
/// The maximum value of the brightness. By default it's 255.
HANumeric _brightnessScale;
/// The current state of the light. By default it's `false`.
bool _currentState;
/// The current brightness of the light. By default it's `0`.
uint8_t _BRIGHTNESS;
/// The minimum color temperature (mireds). By default the value is not set.
HANumeric _minMireds;
/// The maximum color temperature (mireds). By default the value is not set.
HANumeric _maxMireds;
/// The current color temperature (mireds). By default the value is not set.
uint16_t _currentColorTemperature;
/// The current RBB color. By default the value is not set.
RGBColor _currentRGBColor;
/// The callback that will be called when the state command is received from the HA.
HALIGHT_STATE_CALLBACK(_stateCallback);
/// The callback that will be called when the brightness command is received from the HA.
HALIGHT_BRIGHTNESS_CALLBACK(_brightnessCallback);
/// The callback that will be called when the color temperature command is received from the HA.
HALIGHT_COLOR_TEMP_CALLBACK(_colorTemperatureCallback);
/// The callback that will be called when the RGB command is received from the HA.
HALIGHT_RGB_COLOR_CALLBACK(_rgbColorCallback);
};
#endif
#endif

View File

@@ -0,0 +1,124 @@
#include "HALock.h"
#ifndef EX_ARDUINOHA_LOCK
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HALock::HALock(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentLock), uniqueId),
_icon(nullptr),
_retain(false),
_optimistic(false),
_currentState(StateUnknown),
_commandCallback(nullptr)
{
}
bool HALock::setState(const LockState state, const bool force)
{
if (!force && state == _currentState) {
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
}
void HALock::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 9); // 9 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
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 HALock::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
if (!_retain) {
publishState(_currentState);
}
subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic));
}
void HALock::onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
)
{
if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HACommandTopic)
)) {
handleCommand(payload, length);
}
}
bool HALock::publishState(const LockState state)
{
if (state == StateUnknown) {
return false;
}
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
AHATOFSTR(state == StateLocked ? HAStateLocked : HAStateUnlocked),
true
);
}
void HALock::handleCommand(const uint8_t* cmd, const uint16_t length)
{
if (!_commandCallback) {
return;
}
if (memcmp_P(cmd, HALockCommand, length) == 0) {
_commandCallback(CommandLock, this);
} else if (memcmp_P(cmd, HAUnlockCommand, length) == 0) {
_commandCallback(CommandUnlock, this);
} else if (memcmp_P(cmd, HAOpenCommand, length) == 0) {
_commandCallback(CommandOpen, this);
}
}
#endif

View File

@@ -0,0 +1,148 @@
#ifndef AHA_HALOCK_H
#define AHA_HALOCK_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_LOCK
#define HALOCK_CALLBACK(name) void (*name)(LockCommand command, HALock* sender)
/**
* HALock allows to implement a custom lock (for example: door lock)
* that can be controlled from the Home Assistant panel.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/lock.mqtt/
*/
class HALock : public HABaseDeviceType
{
public:
/// Available states of the lock that can be reported to the HA panel.
enum LockState {
StateUnknown = 0,
StateLocked,
StateUnlocked
};
/// Commands that will be produced by the HA panel.
enum LockCommand {
CommandLock = 1,
CommandUnlock,
CommandOpen
};
/**
* @param uniqueId The unique ID of the lock. It needs to be unique in a scope of your device.
*/
HALock(const char* uniqueId);
/**
* Changes state of the lock 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 lock.
* @param force Forces to update state without comparing it to a previous known state.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setState(const LockState state, const bool force = false);
/**
* Sets current state of the lock 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 lock.
*/
inline void setCurrentState(const LockState state)
{ _currentState = state; }
/**
* Returns last known state of the lock.
* If setState method wasn't called the initial value will be returned.
*/
inline LockState getCurrentState() const
{ return _currentState; }
/**
* Sets icon of the lock.
* 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 lock's 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 lock state.
* In this mode the lock 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; }
/**
* Registers callback that will be called each time the lock/unlock/open command from the HA is received.
* Please note that it's not possible to register multiple callbacks for the same lock.
*
* @param callback
*/
inline void onCommand(HALOCK_CALLBACK(callback))
{ _commandCallback = callback; }
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 LockState state);
/**
* Parses the given command and executes the lock's callback with proper enum's property.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleCommand(const uint8_t* cmd, const uint16_t length);
/// The icon of the lock. It can be nullptr.
const char* _icon;
/// The retain flag for the HA commands.
bool _retain;
/// The optimistic mode of the lock (`true` - enabled, `false` - disabled).
bool _optimistic;
/// The current state of the lock. By default it's `HALock::StateUnknown`.
LockState _currentState;
/// The callback that will be called when lock/unlock/open command is received from the HA.
HALOCK_CALLBACK(_commandCallback);
};
#endif
#endif

View File

@@ -0,0 +1,214 @@
#include "HANumber.h"
#ifndef EX_ARDUINOHA_NUMBER
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HANumber::HANumber(const char* uniqueId, const NumberPrecision precision) :
HABaseDeviceType(AHATOFSTR(HAComponentNumber), uniqueId),
_precision(precision),
_class(nullptr),
_icon(nullptr),
_retain(false),
_optimistic(false),
_mode(ModeAuto),
_unitOfMeasurement(nullptr),
_minValue(),
_maxValue(),
_step(),
_currentState(),
_commandCallback(nullptr)
{
}
bool HANumber::setState(const HANumeric& state, const bool force)
{
if (!force && state == _currentState) {
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
}
void HANumber::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 15); // 15 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
_serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), _unitOfMeasurement);
_serializer->set(
AHATOFSTR(HAModeProperty),
getModeProperty(),
HASerializer::ProgmemPropertyValue
);
_serializer->set(
AHATOFSTR(HACommandTemplateProperty),
getCommandTemplate(),
HASerializer::ProgmemPropertyValue
);
if (_minValue.isSet()) {
_serializer->set(
AHATOFSTR(HAMinProperty),
&_minValue,
HASerializer::NumberPropertyType
);
}
if (_maxValue.isSet()) {
_serializer->set(
AHATOFSTR(HAMaxProperty),
&_maxValue,
HASerializer::NumberPropertyType
);
}
if (_step.isSet()) {
_serializer->set(
AHATOFSTR(HAStepProperty),
&_step,
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 HANumber::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
if (!_retain) {
publishState(_currentState);
}
subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic));
}
void HANumber::onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
)
{
if (HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HACommandTopic)
)) {
handleCommand(payload, length);
}
}
bool HANumber::publishState(const HANumeric& state)
{
if (!state.isSet()) {
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
AHATOFSTR(HAStateNone),
true
);
}
const uint8_t size = state.calculateSize();
if (size == 0) {
return false;
}
char str[size + 1]; // with null terminator
str[size] = 0;
state.toStr(str);
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
str,
true
);
}
void HANumber::handleCommand(const uint8_t* cmd, const uint16_t length)
{
if (!_commandCallback) {
return;
}
if (memcmp_P(cmd, HAStateNone, length) == 0) {
_commandCallback(HANumeric(), this);
} else {
HANumeric number = HANumeric::fromStr(cmd, length);
if (number.isSet()) {
number.setPrecision(_precision);
_commandCallback(number, this);
}
}
}
const __FlashStringHelper* HANumber::getModeProperty() const
{
switch (_mode) {
case ModeBox:
return AHATOFSTR(HAModeBox);
case ModeSlider:
return AHATOFSTR(HAModeSlider);
default:
return nullptr;
}
}
const __FlashStringHelper* HANumber::getCommandTemplate()
{
switch (_precision) {
case PrecisionP1:
return AHATOFSTR(HAValueTemplateFloatP1);
case PrecisionP2:
return AHATOFSTR(HAValueTemplateFloatP2);
case PrecisionP3:
return AHATOFSTR(HAValueTemplateFloatP3);
default:
return nullptr;
}
}
#endif

View File

@@ -0,0 +1,263 @@
#ifndef AHA_HANUMBER_H
#define AHA_HANUMBER_H
#include "HABaseDeviceType.h"
#include "../utils/HANumeric.h"
#ifndef EX_ARDUINOHA_NUMBER
#define _SET_STATE_OVERLOAD(type) \
/** @overload */ \
inline bool setState(const type state, const bool force = false) \
{ return setState(HANumeric(state, _precision), force); }
#define _SET_CURRENT_STATE_OVERLOAD(type) \
/** @overload */ \
inline void setCurrentState(const type state) \
{ setCurrentState(HANumeric(state, _precision)); }
#define HANUMBER_CALLBACK(name) void (*name)(HANumeric number, HANumber* sender)
/**
* HANumber adds a slider or a box in the Home Assistant panel
* that controls the numeric value stored on your device.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/number.mqtt/
*/
class HANumber : public HABaseDeviceType
{
public:
/// Represents mode of the number.
enum Mode {
ModeAuto = 0,
ModeBox,
ModeSlider
};
/**
* Creates instance of the HANumber entity with the given numbers precision.
* The given precision applies to the state, min, max and step values.
*
* @param uniqueId The unique ID of the number. It needs to be unique in a scope of your device.
* @param precision Precision of the floating point number that will be displayed in the HA panel.
*/
HANumber(const char* uniqueId, const NumberPrecision precision = PrecisionP0);
/**
* Changes state of the number 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 number.
* @param force Forces to update state without comparing it to a previous known state.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setState(const HANumeric& state, const bool force = false);
_SET_STATE_OVERLOAD(int8_t)
_SET_STATE_OVERLOAD(int16_t)
_SET_STATE_OVERLOAD(int32_t)
_SET_STATE_OVERLOAD(uint8_t)
_SET_STATE_OVERLOAD(uint16_t)
_SET_STATE_OVERLOAD(uint32_t)
_SET_STATE_OVERLOAD(float)
#ifdef __SAMD21G18A__
_SET_STATE_OVERLOAD(int)
#endif
/**
* Sets current state of the number 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 number.
*/
inline void setCurrentState(const HANumeric& state)
{ if (state.getPrecision() == _precision) { _currentState = state; } }
_SET_CURRENT_STATE_OVERLOAD(int8_t)
_SET_CURRENT_STATE_OVERLOAD(int16_t)
_SET_CURRENT_STATE_OVERLOAD(int32_t)
_SET_CURRENT_STATE_OVERLOAD(uint8_t)
_SET_CURRENT_STATE_OVERLOAD(uint16_t)
_SET_CURRENT_STATE_OVERLOAD(uint32_t)
_SET_CURRENT_STATE_OVERLOAD(float)
#ifdef __SAMD21G18A__
_SET_CURRENT_STATE_OVERLOAD(int)
#endif
/**
* Returns last known state of the number.
* If setState method wasn't called the initial value will be returned.
*/
inline const HANumeric& getCurrentState() const
{ return _currentState; }
/**
* Sets class of the device.
* You can find list of available values here: https://www.home-assistant.io/integrations/number/#device-class
*
* @param deviceClass The class name.
*/
inline void setDeviceClass(const char* deviceClass)
{ _class = deviceClass; }
/**
* Sets icon of the number.
* 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 number's 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 number state.
* In this mode the number 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 number.
* It controls how the number should be displayed in the UI.
* By default it's `HANumber::ModeAuto`.
*
* @param mode Mode to set.
*/
inline void setMode(const Mode mode)
{ _mode = mode; }
/**
* Defines the units of measurement of the number, if any.
*
* @param units For example: °C, %
*/
inline void setUnitOfMeasurement(const char* unitOfMeasurement)
{ _unitOfMeasurement = unitOfMeasurement; }
/**
* Sets the minimum value that can be set from the Home Assistant panel.
*
* @param min The minimal value. By default the value is not set.
*/
inline void setMin(const float min)
{ _minValue = HANumeric(min, _precision); }
/**
* Sets the maximum value that can be set from the Home Assistant panel.
*
* @param min The maximum value. By default the value is not set.
*/
inline void setMax(const float max)
{ _maxValue = HANumeric(max, _precision); }
/**
* Sets step of the slider's movement in the Home Assistant panel.
*
* @param step The step value. Smallest value `0.001`. By default the value is not set.
*/
inline void setStep(const float step)
{ _step = HANumeric(step, _precision); }
/**
* Registers callback that will be called each time the number is changed in the HA panel.
* Please note that it's not possible to register multiple callbacks for the same number.
*
* @param callback
* @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; }
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 HANumeric& state);
/**
* Parses the given command and executes the number's callback with proper value.
*
* @param cmd The data of the command.
* @param length Length of the command.
*/
void handleCommand(const uint8_t* cmd, const uint16_t length);
/**
* Returns progmem string representing mode of the number
*/
const __FlashStringHelper* getModeProperty() const;
/**
* Returns progmem string representing value template for the command.
*/
const __FlashStringHelper* getCommandTemplate();
/// The precision of the number. By default it's `HANumber::PrecisionP0`.
const NumberPrecision _precision;
/// The device class. It can be nullptr.
const char* _class;
/// The icon of the number. It can be nullptr.
const char* _icon;
/// The retain flag for the HA commands.
bool _retain;
/// The optimistic mode of the number (`true` - enabled, `false` - disabled).
bool _optimistic;
/// Controls how the number should be displayed in the UI. By default it's `HANumber::ModeAuto`.
Mode _mode;
/// The unit of measurement for the sensor. It can be nullptr.
const char* _unitOfMeasurement;
/// The minimal value that can be set from the HA panel.
HANumeric _minValue;
/// The maximum value that can be set from the HA panel.
HANumeric _maxValue;
/// The step of the slider's movement.
HANumeric _step;
/// The current state of the number. By default the value is not set.
HANumeric _currentState;
/// The callback that will be called when the command is received from the HA.
HANUMBER_CALLBACK(_commandCallback);
};
#endif
#endif

View File

@@ -0,0 +1,76 @@
#include "HAScene.h"
#ifndef EX_ARDUINOHA_SCENE
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HAScene::HAScene(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentScene), uniqueId),
_icon(nullptr),
_retain(false),
_commandCallback(nullptr)
{
}
void HAScene::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 7); // 7 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
// optional property
if (_retain) {
_serializer->set(
AHATOFSTR(HARetainProperty),
&_retain,
HASerializer::BoolPropertyType
);
}
// HA 2022.10 throws an exception if this property is not set
_serializer->set(
AHATOFSTR(HAPayloadOnProperty),
AHATOFSTR(HAStateOn),
HASerializer::ProgmemPropertyValue
);
_serializer->set(HASerializer::WithAvailability);
_serializer->topic(AHATOFSTR(HACommandTopic));
}
void HAScene::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic));
}
void HAScene::onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
)
{
(void)payload;
(void)length;
if (_commandCallback && HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HACommandTopic)
)) {
_commandCallback(this);
}
}
#endif

View File

@@ -0,0 +1,73 @@
#ifndef AHA_HASCENE_H
#define AHA_HASCENE_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_SCENE
#define HASCENE_CALLBACK(name) void (*name)(HAScene* sender)
/**
* HAScene adds a new scene to the Home Assistant that triggers your callback once activated.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/scene.mqtt/
*/
class HAScene : public HABaseDeviceType
{
public:
/**
* @param uniqueId The unique ID of the scene. It needs to be unique in a scope of your device.
*/
HAScene(const char* uniqueId);
/**
* Sets icon of the scene.
* 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 scene's command.
* If set to `true` the command produced by Home Assistant will be retained.
*
* @param retain
*/
inline void setRetain(const bool retain)
{ _retain = retain; }
/**
* Registers callback that will be called when the scene is activated in the HA panel.
* Please note that it's not possible to register multiple callbacks for the same scene.
*
* @param callback
*/
inline void onCommand(HASCENE_CALLBACK(callback))
{ _commandCallback = callback; }
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:
/// The icon of the scene. It can be nullptr.
const char* _icon;
/// The retain flag for the HA commands.
bool _retain;
/// The command callback that will be called when scene is activated from the HA panel.
HASCENE_CALLBACK(_commandCallback);
};
#endif
#endif

View File

@@ -0,0 +1,198 @@
#include "HASelect.h"
#ifndef EX_ARDUINOHA_SELECT
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HASelect::HASelect(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentSelect), uniqueId),
_options(nullptr),
_currentState(-1),
_icon(nullptr),
_retain(false),
_optimistic(false),
_commandCallback(nullptr)
{
}
HASelect::~HASelect()
{
if (_options) {
const uint8_t optionsNb = _options->getItemsNb();
const HASerializerArray::ItemType* options = _options->getItems();
if (optionsNb > 1) {
for (uint8_t i = 0; i < optionsNb; i++) {
delete options[i];
}
}
delete _options;
}
}
void HASelect::setOptions(const char* options)
{
if (!options || _options) { // options can be set only once
return;
}
const uint16_t optionsNb = countOptionsInString(options);
if (optionsNb == 0) {
return;
}
const uint16_t optionsLen = strlen(options) + 1; // include null terminator
_options = new HASerializerArray(optionsNb, false);
if (optionsNb == 1) {
_options->add(options);
return;
}
uint8_t optionLen = 0;
for (uint16_t i = 0; i < optionsLen; i++) {
if (options[i] == ';' || options[i] == 0) {
if (optionLen == 0) {
break;
}
char* option = new char[optionLen + 1]; // including null terminator
option[optionLen] = 0;
memcpy(option, &options[i - optionLen], optionLen);
_options->add(option);
optionLen = 0;
continue;
}
optionLen++;
}
}
bool HASelect::setState(const int8_t state, const bool force)
{
if (!force && _currentState == state) {
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
}
void HASelect::buildSerializer()
{
if (_serializer || !uniqueId() || !_options) {
return;
}
_serializer = new HASerializer(this, 10); // 10 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
_serializer->set(
AHATOFSTR(HAOptionsProperty),
_options,
HASerializer::ArrayPropertyType
);
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 HASelect::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
if (!_retain) {
publishState(_currentState);
}
subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic));
}
void HASelect::onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
)
{
if (_commandCallback && HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HACommandTopic)
)) {
const uint8_t optionsNb = _options->getItemsNb();
const HASerializerArray::ItemType* options = _options->getItems();
for (uint8_t i = 0; i < optionsNb; i++) {
if (memcmp(payload, options[i], length) == 0) {
_commandCallback(i, this);
return;
}
}
}
}
bool HASelect::publishState(const int8_t state)
{
if (state == -1 || !_options || state >= _options->getItemsNb()) {
return false;
}
const char* item = _options->getItems()[state];
if (!item) {
return false;
}
return publishOnDataTopic(AHATOFSTR(HAStateTopic), item, true);
}
uint8_t HASelect::countOptionsInString(const char* options) const
{
// the given string is treated as a single option if there are no semicolons
uint8_t optionsNb = 1;
const uint16_t optionsLen = strlen(options);
if (optionsLen == 0) {
return 0;
}
for (uint8_t i = 0; i < optionsLen; i++) {
if (options[i] == ';') {
optionsNb++;
}
}
return optionsNb;
}
#endif

View File

@@ -0,0 +1,155 @@
#ifndef AHA_HASELECT_H
#define AHA_HASELECT_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_SELECT
class HASerializerArray;
#define HASELECT_CALLBACK(name) void (*name)(int8_t index, HASelect* sender)
/**
* HASelect adds a dropdown with options 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/button.mqtt/
*/
class HASelect : public HABaseDeviceType
{
public:
/**
* @param uniqueId The unique ID of the select. It needs to be unique in a scope of your device.
*/
HASelect(const char* uniqueId);
~HASelect();
/**
* Sets the list of available options that will be listed in the dropdown.
* The input string should contain options separated using semicolons.
* For example: `setOptions("Option A;Option B;Option C");
*
* @param options The list of options that are separated by semicolons.
* @note The options list can be set only once.
*/
void setOptions(const char* options);
/**
* Changes state of the select and publishes MQTT message.
* State represents the index of the option that was set using the setOptions method.
* 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 select.
* @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 int8_t state, const bool force = false);
/**
* Sets the current state of the select without publishing it to Home Assistant.
* State represents the index of the option that was set using the setOptions method.
* This method may be useful if you want to change the state before the connection
* with the MQTT broker is acquired.
*
* @param state The new state of the cover.
*/
inline void setCurrentState(const int8_t state)
{ _currentState = state; }
/**
* Returns last known state of the select.
* State represents the index of the option that was set using the setOptions method.
* By default the state is set to `-1`.
*/
inline int8_t getCurrentState() const
{ return _currentState; }
/**
* Sets icon of the select.
* 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 select's 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 select state.
* In this mode the select 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; }
/**
* Registers callback that will be called each time the option is changed from the HA panel.
* Please note that it's not possible to register multiple callbacks for the same select.
*
* @param callback
* @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; }
#ifdef ARDUINOHA_TEST
inline HASerializerArray* getOptions() const
{ return _options; }
#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 int8_t state);
/**
* Counts the amount of options in the given string.
*/
uint8_t countOptionsInString(const char* options) const;
/// Array of options for the serializer.
HASerializerArray* _options;
/// Stores the current state (the current option's index). By default it's `-1`.
int8_t _currentState;
/// The icon of the select. It can be nullptr.
const char* _icon;
/// The retain flag for the HA commands.
bool _retain;
/// The optimistic mode of the select (`true` - enabled, `false` - disabled).
bool _optimistic;
/// The command callback that will be called when option is changed via the HA panel.
HASELECT_CALLBACK(_commandCallback);
};
#endif
#endif

View File

@@ -0,0 +1,59 @@
#include "HASensor.h"
#ifndef EX_ARDUINOHA_SENSOR
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HASensor::HASensor(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentSensor), uniqueId),
_deviceClass(nullptr),
_forceUpdate(false),
_icon(nullptr),
_unitOfMeasurement(nullptr)
{
}
bool HASensor::setValue(const char* value)
{
return publishOnDataTopic(AHATOFSTR(HAStateTopic), value, true);
}
void HASensor::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 9); // 9 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _deviceClass);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
_serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), _unitOfMeasurement);
// optional property
if (_forceUpdate) {
_serializer->set(
AHATOFSTR(HAForceUpdateProperty),
&_forceUpdate,
HASerializer::BoolPropertyType
);
}
_serializer->set(HASerializer::WithDevice);
_serializer->set(HASerializer::WithAvailability);
_serializer->topic(AHATOFSTR(HAStateTopic));
}
void HASensor::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
}
#endif

View File

@@ -0,0 +1,89 @@
#ifndef AHA_HASENSOR_H
#define AHA_HASENSOR_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_SENSOR
/**
* HASensor allows to publish textual sensor values that will be displayed in the HA panel.
* If you need to publish numbers then HASensorNumber is what you're looking for.
*
* @note It's not possible to define a sensor that publishes mixed values (e.g. string + integer values).
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/sensor.mqtt/
*/
class HASensor : public HABaseDeviceType
{
public:
/**
* @param uniqueId The unique ID of the sensor. It needs to be unique in a scope of your device.
*/
HASensor(const char* uniqueId);
/**
* Publishes the MQTT message with the given value.
* Unlike the other device types, the HASensor doesn't store the previous value that was set.
* It means that the MQTT message is produced each time the setValue method is called.
*
* @param value String representation of the sensor's value.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool setValue(const char* value);
/**
* Sets class of the device.
* You can find list of available values here: https://www.home-assistant.io/integrations/sensor/#device-class
*
* @param deviceClass The class name.
*/
inline void setDeviceClass(const char* deviceClass)
{ _deviceClass = deviceClass; }
/**
* Forces HA panel to process each incoming value (MQTT message).
* It's useful if you want to have meaningful value graphs in history.
*
* @param forceUpdate
*/
inline void setForceUpdate(bool forceUpdate)
{ _forceUpdate = forceUpdate; }
/**
* Sets icon of the sensor.
* Any icon from MaterialDesignIcons.com (for example: `mdi:home`).
*
* @param class The icon name.
*/
inline void setIcon(const char* icon)
{ _icon = icon; }
/**
* Defines the units of measurement of the sensor, if any.
*
* @param units For example: °C, %
*/
inline void setUnitOfMeasurement(const char* unitOfMeasurement)
{ _unitOfMeasurement = unitOfMeasurement; }
protected:
virtual void buildSerializer() override final;
virtual void onMqttConnected() override;
private:
/// The device class. It can be nullptr.
const char* _deviceClass;
/// The force update flag for the HA panel.
bool _forceUpdate;
/// The icon of the sensor. It can be nullptr.
const char* _icon;
/// The unit of measurement for the sensor. It can be nullptr.
const char* _unitOfMeasurement;
};
#endif
#endif

View File

@@ -0,0 +1,67 @@
#include "HASensorNumber.h"
#ifndef EX_ARDUINOHA_SENSOR
#include "../utils/HASerializer.h"
HASensorNumber::HASensorNumber(
const char* uniqueId,
const NumberPrecision precision
) :
HASensor(uniqueId),
_precision(precision),
_currentValue()
{
}
bool HASensorNumber::setValue(const HANumeric& value, const bool force)
{
if (value.getPrecision() != _precision) {
return false;
}
if (!force && value == _currentValue) {
return true;
}
if (publishValue(value)) {
_currentValue = value;
return true;
}
return false;
}
void HASensorNumber::onMqttConnected()
{
if (!uniqueId()) {
return;
}
HASensor::onMqttConnected();
publishValue(_currentValue);
}
bool HASensorNumber::publishValue(const HANumeric& value)
{
if (!value.isSet()) {
return false;
}
uint8_t size = value.calculateSize();
if (size == 0) {
return false;
}
char str[size + 1]; // with null terminator
str[size] = 0;
value.toStr(str);
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
str,
true
);
}
#endif

View File

@@ -0,0 +1,106 @@
#ifndef AHA_HASENSORNUMBER_H
#define AHA_HASENSORNUMBER_H
#include "HASensor.h"
#include "../utils/HANumeric.h"
#ifndef EX_ARDUINOHA_SENSOR
#define _SET_VALUE_OVERLOAD(type) \
/** @overload */ \
inline bool setValue(const type value, const bool force = false) \
{ return setValue(HANumeric(value, _precision), force); }
#define _SET_CURRENT_VALUE_OVERLOAD(type) \
/** @overload */ \
inline void setCurrentValue(const type value) \
{ setCurrentValue(HANumeric(value, _precision)); }
/**
* HASensorInteger allows to publish numeric values of a sensor that will be displayed in the HA panel.
*
* @note You can find more information about this class in HASensor documentation.
*/
class HASensorNumber : public HASensor
{
public:
/**
* @param uniqueId The unique ID of the sensor. It needs to be unique in a scope of your device.
* @param precision Precision of the floating point number that will be displayed in the HA panel.
*/
HASensorNumber(
const char* uniqueId,
const NumberPrecision precision = PrecisionP0
);
/**
* Changes value of the sensor and publish MQTT message.
* Please note that if a new value is the same as the previous one the MQTT message won't be published.
*
* @param value New value of the sensor. THe precision of the value needs to match precision of the sensor.
* @param force Forces to update the value without comparing it to a previous known value.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool setValue(const HANumeric& value, const bool force = false);
_SET_VALUE_OVERLOAD(int8_t)
_SET_VALUE_OVERLOAD(int16_t)
_SET_VALUE_OVERLOAD(int32_t)
_SET_VALUE_OVERLOAD(uint8_t)
_SET_VALUE_OVERLOAD(uint16_t)
_SET_VALUE_OVERLOAD(uint32_t)
_SET_VALUE_OVERLOAD(float)
#ifdef __SAMD21G18A__
_SET_VALUE_OVERLOAD(int)
#endif
/**
* Sets the current value of the sensor without publishing it to Home Assistant.
* This method may be useful if you want to change the value before the connection with the MQTT broker is acquired.
*
* @param value New value of the sensor.
*/
inline void setCurrentValue(const HANumeric& value)
{ if (value.getPrecision() == _precision) { _currentValue = value; } }
_SET_CURRENT_VALUE_OVERLOAD(int8_t)
_SET_CURRENT_VALUE_OVERLOAD(int16_t)
_SET_CURRENT_VALUE_OVERLOAD(int32_t)
_SET_CURRENT_VALUE_OVERLOAD(uint8_t)
_SET_CURRENT_VALUE_OVERLOAD(uint16_t)
_SET_CURRENT_VALUE_OVERLOAD(uint32_t)
_SET_CURRENT_VALUE_OVERLOAD(float)
#ifdef __SAMD21G18A__
_SET_CURRENT_VALUE_OVERLOAD(int)
#endif
/**
* Returns the last known value of the sensor.
* By default the value is not set.
*/
inline const HANumeric& getCurrentValue() const
{ return _currentValue; }
protected:
virtual void onMqttConnected() override;
private:
/**
* Publishes the MQTT message with the given value.
*
* @param state The value to publish.
* @returns Returns `true` if the MQTT message has been published successfully.
*/
bool publishValue(const HANumeric& value);
/// The precision of the sensor. By default it's `HASensorNumber::PrecisionP0`.
const NumberPrecision _precision;
/// The current value of the sensor. By default the value is not set.
HANumeric _currentValue;
};
#endif
#endif

View File

@@ -0,0 +1,111 @@
#include "HASwitch.h"
#ifndef EX_ARDUINOHA_SWITCH
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HASwitch::HASwitch(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentSwitch), uniqueId),
_class(nullptr),
_icon(nullptr),
_retain(false),
_optimistic(false),
_currentState(false),
_commandCallback(nullptr)
{
}
bool HASwitch::setState(const bool state, const bool force)
{
if (!force && state == _currentState) {
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
}
void HASwitch::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 10); // 10 - max properties nb
_serializer->set(AHATOFSTR(HANameProperty), _name);
_serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
// optional property
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 HASwitch::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
publishAvailability();
if (!_retain) {
publishState(_currentState);
}
subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic));
}
void HASwitch::onMqttMessage(
const char* topic,
const uint8_t* payload,
const uint16_t length
)
{
(void)payload;
if (_commandCallback && HASerializer::compareDataTopics(
topic,
uniqueId(),
AHATOFSTR(HACommandTopic)
)) {
bool state = length == strlen_P(HAStateOn);
_commandCallback(state, this);
}
}
bool HASwitch::publishState(const bool state)
{
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
AHATOFSTR(state ? HAStateOn : HAStateOff),
true
);
}
#endif

View File

@@ -0,0 +1,150 @@
#ifndef AHA_HASWITCH_H
#define AHA_HASWITCH_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_SWITCH
#define HASWITCH_CALLBACK(name) void (*name)(bool state, HASwitch* sender)
/**
* HASwitch allows to display on/off switch in the HA panel and receive commands on your device.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/switch.mqtt/
*/
class HASwitch : public HABaseDeviceType
{
public:
/**
* @param uniqueId The unique ID of the sensor. It needs to be unique in a scope of your device.
*/
HASwitch(const char* uniqueId);
/**
* Changes state of the switch 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 switch.
* @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 bool state, const bool force = false);
/**
* Alias for `setState(true)`.
*/
inline bool turnOn()
{ return setState(true); }
/**
* Alias for `setState(false)`.
*/
inline bool turnOff()
{ return setState(false); }
/**
* Sets current state of the switch 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 switch.
*/
inline void setCurrentState(const bool state)
{ _currentState = state; }
/**
* Returns last known state of the switch.
* By default it's `false`.
*/
inline bool getCurrentState() const
{ return _currentState; }
/**
* Sets class of the device.
* You can find list of available values here: https://www.home-assistant.io/integrations/switch/#device-class
*
* @param deviceClass The class name.
*/
inline void setDeviceClass(const char* deviceClass)
{ _class = deviceClass; }
/**
* Sets icon of the sensor.
* 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 switch 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 switch state.
* In this mode the switch 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; }
/**
* Registers callback that will be called each time the on/off command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same switch.
*
* @param callback
* @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; }
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 bool state);
/// The device class. It can be nullptr.
const char* _class;
/// The icon of the button. It can be nullptr.
const char* _icon;
/// The retain flag for the HA commands.
bool _retain;
/// The optimistic mode of the switch (`true` - enabled, `false` - disabled).
bool _optimistic;
/// The current state of the switch. By default it's `false`.
bool _currentState;
/// The callback that will be called when switch command is received from the HA.
HASWITCH_CALLBACK(_commandCallback);
};
#endif
#endif

View File

@@ -0,0 +1,42 @@
#include "HATagScanner.h"
#ifndef EX_ARDUINOHA_TAG_SCANNER
#include "../HAMqtt.h"
#include "../utils/HASerializer.h"
HATagScanner::HATagScanner(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentTag), uniqueId)
{
}
bool HATagScanner::tagScanned(const char* tag)
{
if (!tag || strlen(tag) == 0) {
return false;
}
return publishOnDataTopic(AHATOFSTR(HATopic), tag);
}
void HATagScanner::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 2); // 2 - max properties nb
_serializer->set(HASerializer::WithDevice);
_serializer->topic(AHATOFSTR(HATopic));
}
void HATagScanner::onMqttConnected()
{
if (!uniqueId()) {
return;
}
publishConfig();
}
#endif

View File

@@ -0,0 +1,38 @@
#ifndef AHA_HATAGSCANNER_H
#define AHA_HATAGSCANNER_H
#include "HABaseDeviceType.h"
#ifndef EX_ARDUINOHA_TAG_SCANNER
/**
* HATagScanner allow to produce scan events that can be used in the HA automation.
*
* @note
* You can find more information about this entity in the Home Assistant documentation:
* https://www.home-assistant.io/integrations/tag.mqtt/
*/
class HATagScanner : public HABaseDeviceType
{
public:
/**
* @param uniqueId The unique ID of the scanner. It needs to be unique in a scope of your device.
*/
HATagScanner(const char* uniqueId);
/**
* Sends "tag scanned" event to the MQTT (Home Assistant).
* Based on this event HA may perform user-defined automation.
*
* @param tag Value of the scanned tag.
* @returns Returns `true` if MQTT message has been published successfully.
*/
bool tagScanned(const char* tag);
protected:
virtual void buildSerializer() override;
virtual void onMqttConnected() override;
};
#endif
#endif

View File

@@ -0,0 +1,102 @@
#ifndef AHA_AUNITHELPERS_H
#define AHA_AUNITHELPERS_H
#ifdef ARDUINOHA_TEST
#ifdef AUNITER
#include <Arduino.h>
#if defined(__AVR__)
#include <MemoryUsage.h>
#endif
#endif
#define initMqttTest(testDeviceId) \
PubSubClientMock* mock = new PubSubClientMock(); \
HADevice device(testDeviceId); \
HAMqtt mqtt(mock, device); \
mqtt.setDataPrefix("testData"); \
mqtt.begin("testHost", "testUser", "testPass");
#define assertNoMqttMessage() \
assertTrue(mock->getFlushedMessagesNb() == 0);
#define assertMqttMessage(index, eTopic, eMessage, eRetained) { \
const __FlashStringHelper* messageP = F(eMessage); \
size_t messageLen = strlen_P(reinterpret_cast<const char *>(messageP)); \
assertTrue(mock->getFlushedMessagesNb() > 0); \
assertTrue(mock->getFlushedMessagesNb() > index); \
MqttMessage* publishedMessage = mock->getFlushedMessages()[index]; \
assertEqual(eTopic, publishedMessage->topic); \
assertEqual(messageP, publishedMessage->buffer); \
assertEqual(messageLen, publishedMessage->bufferSize - 1); \
assertEqual(eRetained, publishedMessage->retained); \
}
#define assertSingleMqttMessage(eTopic, eMessage, eRetained) { \
assertEqual(1, mock->getFlushedMessagesNb()); \
assertMqttMessage(0, eTopic, eMessage, eRetained) \
}
#define assertEntityConfig(mock, entity, expectedJson) \
{ \
mqtt.loop(); \
assertMqttMessage(0, AHATOFSTR(ConfigTopic), expectedJson, true) \
assertTrue(entity.getSerializer() == nullptr); \
}
#define assertEntityConfigOnTopic(mock, entity, topic, expectedJson) \
{ \
mqtt.loop(); \
assertMqttMessage(0, topic, expectedJson, true) \
assertTrue(entity.getSerializer() == nullptr); \
}
#ifdef AUNITER
#if defined(__AVR__)
#define AHA_FREERAM mu_freeRam()
#elif defined(ESP8266) || defined(ESP32)
#define AHA_FREERAM ESP.getFreeHeap()
#else
#define AHA_FREERAM 0
#endif
#define AHA_LEAKTRACKSTART \
int freeRam = AHA_FREERAM;
#define AHA_LEAKTRACKEND \
int diff = freeRam - AHA_FREERAM; \
if (diff < 0) { diff *= -1; } \
if (diff != 0) { \
Serial.print(Test::getName().getFString()); \
Serial.print(F(" memory leak: ")); \
Serial.print(diff); \
Serial.println(F("b")); \
Test::fail(); \
}
#else
// EpoxyDuino doesn't support memory tracking
#define AHA_LEAKTRACKSTART
#define AHA_LEAKTRACKEND
#endif
#define AHA_TEST(suiteName, name) \
class suiteName##_##name : public aunit::TestOnce { \
public: \
suiteName##_##name(); \
void once() override; \
void loop() override { \
AHA_LEAKTRACKSTART \
once(); \
if (isNotDone()) { pass(); } \
AHA_LEAKTRACKEND \
} \
} suiteName##_##name##_instance; \
suiteName##_##name :: suiteName##_##name() { \
init(AUNIT_F(#suiteName "_" #name)); \
} \
void suiteName##_##name :: once()
#endif
#endif

View File

@@ -0,0 +1,268 @@
#include "PubSubClientMock.h"
#ifdef ARDUINOHA_TEST
#include "../ArduinoHADefines.h"
PubSubClientMock::PubSubClientMock() :
_pendingMessage(nullptr),
_flushedMessages(nullptr),
_flushedMessagesNb(0),
_subscriptions(nullptr),
_subscriptionsNb(0),
callback(nullptr)
{
}
PubSubClientMock::~PubSubClientMock()
{
if (_pendingMessage) {
delete _pendingMessage;
}
clearFlushedMessages();
clearSubscriptions();
}
bool PubSubClientMock::loop()
{
return connected();
}
void PubSubClientMock::disconnect()
{
_connection.connected = false;
}
bool PubSubClientMock::connected()
{
return _connection.connected;
}
bool PubSubClientMock::connect(
const char *id,
const char *user,
const char *pass,
const char* willTopic,
uint8_t willQos,
bool willRetain,
const char* willMessage,
bool cleanSession
)
{
(void)willQos;
(void)cleanSession;
_connection.connected = true;
_connection.id = id;
_connection.user = user;
_connection.pass = pass;
_lastWill.topic = willTopic;
_lastWill.message = willMessage;
_lastWill.retain = willRetain;
return true;
}
bool PubSubClientMock::connectDummy()
{
_connection.connected = true;
_connection.id = "dummyId";
_connection.user = nullptr;
_connection.pass = nullptr;
_lastWill.topic = nullptr;
_lastWill.message = nullptr;
_lastWill.retain = false;
return true;
}
PubSubClientMock& PubSubClientMock::setServer(IPAddress ip, uint16_t port)
{
_connection.ip = ip;
_connection.port = port;
return *this;
}
PubSubClientMock& PubSubClientMock::setServer(
const char * domain,
uint16_t port
)
{
_connection.domain = domain;
_connection.port = port;
return *this;
}
PubSubClientMock& PubSubClientMock::setCallback(MQTT_CALLBACK_SIGNATURE)
{
this->callback = callback;
return *this;
}
bool PubSubClientMock::beginPublish(
const char* topic,
unsigned int plength,
bool retained
)
{
if (!connected()) {
return false;
}
if (_pendingMessage) {
delete _pendingMessage;
}
_pendingMessage = new MqttMessage();
_pendingMessage->retained = retained;
{
size_t size = strlen(topic) + 1;
_pendingMessage->topic = new char[size];
_pendingMessage->topicSize = size;
memset(_pendingMessage->topic, 0, size);
memcpy(_pendingMessage->topic, topic, size);
}
{
size_t size = plength + 1;
_pendingMessage->buffer = new char[size];
_pendingMessage->bufferSize = size;
memset(_pendingMessage->buffer, 0, size);
}
return true;
}
size_t PubSubClientMock::write(const uint8_t *buffer, size_t size)
{
if (!_pendingMessage || !_pendingMessage->buffer) {
return 0;
}
strncat(_pendingMessage->buffer, (const char*)buffer, size);
return size;
}
size_t PubSubClientMock::print(const __FlashStringHelper* buffer)
{
const size_t len = strlen_P(reinterpret_cast<const char*>(buffer));
char data[len + 1]; // including null terminator
strcpy_P(data, reinterpret_cast<const char*>(buffer));
return write((const uint8_t*)(data), len);
}
int PubSubClientMock::endPublish()
{
if (!_pendingMessage) {
return 0;
}
size_t messageSize = _pendingMessage->bufferSize;
uint8_t index = _flushedMessagesNb;
_flushedMessagesNb++;
_flushedMessages = static_cast<MqttMessage**>(
realloc(_flushedMessages, _flushedMessagesNb * sizeof(MqttMessage*))
);
_flushedMessages[index] = _pendingMessage; // handover memory responsibility
_pendingMessage = nullptr; // do not call destructor
return messageSize;
}
bool PubSubClientMock::subscribe(const char* topic)
{
uint8_t index = _subscriptionsNb;
_subscriptionsNb++;
_subscriptions = static_cast<MqttSubscription**>(
realloc(_subscriptions, _subscriptionsNb * sizeof(MqttSubscription*))
);
size_t topicSize = strlen(topic) + 1;
MqttSubscription* subscription = new MqttSubscription();
subscription->topic = new char[topicSize];
memcpy(subscription->topic, topic, topicSize);
_subscriptions[index] = subscription;
return true;
}
void PubSubClientMock::clearFlushedMessages()
{
if (_flushedMessages) {
for (uint8_t i = 0; i < _flushedMessagesNb; i++) {
delete _flushedMessages[i];
}
delete _flushedMessages;
}
_flushedMessagesNb = 0;
}
void PubSubClientMock::clearSubscriptions()
{
if (_subscriptions) {
for (uint8_t i = 0; i < _subscriptionsNb; i++) {
delete _subscriptions[i];
}
delete _subscriptions;
}
_subscriptionsNb = 0;
}
void PubSubClientMock::fakeMessage(const char* topic, const char* message)
{
if (!callback) {
return;
}
uint16_t len = strlen(message);
uint8_t data[len];
memcpy(data, message, len);
callback(const_cast<char*>(topic), data, len);
}
void PubSubClientMock::fakeMessage(
const __FlashStringHelper* topic,
const char* message
)
{
char topicStr[strlen_P(AHAFROMFSTR(topic)) + 1];
topicStr[0] = 0;
strcpy_P(topicStr, AHAFROMFSTR(topic));
fakeMessage(topicStr, message);
}
void PubSubClientMock::fakeMessage(
const __FlashStringHelper* topic,
const __FlashStringHelper* message
)
{
char topicStr[strlen_P(AHAFROMFSTR(topic)) + 1];
topicStr[0] = 0;
strcpy_P(topicStr, AHAFROMFSTR(topic));
char messageStr[strlen_P(AHAFROMFSTR(message)) + 1];
messageStr[0] = 0;
strcpy_P(messageStr, AHAFROMFSTR(message));
fakeMessage(topicStr, messageStr);
}
#endif

View File

@@ -0,0 +1,166 @@
#ifndef AHA_PUBSUBCLIENTMOCK_H
#define AHA_PUBSUBCLIENTMOCK_H
#ifdef ARDUINOHA_TEST
#include <Arduino.h>
#include <IPAddress.h>
#if defined(ESP8266) || defined(ESP32)
#include <functional>
#define MQTT_CALLBACK_SIGNATURE std::function<void(char*, uint8_t*, unsigned int)> callback
#else
#define MQTT_CALLBACK_SIGNATURE void (*callback)(char*, uint8_t*, unsigned int)
#endif
struct MqttMessage
{
char* topic;
size_t topicSize;
char* buffer;
size_t bufferSize;
bool retained;
MqttMessage() :
topic(nullptr),
topicSize(0),
buffer(nullptr),
bufferSize(0),
retained(false)
{
}
~MqttMessage()
{
if (topic) {
delete topic;
}
if (buffer) {
delete buffer;
}
}
};
struct MqttSubscription {
char* topic;
MqttSubscription() :
topic(nullptr)
{
}
~MqttSubscription()
{
if (topic) {
delete topic;
}
}
};
struct MqttConnection
{
bool connected;
const char* domain;
IPAddress ip;
uint16_t port;
const char* id;
const char* user;
const char* pass;
MqttConnection() :
connected(false),
domain(nullptr),
port(0),
id(nullptr),
user(nullptr),
pass(nullptr)
{
}
};
struct MqttWill
{
const char* topic;
const char* message;
bool retain;
MqttWill() :
topic(nullptr),
message(nullptr),
retain(false)
{
}
};
class PubSubClientMock
{
public:
PubSubClientMock();
~PubSubClientMock();
bool loop();
void disconnect();
bool connected();
bool connect(
const char *id,
const char *user,
const char *pass,
const char* willTopic,
uint8_t willQos,
bool willRetain,
const char* willMessage,
bool cleanSession
);
bool connectDummy();
PubSubClientMock& setServer(IPAddress ip, uint16_t port);
PubSubClientMock& setServer(const char* domain, uint16_t port);
PubSubClientMock& setCallback(MQTT_CALLBACK_SIGNATURE);
bool beginPublish(const char* topic, unsigned int plength, bool retained);
size_t write(const uint8_t *buffer, size_t size);
size_t print(const __FlashStringHelper* buffer);
int endPublish();
bool subscribe(const char* topic);
inline uint8_t getFlushedMessagesNb() const
{ return _flushedMessagesNb; }
inline MqttMessage** getFlushedMessages() const
{ return _flushedMessages; }
inline uint8_t getSubscriptionsNb() const
{ return _subscriptionsNb; }
inline MqttSubscription** getSubscriptions() const
{ return _subscriptions; }
inline const MqttConnection& getConnection() const
{ return _connection; }
inline const MqttWill& getLastWill() const
{ return _lastWill; }
void clearFlushedMessages();
void clearSubscriptions();
void fakeMessage(const char* topic, const char* message);
void fakeMessage(const __FlashStringHelper* topic, const char* message);
void fakeMessage(const __FlashStringHelper* topic, const __FlashStringHelper* message);
private:
MqttMessage* _pendingMessage;
MqttMessage** _flushedMessages;
uint8_t _flushedMessagesNb;
MqttSubscription** _subscriptions;
uint8_t _subscriptionsNb;
MqttConnection _connection;
MqttWill _lastWill;
MQTT_CALLBACK_SIGNATURE;
};
#endif
#endif

View File

@@ -0,0 +1,195 @@
#include <Arduino.h>
#include "HADictionary.h"
// components
const char HAComponentBinarySensor[] PROGMEM = {"binary_sensor"};
const char HAComponentButton[] PROGMEM = {"button"};
const char HAComponentCamera[] PROGMEM = {"camera"};
const char HAComponentCover[] PROGMEM = {"cover"};
const char HAComponentDeviceTracker[] PROGMEM = {"device_tracker"};
const char HAComponentDeviceAutomation[] PROGMEM = {"device_automation"};
const char HAComponentLock[] PROGMEM = {"lock"};
const char HAComponentNumber[] PROGMEM = {"number"};
const char HAComponentSelect[] PROGMEM = {"select"};
const char HAComponentSensor[] PROGMEM = {"sensor"};
const char HAComponentSwitch[] PROGMEM = {"switch"};
const char HAComponentTag[] PROGMEM = {"tag"};
const char HAComponentScene[] PROGMEM = {"scene"};
const char HAComponentFan[] PROGMEM = {"fan"};
const char HAComponentLight[] PROGMEM = {"light"};
const char HAComponentClimate[] PROGMEM = {"climate"};
// decorators
const char HASerializerSlash[] PROGMEM = {"/"};
const char HASerializerJsonDataPrefix[] PROGMEM = {"{"};
const char HASerializerJsonDataSuffix[] PROGMEM = {"}"};
const char HASerializerJsonPropertyPrefix[] PROGMEM = {"\""};
const char HASerializerJsonPropertySuffix[] PROGMEM = {"\":"};
const char HASerializerJsonEscapeChar[] PROGMEM = {"\""};
const char HASerializerJsonPropertiesSeparator[] PROGMEM = {","};
const char HASerializerJsonArrayPrefix[] PROGMEM = {"["};
const char HASerializerJsonArraySuffix[] PROGMEM = {"]"};
const char HASerializerUnderscore[] PROGMEM = {"_"};
// properties
const char HADeviceIdentifiersProperty[] PROGMEM = {"ids"};
const char HADeviceManufacturerProperty[] PROGMEM = {"mf"};
const char HADeviceModelProperty[] PROGMEM = {"mdl"};
const char HADeviceSoftwareVersionProperty[] PROGMEM = {"sw"};
const char HANameProperty[] PROGMEM = {"name"};
const char HAUniqueIdProperty[] PROGMEM = {"uniq_id"};
const char HADeviceProperty[] PROGMEM = {"dev"};
const char HADeviceClassProperty[] PROGMEM = {"dev_cla"};
const char HAIconProperty[] PROGMEM = {"ic"};
const char HARetainProperty[] PROGMEM = {"ret"};
const char HASourceTypeProperty[] PROGMEM = {"src_type"};
const char HAEncodingProperty[] PROGMEM = {"e"};
const char HAOptimisticProperty[] PROGMEM = {"opt"};
const char HAAutomationTypeProperty[] PROGMEM = {"atype"};
const char HATypeProperty[] PROGMEM = {"type"};
const char HASubtypeProperty[] PROGMEM = {"stype"};
const char HAForceUpdateProperty[] PROGMEM = {"frc_upd"};
const char HAUnitOfMeasurementProperty[] PROGMEM = {"unit_of_meas"};
const char HAValueTemplateProperty[] PROGMEM = {"val_tpl"};
const char HAOptionsProperty[] PROGMEM = {"options"};
const char HAMinProperty[] PROGMEM = {"min"};
const char HAMaxProperty[] PROGMEM = {"max"};
const char HAStepProperty[] PROGMEM = {"step"};
const char HAModeProperty[] PROGMEM = {"mode"};
const char HACommandTemplateProperty[] PROGMEM = {"cmd_tpl"};
const char HASpeedRangeMaxProperty[] PROGMEM = {"spd_rng_max"};
const char HASpeedRangeMinProperty[] PROGMEM = {"spd_rng_min"};
const char HABrightnessScaleProperty[] PROGMEM = {"bri_scl"};
const char HAMinMiredsProperty[] PROGMEM = {"min_mirs"};
const char HAMaxMiredsProperty[] PROGMEM = {"max_mirs"};
const char HATemperatureUnitProperty[] PROGMEM = {"temp_unit"};
const char HAMinTempProperty[] PROGMEM = {"min_temp"};
const char HAMaxTempProperty[] PROGMEM = {"max_temp"};
const char HATempStepProperty[] PROGMEM = {"temp_step"};
const char HAFanModesProperty[] PROGMEM = {"fan_modes"};
const char HASwingModesProperty[] PROGMEM = {"swing_modes"};
const char HAModesProperty[] PROGMEM = {"modes"};
const char HATemperatureCommandTemplateProperty[] PROGMEM = {"temp_cmd_tpl"};
const char HAPayloadOnProperty[] PROGMEM = {"pl_on"};
// topics
const char HAConfigTopic[] PROGMEM = {"config"};
const char HAAvailabilityTopic[] PROGMEM = {"avty_t"};
const char HATopic[] PROGMEM = {"t"};
const char HAStateTopic[] PROGMEM = {"stat_t"};
const char HACommandTopic[] PROGMEM = {"cmd_t"};
const char HAPositionTopic[] PROGMEM = {"pos_t"};
const char HAPercentageStateTopic[] PROGMEM = {"pct_stat_t"};
const char HAPercentageCommandTopic[] PROGMEM = {"pct_cmd_t"};
const char HABrightnessCommandTopic[] PROGMEM = {"bri_cmd_t"};
const char HABrightnessStateTopic[] PROGMEM = {"bri_stat_t"};
const char HAColorTemperatureCommandTopic[] PROGMEM = {"clr_temp_cmd_t"};
const char HAColorTemperatureStateTopic[] PROGMEM = {"clr_temp_stat_t"};
const char HACurrentTemperatureTopic[] PROGMEM = {"curr_temp_t"};
const char HAActionTopic[] PROGMEM = {"act_t"};
const char HAAuxCommandTopic[] PROGMEM = {"aux_cmd_t"};
const char HAAuxStateTopic[] PROGMEM = {"aux_stat_t"};
const char HAPowerCommandTopic[] PROGMEM = {"pow_cmd_t"};
const char HAFanModeCommandTopic[] PROGMEM = {"fan_mode_cmd_t"};
const char HAFanModeStateTopic[] PROGMEM = {"fan_mode_stat_t"};
const char HASwingModeCommandTopic[] PROGMEM = {"swing_mode_cmd_t"};
const char HASwingModeStateTopic[] PROGMEM = {"swing_mode_stat_t"};
const char HAModeCommandTopic[] PROGMEM = {"mode_cmd_t"};
const char HAModeStateTopic[] PROGMEM = {"mode_stat_t"};
const char HATemperatureCommandTopic[] PROGMEM = {"temp_cmd_t"};
const char HATemperatureStateTopic[] PROGMEM = {"temp_stat_t"};
const char HARGBCommandTopic[] PROGMEM = {"rgb_cmd_t"};
const char HARGBStateTopic[] PROGMEM = {"rgb_stat_t"};
// misc
const char HAOnline[] PROGMEM = {"online"};
const char HAOffline[] PROGMEM = {"offline"};
const char HAStateOn[] PROGMEM = {"ON"};
const char HAStateOff[] PROGMEM = {"OFF"};
const char HAStateLocked[] PROGMEM = {"LOCKED"};
const char HAStateUnlocked[] PROGMEM = {"UNLOCKED"};
const char HAStateNone[] PROGMEM = {"None"};
const char HATrue[] PROGMEM = {"true"};
const char HAFalse[] PROGMEM = {"false"};
const char HAHome[] PROGMEM = {"home"};
const char HANotHome[] PROGMEM = {"not_home"};
const char HATrigger[] PROGMEM = {"trigger"};
const char HAModeBox[] PROGMEM = {"box"};
const char HAModeSlider[] PROGMEM = {"slider"};
// covers
const char HAClosedState[] PROGMEM = {"closed"};
const char HAClosingState[] PROGMEM = {"closing"};
const char HAOpenState[] PROGMEM = {"open"};
const char HAOpeningState[] PROGMEM = {"opening"};
const char HAStoppedState[] PROGMEM = {"stopped"};
// commands
const char HAOpenCommand[] PROGMEM = {"OPEN"};
const char HACloseCommand[] PROGMEM = {"CLOSE"};
const char HAStopCommand[] PROGMEM = {"STOP"};
const char HALockCommand[] PROGMEM = {"LOCK"};
const char HAUnlockCommand[] PROGMEM = {"UNLOCK"};
// device tracker
const char HAGPSType[] PROGMEM = {"gps"};
const char HARouterType[] PROGMEM = {"router"};
const char HABluetoothType[] PROGMEM = {"bluetooth"};
const char HABluetoothLEType[] PROGMEM = {"bluetooth_le"};
// camera
const char HAEncodingBase64[] PROGMEM = {"b64"};
// trigger
const char HAButtonShortPressType[] PROGMEM = {"button_short_press"};
const char HAButtonShortReleaseType[] PROGMEM = {"button_short_release"};
const char HAButtonLongPressType[] PROGMEM = {"button_long_press"};
const char HAButtonLongReleaseType[] PROGMEM = {"button_long_release"};
const char HAButtonDoublePressType[] PROGMEM = {"button_double_press"};
const char HAButtonTriplePressType[] PROGMEM = {"button_triple_press"};
const char HAButtonQuadruplePressType[] PROGMEM = {"button_quadruple_press"};
const char HAButtonQuintuplePressType[] PROGMEM = {"button_quintuple_press"};
const char HATurnOnSubtype[] PROGMEM = {"turn_on"};
const char HATurnOffSubtype[] PROGMEM = {"turn_off"};
const char HAButton1Subtype[] PROGMEM = {"button_1"};
const char HAButton2Subtype[] PROGMEM = {"button_2"};
const char HAButton3Subtype[] PROGMEM = {"button_3"};
const char HAButton4Subtype[] PROGMEM = {"button_4"};
const char HAButton5Subtype[] PROGMEM = {"button_5"};
const char HAButton6Subtype[] PROGMEM = {"button_6"};
// actions
const char HAActionOff[] PROGMEM = {"off"};
const char HAActionHeating[] PROGMEM = {"heating"};
const char HAActionCooling[] PROGMEM = {"cooling"};
const char HAActionDrying[] PROGMEM = {"drying"};
const char HAActionIdle[] PROGMEM = {"idle"};
const char HAActionFan[] PROGMEM = {"fan"};
// fan modes
const char HAFanModeAuto[] PROGMEM = {"auto"};
const char HAFanModeLow[] PROGMEM = {"low"};
const char HAFanModeMedium[] PROGMEM = {"medium"};
const char HAFanModeHigh[] PROGMEM = {"high"};
// swing modes
const char HASwingModeOn[] PROGMEM = {"on"};
const char HASwingModeOff[] PROGMEM = {"off"};
// HVAC modes
const char HAModeAuto[] PROGMEM = {"auto"};
const char HAModeOff[] PROGMEM = {"off"};
const char HAModeCool[] PROGMEM = {"cool"};
const char HAModeHeat[] PROGMEM = {"heat"};
const char HAModeDry[] PROGMEM = {"dry"};
const char HAModeFanOnly[] PROGMEM = {"fan_only"};
// other
const char HAHexMap[] PROGMEM = {"0123456789abcdef"};
// value templates
const char HAValueTemplateFloatP1[] PROGMEM = {"{{int(float(value)*10**1)}}"};
const char HAValueTemplateFloatP2[] PROGMEM = {"{{int(float(value)*10**2)}}"};
const char HAValueTemplateFloatP3[] PROGMEM = {"{{int(float(value)*10**3)}}"};
const char HATemperatureUnitC[] PROGMEM = {"C"};
const char HATemperatureUnitF[] PROGMEM = {"F"};

View File

@@ -0,0 +1,197 @@
#ifndef AHA_HADICTIONARY_H
#define AHA_HADICTIONARY_H
// components
extern const char HAComponentBinarySensor[];
extern const char HAComponentButton[];
extern const char HAComponentCamera[];
extern const char HAComponentCover[];
extern const char HAComponentDeviceTracker[];
extern const char HAComponentDeviceAutomation[];
extern const char HAComponentLock[];
extern const char HAComponentNumber[];
extern const char HAComponentSelect[];
extern const char HAComponentSensor[];
extern const char HAComponentSwitch[];
extern const char HAComponentTag[];
extern const char HAComponentScene[];
extern const char HAComponentFan[];
extern const char HAComponentLight[];
extern const char HAComponentClimate[];
// decorators
extern const char HASerializerSlash[];
extern const char HASerializerJsonDataPrefix[];
extern const char HASerializerJsonDataSuffix[];
extern const char HASerializerJsonPropertyPrefix[];
extern const char HASerializerJsonPropertySuffix[];
extern const char HASerializerJsonEscapeChar[];
extern const char HASerializerJsonPropertiesSeparator[];
extern const char HASerializerJsonArrayPrefix[];
extern const char HASerializerJsonArraySuffix[];
extern const char HASerializerUnderscore[];
// properties
extern const char HADeviceIdentifiersProperty[];
extern const char HADeviceManufacturerProperty[];
extern const char HADeviceModelProperty[];
extern const char HADeviceSoftwareVersionProperty[];
extern const char HANameProperty[];
extern const char HAUniqueIdProperty[];
extern const char HADeviceProperty[];
extern const char HADeviceClassProperty[];
extern const char HAIconProperty[];
extern const char HARetainProperty[];
extern const char HASourceTypeProperty[];
extern const char HAEncodingProperty[];
extern const char HAOptimisticProperty[];
extern const char HAAutomationTypeProperty[];
extern const char HATypeProperty[];
extern const char HASubtypeProperty[];
extern const char HAForceUpdateProperty[];
extern const char HAUnitOfMeasurementProperty[];
extern const char HAValueTemplateProperty[];
extern const char HAOptionsProperty[];
extern const char HAMinProperty[];
extern const char HAMaxProperty[];
extern const char HAStepProperty[];
extern const char HAModeProperty[];
extern const char HACommandTemplateProperty[];
extern const char HASpeedRangeMaxProperty[];
extern const char HASpeedRangeMinProperty[];
extern const char HABrightnessScaleProperty[];
extern const char HAMinMiredsProperty[];
extern const char HAMaxMiredsProperty[];
extern const char HATemperatureUnitProperty[];
extern const char HAMinTempProperty[];
extern const char HAMaxTempProperty[];
extern const char HATempStepProperty[];
extern const char HAFanModesProperty[];
extern const char HASwingModesProperty[];
extern const char HAModesProperty[];
extern const char HATemperatureCommandTemplateProperty[];
extern const char HAPayloadOnProperty[];
// topics
extern const char HAConfigTopic[];
extern const char HAAvailabilityTopic[];
extern const char HATopic[];
extern const char HAStateTopic[];
extern const char HACommandTopic[];
extern const char HAPositionTopic[];
extern const char HAPercentageStateTopic[];
extern const char HAPercentageCommandTopic[];
extern const char HABrightnessCommandTopic[];
extern const char HABrightnessStateTopic[];
extern const char HAColorTemperatureCommandTopic[];
extern const char HAColorTemperatureStateTopic[];
extern const char HACurrentTemperatureTopic[];
extern const char HAActionTopic[];
extern const char HAAuxCommandTopic[];
extern const char HAAuxStateTopic[];
extern const char HAPowerCommandTopic[];
extern const char HAFanModeCommandTopic[];
extern const char HAFanModeStateTopic[];
extern const char HASwingModeCommandTopic[];
extern const char HASwingModeStateTopic[];
extern const char HAModeCommandTopic[];
extern const char HAModeStateTopic[];
extern const char HATemperatureCommandTopic[];
extern const char HATemperatureStateTopic[];
extern const char HARGBCommandTopic[];
extern const char HARGBStateTopic[];
// misc
extern const char HAOnline[];
extern const char HAOffline[];
extern const char HAStateOn[];
extern const char HAStateOff[];
extern const char HAStateLocked[];
extern const char HAStateUnlocked[];
extern const char HAStateNone[];
extern const char HATrue[];
extern const char HAFalse[];
extern const char HAHome[];
extern const char HANotHome[];
extern const char HATrigger[];
extern const char HAModeBox[];
extern const char HAModeSlider[];
// covers
extern const char HAClosedState[];
extern const char HAClosingState[];
extern const char HAOpenState[];
extern const char HAOpeningState[];
extern const char HAStoppedState[];
// commands
extern const char HAOpenCommand[];
extern const char HACloseCommand[];
extern const char HAStopCommand[];
extern const char HALockCommand[];
extern const char HAUnlockCommand[];
// device tracker
extern const char HAGPSType[];
extern const char HARouterType[];
extern const char HABluetoothType[];
extern const char HABluetoothLEType[];
// camera
extern const char HAEncodingBase64[];
// trigger
extern const char HAButtonShortPressType[];
extern const char HAButtonShortReleaseType[];
extern const char HAButtonLongPressType[];
extern const char HAButtonLongReleaseType[];
extern const char HAButtonDoublePressType[];
extern const char HAButtonTriplePressType[];
extern const char HAButtonQuadruplePressType[];
extern const char HAButtonQuintuplePressType[];
extern const char HATurnOnSubtype[];
extern const char HATurnOffSubtype[];
extern const char HAButton1Subtype[];
extern const char HAButton2Subtype[];
extern const char HAButton3Subtype[];
extern const char HAButton4Subtype[];
extern const char HAButton5Subtype[];
extern const char HAButton6Subtype[];
// actions
extern const char HAActionOff[];
extern const char HAActionHeating[];
extern const char HAActionCooling[];
extern const char HAActionDrying[];
extern const char HAActionIdle[];
extern const char HAActionFan[];
// fan modes
extern const char HAFanModeAuto[];
extern const char HAFanModeLow[];
extern const char HAFanModeMedium[];
extern const char HAFanModeHigh[];
// swing modes
extern const char HASwingModeOn[];
extern const char HASwingModeOff[];
// HVAC modes
extern const char HAModeAuto[];
extern const char HAModeOff[];
extern const char HAModeCool[];
extern const char HAModeHeat[];
extern const char HAModeDry[];
extern const char HAModeFanOnly[];
// other
extern const char HAHexMap[];
// value templates
extern const char HAValueTemplateFloatP1[];
extern const char HAValueTemplateFloatP2[];
extern const char HAValueTemplateFloatP3[];
extern const char HATemperatureUnitC[];
extern const char HATemperatureUnitF[];
#endif

View File

@@ -0,0 +1,214 @@
#include "HANumeric.h"
const uint8_t HANumeric::MaxDigitsNb = 19;
HANumeric HANumeric::fromStr(const uint8_t* buffer, const uint16_t length)
{
if (length == 0) {
return HANumeric();
}
const uint8_t* firstCh = &buffer[0];
int64_t out = 0;
bool isSigned = false;
if (*firstCh == '-') {
isSigned = true;
firstCh++;
}
uint8_t digitsNb = isSigned ? length - 1 : length;
if (digitsNb > MaxDigitsNb) {
return HANumeric();
}
uint64_t base = 1;
const uint8_t* ptr = &buffer[length - 1];
while (ptr >= firstCh) {
uint8_t digit = *ptr - '0';
if (digit > 9) {
return HANumeric();
}
out += digit * base;
ptr--;
base *= 10;
}
return HANumeric(isSigned ? out * -1 : out);
}
HANumeric::HANumeric():
_isSet(false),
_value(0),
_precision(0)
{
}
HANumeric::HANumeric(const float value, const uint8_t precision):
_isSet(true),
_precision(precision)
{
_value = value * static_cast<float>(getPrecisionBase());
}
HANumeric::HANumeric(const int8_t value, const uint8_t precision):
_isSet(true),
_precision(precision)
{
_value = value * static_cast<int32_t>(getPrecisionBase());
}
HANumeric::HANumeric(const int16_t value, const uint8_t precision):
_isSet(true),
_precision(precision)
{
_value = value * static_cast<int32_t>(getPrecisionBase());
}
HANumeric::HANumeric(const int32_t value, const uint8_t precision):
_isSet(true),
_precision(precision)
{
_value = value * static_cast<int32_t>(getPrecisionBase());
}
HANumeric::HANumeric(const uint8_t value, const uint8_t precision):
_isSet(true),
_precision(precision)
{
_value = value * getPrecisionBase();
}
HANumeric::HANumeric(const uint16_t value, const uint8_t precision):
_isSet(true),
_precision(precision)
{
_value = value * getPrecisionBase();
}
HANumeric::HANumeric(const uint32_t value, const uint8_t precision):
_isSet(true),
_precision(precision)
{
_value = value * getPrecisionBase();
}
#ifdef __SAMD21G18A__
HANumeric::HANumeric(const int value, const uint8_t precision):
_isSet(true),
_precision(precision)
{
_value = value * static_cast<int>(getPrecisionBase());
}
#endif
HANumeric::HANumeric(const int64_t value):
_isSet(true),
_value(value),
_precision(0)
{
}
uint32_t HANumeric::getPrecisionBase() const
{
// using pow() increases the flash size by ~2KB
switch (_precision) {
case 1:
return 10;
case 2:
return 100;
case 3:
return 1000;
default:
return 1;
}
}
uint8_t HANumeric::calculateSize() const
{
if (!_isSet) {
return 0;
}
int64_t value = _value;
const bool isSigned = value < 0;
if (isSigned) {
value *= -1;
}
uint8_t digitsNb = 1;
while (value > 9) {
value /= 10;
digitsNb++;
}
if (isSigned) {
digitsNb++; // sign
}
if (_precision > 0) {
if (value == 0) {
return 1;
}
// one digit + dot + decimal digits (+ sign)
const uint8_t minValue = isSigned ? _precision + 3 : _precision + 2;
return digitsNb >= minValue ? digitsNb + 1 : minValue;
}
return digitsNb;
}
uint16_t HANumeric::toStr(char* dst) const
{
char* prefixCh = &dst[0];
if (!_isSet || _value == 0) {
*prefixCh = '0';
return 1;
}
int64_t value = _value;
const uint8_t numberLength = calculateSize();
if (value < 0) {
value *= -1;
*prefixCh = '-';
prefixCh++;
}
if (_precision > 0) {
uint8_t i = _precision;
char* dotPtr = prefixCh + 1;
do {
*prefixCh = '0';
prefixCh++;
} while(i-- > 0);
*dotPtr = '.';
}
char* ch = &dst[numberLength - 1];
char* lastCh = ch;
char* dotPos = _precision > 0 ? &dst[numberLength - 1 - _precision] : nullptr;
while (value != 0) {
if (ch == dotPos) {
*dotPos = '.';
ch--;
continue;
}
*ch = (value % 10) + '0';
value /= 10;
ch--;
}
return lastCh - &dst[0] + 1;
}

View File

@@ -0,0 +1,233 @@
#ifndef AHA_NUMERIC_H
#define AHA_NUMERIC_H
#include <stdint.h>
/**
* This class represents a numeric value that simplifies use of different types of numbers across the library.
*/
class HANumeric
{
public:
/// The maximum number of digits that the base value can have (int64_t).
static const uint8_t MaxDigitsNb;
/**
* Deserializes number from the given buffer.
* Please note that the class expected buffer to contain the base number.
* For example, deserializing `1234` number and setting precision to `1`
* results in representation of `123.4` float.
*
* @param buffer The buffer that contains the number.
* @param length The length of the buffer.
*/
static HANumeric fromStr(const uint8_t* buffer, const uint16_t length);
/**
* Creates an empty number representation.
*/
HANumeric();
/**
* Converts the given float into number representation of the given precision.
* If the precision is set to zero the given float will be converted into integer.
*
* @param value The value that should be used as a base.
* @param precision The number of digits in the decimal part.
*/
HANumeric(const float value, const uint8_t precision);
/**
* Converts the given int8_t into number representation of the given precision.
* If the precision is greater than zero the given value will be converted to float.
*
* @param value The value that should be used as a base.
* @param precision The number of digits in the decimal part.
*/
HANumeric(const int8_t value, const uint8_t precision);
/**
* Converts the given int16_t into number representation of the given precision.
* If the precision is greater than zero the given value will be converted to float.
*
* @param value The value that should be used as a base.
* @param precision The number of digits in the decimal part.
*/
HANumeric(const int16_t value, const uint8_t precision);
/**
* Converts the given int32_t into number representation of the given precision.
* If the precision is greater than zero the given value will be converted to float.
*
* @param value The value that should be used as a base.
* @param precision The number of digits in the decimal part.
*/
HANumeric(const int32_t value, const uint8_t precision);
/**
* Converts the given uint8_t into number representation of the given precision.
* If the precision is greater than zero the given value will be converted to float.
*
* @param value The value that should be used as a base.
* @param precision The number of digits in the decimal part.
*/
HANumeric(const uint8_t value, const uint8_t precision);
/**
* Converts the given uint16_t into number representation of the given precision.
* If the precision is greater than zero the given value will be converted to float.
*
* @param value The value that should be used as a base.
* @param precision The number of digits in the decimal part.
*/
HANumeric(const uint16_t value, const uint8_t precision);
/**
* Converts the given uint32_t into number representation of the given precision.
* If the precision is greater than zero the given value will be converted to float.
*
* @param value The value that should be used as a base.
* @param precision The number of digits in the decimal part.
*/
HANumeric(const uint32_t value, const uint8_t precision);
#ifdef __SAMD21G18A__
/**
* Converts the given int into number representation of the given precision.
* If the precision is greater than zero the given value will be converted to float.
*
* @param value The value that should be used as a base.
* @param precision The number of digits in the decimal part.
*/
HANumeric(const int value, const uint8_t precision);
#endif
void operator= (const HANumeric& a) {
if (!a.isSet()) {
reset();
} else {
_isSet = a.isSet();
_value = a.getBaseValue();
_precision = a.getPrecision();
}
}
bool operator== (const HANumeric& a) const {
return (
isSet() == a.isSet() &&
getBaseValue() == a.getBaseValue() &&
getPrecision() == a.getPrecision()
);
}
/**
* Returns multiplier that used to generate base value based on the precision.
* The multiplier is generated using the formula: `pow(precision, 10)`.
*/
uint32_t getPrecisionBase() const;
/**
* Returns size of the number
*/
uint8_t calculateSize() const;
/**
* Converts the number to the string.
*
* @param dst Destination where the number will be saved.
* The null terminator is not added at the end.
* @return The number of written characters.
* @note The `dst` size should be calculated using HANumeric::calculateSize() method plus 1 extra byte for the null terminator.
*/
uint16_t toStr(char* dst) const;
/**
* Returns true if the base value is set.
*/
inline bool isSet() const
{ return _isSet; }
/**
* Sets the base value without converting it to the proper precision.
*/
inline void setBaseValue(int64_t value)
{ _isSet = true; _value = value; }
/**
* Returns the base value of the number.
*/
inline int64_t getBaseValue() const
{ return _value; }
/**
* Sets the precision of the number (number of digits in the decimal part).
*
* @param precision The precision to use.
*/
inline void setPrecision(const uint8_t precision)
{ _precision = precision; }
/**
* Returns the precision of the number.
*/
inline uint8_t getPrecision() const
{ return _precision; }
/**
* Resets the number to the defaults.
*/
inline void reset()
{ _isSet = false; _value = 0; _precision = 0; }
inline bool isUInt8() const
{ return _isSet && _precision == 0 && _value >= 0 && _value <= UINT8_MAX; }
inline bool isUInt16() const
{ return _isSet && _precision == 0 && _value >= 0 && _value <= UINT16_MAX; }
inline bool isUInt32() const
{ return _isSet && _precision == 0 && _value >= 0 && _value <= UINT32_MAX; }
inline bool isInt8() const
{ return _isSet && _precision == 0 && _value >= INT8_MIN && _value <= INT8_MAX; }
inline bool isInt16() const
{ return _isSet && _precision == 0 && _value >= INT16_MIN && _value <= INT16_MAX; }
inline bool isInt32() const
{ return _isSet && _precision == 0 && _value >= INT32_MIN && _value <= INT32_MAX; }
inline bool isFloat() const
{ return _isSet && _precision > 0; }
inline uint8_t toUInt8() const
{ return static_cast<uint8_t>(_value); }
inline uint16_t toUInt16() const
{ return static_cast<uint16_t>(_value); }
inline uint32_t toUInt32() const
{ return static_cast<uint32_t>(_value); }
inline int8_t toInt8() const
{ return static_cast<int8_t>(_value); }
inline int16_t toInt16() const
{ return static_cast<int16_t>(_value); }
inline int32_t toInt32() const
{ return static_cast<int32_t>(_value); }
inline float toFloat() const
{ return _value / (float)getPrecisionBase(); }
private:
bool _isSet;
int64_t _value;
uint8_t _precision;
explicit HANumeric(const int64_t value);
};
#endif

View File

@@ -0,0 +1,526 @@
#include <Arduino.h>
#ifdef ARDUINO_ARCH_SAMD
#include <avr/dtostrf.h>
#endif
#include "HASerializer.h"
#include "../ArduinoHADefines.h"
#include "../HADevice.h"
#include "../HAMqtt.h"
#include "../utils/HAUtils.h"
#include "../utils/HANumeric.h"
#include "../device-types/HABaseDeviceType.h"
uint16_t HASerializer::calculateConfigTopicLength(
const __FlashStringHelper* componentName,
const char* objectId
)
{
const HAMqtt* mqtt = HAMqtt::instance();
if (
!componentName ||
!objectId ||
!mqtt ||
!mqtt->getDiscoveryPrefix() ||
!mqtt->getDevice()
) {
return 0;
}
return
strlen(mqtt->getDiscoveryPrefix()) + 1 + // prefix with slash
strlen_P(AHAFROMFSTR(componentName)) + 1 + // component name with slash
strlen(mqtt->getDevice()->getUniqueId()) + 1 + // device ID with slash
strlen(objectId) + 1 + // object ID with slash
strlen_P(HAConfigTopic) + 1; // including null terminator
}
bool HASerializer::generateConfigTopic(
char* output,
const __FlashStringHelper* componentName,
const char* objectId
)
{
const HAMqtt* mqtt = HAMqtt::instance();
if (
!output ||
!componentName ||
!objectId ||
!mqtt ||
!mqtt->getDiscoveryPrefix() ||
!mqtt->getDevice()
) {
return false;
}
strcpy(output, mqtt->getDiscoveryPrefix());
strcat_P(output, HASerializerSlash);
strcat_P(output, AHAFROMFSTR(componentName));
strcat_P(output, HASerializerSlash);
strcat(output, mqtt->getDevice()->getUniqueId());
strcat_P(output, HASerializerSlash);
strcat(output, objectId);
strcat_P(output, HASerializerSlash);
strcat_P(output, HAConfigTopic);
return true;
}
uint16_t HASerializer::calculateDataTopicLength(
const char* objectId,
const __FlashStringHelper* topic
)
{
const HAMqtt* mqtt = HAMqtt::instance();
if (
!topic ||
!mqtt ||
!mqtt->getDataPrefix() ||
!mqtt->getDevice()
) {
return 0;
}
uint16_t size =
strlen(mqtt->getDataPrefix()) + 1 + // prefix with slash
strlen(mqtt->getDevice()->getUniqueId()) + 1 + // device ID with slash
strlen_P(AHAFROMFSTR(topic));
if (objectId) {
size += strlen(objectId) + 1; // object ID with slash;
}
return size + 1; // including null terminator
}
bool HASerializer::generateDataTopic(
char* output,
const char* objectId,
const __FlashStringHelper* topic
)
{
const HAMqtt* mqtt = HAMqtt::instance();
if (
!output ||
!topic ||
!mqtt ||
!mqtt->getDataPrefix() ||
!mqtt->getDevice()
) {
return false;
}
strcpy(output, mqtt->getDataPrefix());
strcat_P(output, HASerializerSlash);
strcat(output, mqtt->getDevice()->getUniqueId());
strcat_P(output, HASerializerSlash);
if (objectId) {
strcat(output, objectId);
strcat_P(output, HASerializerSlash);
}
strcat_P(output, AHAFROMFSTR(topic));
return true;
}
bool HASerializer::compareDataTopics(
const char* actualTopic,
const char* objectId,
const __FlashStringHelper* topic
)
{
if (!actualTopic) {
return false;
}
const uint16_t topicLength = calculateDataTopicLength(objectId, topic);
if (topicLength == 0) {
return false;
}
char expectedTopic[topicLength];
if (!generateDataTopic(expectedTopic, objectId, topic)) {
return false;
}
return memcmp(actualTopic, expectedTopic, topicLength) == 0;
}
HASerializer::HASerializer(
HABaseDeviceType* deviceType,
const uint8_t maxEntriesNb
) :
_deviceType(deviceType),
_entriesNb(0),
_maxEntriesNb(maxEntriesNb),
_entries(new SerializerEntry[maxEntriesNb])
{
}
HASerializer::~HASerializer()
{
delete[] _entries;
}
void HASerializer::set(
const __FlashStringHelper* property,
const void* value,
PropertyValueType valueType
)
{
if (!property || !value) {
return;
}
SerializerEntry* entry = addEntry();
entry->type = PropertyEntryType;
entry->subtype = static_cast<uint8_t>(valueType);
entry->property = property;
entry->value = value;
}
void HASerializer::set(const FlagType flag)
{
if (flag == WithDevice) {
SerializerEntry* entry = addEntry();
entry->type = FlagEntryType;
entry->subtype = static_cast<uint8_t>(WithDevice);
entry->property = nullptr;
entry->value = nullptr;
} else if (flag == WithAvailability) {
HAMqtt* mqtt = HAMqtt::instance();
const bool isSharedAvailability = mqtt->getDevice()->isSharedAvailabilityEnabled();
const bool isAvailabilityConfigured = _deviceType->isAvailabilityConfigured();
if (!isSharedAvailability && !isAvailabilityConfigured) {
return; // not configured
}
SerializerEntry* entry = addEntry();
entry->type = TopicEntryType;
entry->property = AHATOFSTR(HAAvailabilityTopic);
entry->value = isSharedAvailability
? mqtt->getDevice()->getAvailabilityTopic()
: nullptr;
}
}
void HASerializer::topic(const __FlashStringHelper* topic)
{
if (!_deviceType || !topic) {
return;
}
SerializerEntry* entry = addEntry();
entry->type = TopicEntryType;
entry->property = topic;
}
HASerializer::SerializerEntry* HASerializer::addEntry()
{
return &_entries[_entriesNb++]; // intentional lack of protection against overflow
}
uint16_t HASerializer::calculateSize() const
{
uint16_t size =
strlen_P(HASerializerJsonDataPrefix) +
strlen_P(HASerializerJsonDataSuffix);
for (uint8_t i = 0; i < _entriesNb; i++) {
const uint16_t entrySize = calculateEntrySize(&_entries[i]);
if (entrySize == 0) {
continue;
}
size += entrySize;
// items separator
if (i > 0) {
size += strlen_P(HASerializerJsonPropertiesSeparator);
}
}
return size;
}
bool HASerializer::flush() const
{
HAMqtt* mqtt = HAMqtt::instance();
if (!mqtt || (_deviceType && !mqtt->getDevice())) {
return false;
}
mqtt->writePayload(AHATOFSTR(HASerializerJsonDataPrefix));
for (uint8_t i = 0; i < _entriesNb; i++) {
if (i > 0) {
mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertiesSeparator));
}
if (!flushEntry(&_entries[i])) {
return false;
}
}
mqtt->writePayload(AHATOFSTR(HASerializerJsonDataSuffix));
return true;
}
uint16_t HASerializer::calculateEntrySize(const SerializerEntry* entry) const
{
switch (entry->type) {
case PropertyEntryType:
return
// property name
strlen_P(HASerializerJsonPropertyPrefix) +
strlen_P(AHAFROMFSTR(entry->property)) +
strlen_P(HASerializerJsonPropertySuffix) +
// property value
calculatePropertyValueSize(entry);
case TopicEntryType:
return calculateTopicEntrySize(entry);
case FlagEntryType:
return calculateFlagSize(
static_cast<FlagType>(entry->subtype)
);
default:
return 0;
}
}
uint16_t HASerializer::calculateTopicEntrySize(
const SerializerEntry* entry
) const
{
uint16_t size = 0;
// property name
size +=
strlen_P(HASerializerJsonPropertyPrefix) +
strlen_P(AHAFROMFSTR(entry->property)) +
strlen_P(HASerializerJsonPropertySuffix);
// topic escape
size += 2 * strlen_P(HASerializerJsonEscapeChar);
// topic
if (entry->value) {
size += strlen(static_cast<const char*>(entry->value));
} else {
if (!_deviceType) {
return 0;
}
size += calculateDataTopicLength(
_deviceType->uniqueId(),
entry->property
) - 1; // exclude null terminator
}
return size;
}
uint16_t HASerializer::calculateFlagSize(const FlagType flag) const
{
const HAMqtt* mqtt = HAMqtt::instance();
const HADevice* device = mqtt->getDevice();
if (flag == WithDevice && device->getSerializer()) {
const uint16_t deviceLength = device->getSerializer()->calculateSize();
if (deviceLength == 0) {
return 0;
}
return
strlen_P(HASerializerJsonPropertyPrefix) +
strlen_P(HADeviceProperty) +
strlen_P(HASerializerJsonPropertySuffix) +
deviceLength;
}
return 0;
}
uint16_t HASerializer::calculatePropertyValueSize(
const SerializerEntry* entry
) const
{
switch (entry->subtype) {
case ConstCharPropertyValue:
case ProgmemPropertyValue: {
const char* value = static_cast<const char*>(entry->value);
const uint16_t len =
entry->subtype == ConstCharPropertyValue ? strlen(value) : strlen_P(value);
return 2 * strlen_P(HASerializerJsonEscapeChar) + len;
}
case BoolPropertyType: {
const bool value = *static_cast<const bool*>(entry->value);
return value ? strlen_P(HATrue) : strlen_P(HAFalse);
}
case NumberPropertyType: {
const HANumeric* value = static_cast<const HANumeric*>(
entry->value
);
return value->calculateSize();
}
case ArrayPropertyType: {
const HASerializerArray* array = static_cast<const HASerializerArray*>(
entry->value
);
return array->calculateSize();
}
default:
return 0;
}
}
bool HASerializer::flushEntry(const SerializerEntry* entry) const
{
HAMqtt* mqtt = HAMqtt::instance();
switch (entry->type) {
case PropertyEntryType: {
mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix));
mqtt->writePayload(entry->property);
mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertySuffix));
return flushEntryValue(entry);
}
case TopicEntryType:
return flushTopic(entry);
case FlagEntryType:
return flushFlag(entry);
default:
return true;
}
}
bool HASerializer::flushEntryValue(const SerializerEntry* entry) const
{
HAMqtt* mqtt = HAMqtt::instance();
switch (entry->subtype) {
case ConstCharPropertyValue:
case ProgmemPropertyValue: {
const char* value = static_cast<const char*>(entry->value);
mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar));
if (entry->subtype == ConstCharPropertyValue) {
mqtt->writePayload(value, strlen(value));
} else {
mqtt->writePayload(AHATOFSTR(value));
}
mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar));
return true;
}
case BoolPropertyType: {
const bool value = *static_cast<const bool*>(entry->value);
mqtt->writePayload(AHATOFSTR(value ? HATrue : HAFalse));
return true;
}
case NumberPropertyType: {
const HANumeric* value = static_cast<const HANumeric*>(
entry->value
);
char tmp[HANumeric::MaxDigitsNb + 1];
const uint16_t length = value->toStr(tmp);
mqtt->writePayload(tmp, length);
return true;
}
case ArrayPropertyType: {
const HASerializerArray* array = static_cast<const HASerializerArray*>(
entry->value
);
const uint16_t size = array->calculateSize();
char tmp[size + 1]; // including null terminator
tmp[0] = 0;
array->serialize(tmp);
mqtt->writePayload(tmp, size);
return true;
}
default:
return false;
}
}
bool HASerializer::flushTopic(const SerializerEntry* entry) const
{
HAMqtt* mqtt = HAMqtt::instance();
// property name
mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix));
mqtt->writePayload(entry->property);
mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertySuffix));
// value (escaped)
mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar));
if (entry->value) {
const char* topic = static_cast<const char*>(entry->value);
mqtt->writePayload(topic, strlen(topic));
} else {
const uint16_t length = calculateDataTopicLength(
_deviceType->uniqueId(),
entry->property
);
if (length == 0) {
return false;
}
char topic[length];
generateDataTopic(
topic,
_deviceType->uniqueId(),
entry->property
);
mqtt->writePayload(topic, length - 1);
}
mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar));
return true;
}
bool HASerializer::flushFlag(const SerializerEntry* entry) const
{
HAMqtt* mqtt = HAMqtt::instance();
const HADevice* device = mqtt->getDevice();
const FlagType flag = static_cast<FlagType>(entry->subtype);
if (flag == WithDevice && device) {
mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix));
mqtt->writePayload(AHATOFSTR(HADeviceProperty));
mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertySuffix));
return device->getSerializer()->flush();
}
return false;
}

View File

@@ -0,0 +1,269 @@
#ifndef AHA_SERIALIZER_H
#define AHA_SERIALIZER_H
#include <stdint.h>
#include "HADictionary.h"
#include "HASerializerArray.h"
class HAMqtt;
class HABaseDeviceType;
/**
* This class allows to create JSON objects easily.
* Its main purpose is to handle configuration of a device type that's going to
* be published to the MQTT broker.
*/
class HASerializer
{
public:
/// Type of the object's entry.
enum EntryType {
UnknownEntryType = 0,
PropertyEntryType,
TopicEntryType,
FlagEntryType
};
/// The type of a flag for a FlagEntryType.
enum FlagType {
WithDevice = 1,
WithAvailability
};
/// Available data types of entries.
enum PropertyValueType {
UnknownPropertyValueType = 0,
ConstCharPropertyValue,
ProgmemPropertyValue,
BoolPropertyType,
NumberPropertyType,
ArrayPropertyType
};
/// Representation of a single entry in the object.
struct SerializerEntry {
/// Type of the entry.
EntryType type;
/// Subtype of the entry. It can be `FlagType`, `PropertyValueType` or `TopicType`.
uint8_t subtype;
/// Pointer to the property name (progmem string).
const __FlashStringHelper* property;
/// Pointer to the property value. The value type is determined by `subtype`.
const void* value;
SerializerEntry():
type(UnknownEntryType),
subtype(0),
property(nullptr),
value(nullptr)
{ }
};
/**
* Calculates the size of a configuration topic for the given component and object ID.
* The configuration topic has structure as follows: `[discovery prefix]/[component]/[device ID]_[objectId]/config`
*
* @param component The name of the HA component (e.g. `binary_sensor`).
* @param objectId The unique ID of a device type that's going to publish the config.
*/
static uint16_t calculateConfigTopicLength(
const __FlashStringHelper* component,
const char* objectId
);
/**
* Generates the configuration topic for the given component and object ID.
* The topic will be stored in the `output` variable.
*
* @param output Buffer where the topic will be written.
* @param component The name of the HA component (e.g. `binary_sensor`).
* @param objectId The unique ID of a device type that's going to publish the config.
*/
static bool generateConfigTopic(
char* output,
const __FlashStringHelper* component,
const char* objectId
);
/**
* Calculates the size of the given data topic for the given objectId.
* The data topic has structure as follows: `[data prefix]/[device ID]_[objectId]/[topic]`
*
* @param objectId The unique ID of a device type that's going to publish the data.
* @param topic The topic name (progmem string).
*/
static uint16_t calculateDataTopicLength(
const char* objectId,
const __FlashStringHelper* topic
);
/**
* Generates the data topic for the given object ID.
* The topic will be stored in the `output` variable.
*
* @param output Buffer where the topic will be written.
* @param objectId The unique ID of a device type that's going to publish the data.
* @param topic The topic name (progmem string).
*/
static bool generateDataTopic(
char* output,
const char* objectId,
const __FlashStringHelper* topic
);
/**
* Checks whether the given topic matches the data topic that can be generated
* using the given objectId and topicP.
* This method can be used to check if the received message matches some data topic.
*
* @param actualTopic The actual topic to compare.
* @param objectId The unique ID of a device type that may be the owner of the topic.
* @param topic The topic name (progmem string).
*/
static bool compareDataTopics(
const char* actualTopic,
const char* objectId,
const __FlashStringHelper* topic
);
/**
* Creates instance of the serializer for the given device type.
* Please note that the number JSON object's entries needs to be known upfront.
* This approach reduces number of memory allocations.
*
* @param deviceType The device type that owns the serializer.
* @param maxEntriesNb Maximum number of the output object entries.
*/
HASerializer(HABaseDeviceType* deviceType, const uint8_t maxEntriesNb);
/**
* Frees the dynamic memory allocated by the class.
*/
~HASerializer();
/**
* Returns the number of items that were added to the serializer.
*/
inline uint8_t getEntriesNb() const
{ return _entriesNb; }
/**
* Returns pointer to the serializer's entries.
*/
inline SerializerEntry* getEntries() const
{ return _entries; }
/**
* Adds a new entry to the serialized with a type of `PropertyEntryType`.
*
* @param property Pointer to the name of the property (progmem string).
* @param value Pointer to the value that's being set.
* @param valueType The type of the value that's passed to the method.
*/
void set(
const __FlashStringHelper* property,
const void* value,
PropertyValueType valueType = ConstCharPropertyValue
);
/**
* Adds a new entry to the serializer with a type of `FlagEntryType`.
*
* @param flag Flag to add.
*/
void set(const FlagType flag);
/**
* Adds a new entry to the serialize with a type of `TopicEntryType`.
*
* @param topic The topic name to add (progmem string).
*/
void topic(const __FlashStringHelper* topic);
/**
* Calculates the output size of the serialized JSON object.
*/
uint16_t calculateSize() const;
/**
* Flushes the JSON object to the MQTT stream.
* Please note that this method only writes the MQTT payload.
* The MQTT session needs to be opened before.
*/
bool flush() const;
private:
/// Pointer to the device type that owns the serializer.
HABaseDeviceType* _deviceType;
/// The number of entries added to the serializer.
uint8_t _entriesNb;
/// Maximum number of entries that can be added to the serializer.
uint8_t _maxEntriesNb;
/// Pointer to the serializer entries.
SerializerEntry* _entries;
/**
* Creates a new entry in the serializer's memory.
* If the limit of entries is hit, the nullptr is returned.
*/
SerializerEntry* addEntry();
/**
* Calculates the serialized size of the given entry.
* Internally, this method recognizes the type of the entry and calls
* a proper calculate method listed below.
*/
uint16_t calculateEntrySize(const SerializerEntry* entry) const;
/**
* Calculates the size of the entry of type `TopicEntryType`.
*/
uint16_t calculateTopicEntrySize(const SerializerEntry* entry) const;
/**
* Calculates the size of the entry of type `FlagEntryType`.
*/
uint16_t calculateFlagSize(const FlagType flag) const;
/**
* Calculates the size of the entry's value if the entry is `PropertyEntryType`.
*/
uint16_t calculatePropertyValueSize(const SerializerEntry* entry) const;
/**
* Calculates the size of the array if the property's value is a type of `ArrayPropertyType`.
*/
uint16_t calculateArraySize(const HASerializerArray* array) const;
/**
* Flushes the given entry to the MQTT.
* Internally this method recognizes the type of the entry and calls
* a proper flush method listed below.
*/
bool flushEntry(const SerializerEntry* entry) const;
/**
* Flushes the value of the `PropertyEntryType` entry.
*/
bool flushEntryValue(const SerializerEntry* entry) const;
/**
* Flushes the entry of type `TopicEntryType` to the MQTT.
*/
bool flushTopic(const SerializerEntry* entry) const;
/**
* Flushes the entry of type `FlagEntryType` to the MQTT.
*/
bool flushFlag(const SerializerEntry* entry) const;
};
#endif

View File

@@ -0,0 +1,84 @@
#include <Arduino.h>
#include "HASerializerArray.h"
#include "HADictionary.h"
HASerializerArray::HASerializerArray(const uint8_t size, const bool progmemItems) :
_progmemItems(progmemItems),
_size(size),
_itemsNb(0),
_items(new ItemType[size])
{
}
HASerializerArray::~HASerializerArray()
{
delete[] _items;
}
bool HASerializerArray::add(ItemType item)
{
if (_itemsNb >= _size) {
return false;
}
_items[_itemsNb++] = item;
return true;
}
uint16_t HASerializerArray::calculateSize() const
{
uint16_t size =
strlen_P(HASerializerJsonArrayPrefix) +
strlen_P(HASerializerJsonArraySuffix);
if (_itemsNb == 0) {
return size;
}
// separators between elements
size += (_itemsNb - 1) * strlen_P(HASerializerJsonPropertiesSeparator);
for (uint8_t i = 0; i < _itemsNb; i++) {
size +=
2 * strlen_P(HASerializerJsonEscapeChar)
+ (_progmemItems ? strlen_P(_items[i]) : strlen(_items[i]));
}
return size;
}
bool HASerializerArray::serialize(char* output) const
{
if (!output) {
return false;
}
strcat_P(output, HASerializerJsonArrayPrefix);
for (uint8_t i = 0; i < _itemsNb; i++) {
if (i > 0) {
strcat_P(output, HASerializerJsonPropertiesSeparator);
}
strcat_P(output, HASerializerJsonEscapeChar);
if (_progmemItems) {
strcat_P(output, _items[i]);
} else {
strcat(output, _items[i]);
}
strcat_P(output, HASerializerJsonEscapeChar);
}
strcat_P(output, HASerializerJsonArraySuffix);
return true;
}
void HASerializerArray::clear()
{
_itemsNb = 0;
}

View File

@@ -0,0 +1,75 @@
#ifndef AHA_SERIALIZERARRAY_H
#define AHA_SERIALIZERARRAY_H
#include <stdint.h>
/**
* HASerializerArray represents array of items that can be used as a HASerializer property.
*/
class HASerializerArray
{
public:
typedef const char* ItemType;
/**
* Constructs HASerializerArray with the static size (number of elements).
* The array is allocated dynamically in the memory based on the given size.
*
* @param size The desired number of elements that will be stored in the array.
* @param progmemItems Specifies whether items are going to be stored in the flash memory.
*/
HASerializerArray(const uint8_t size, const bool progmemItems = true);
~HASerializerArray();
/**
* Returns the number of elements that were added to the array.
* It can be lower than size of the array.
*/
inline uint8_t getItemsNb() const
{ return _itemsNb; }
/**
* Returns pointer to the array.
*/
inline ItemType* getItems() const
{ return _items; }
/**
* Adds a new element to the array.
*
* @param itemP Item to add (string).
* @returns Returns `true` if item has been added to the array successfully.
*/
bool add(ItemType item);
/**
* Calculates the size of the serialized array (JSON representation).
*/
uint16_t calculateSize() const;
/**
* Serializes array as JSON to the given output.
*/
bool serialize(char* output) const;
/**
* Clears the array.
*/
void clear();
private:
/// Specifies whether items are stored in the flash memory.
const bool _progmemItems;
/// The maximum size of the array.
const uint8_t _size;
/// The number of items that were added to the array.
uint8_t _itemsNb;
/// Pointer to the array elements.
ItemType* _items;
};
#endif

View File

@@ -0,0 +1,48 @@
#ifdef ARDUINO_ARCH_SAMD
#include <avr/dtostrf.h>
#endif
#include <Arduino.h>
#include "HAUtils.h"
#include "HADictionary.h"
bool HAUtils::endsWith(const char* str, const char* suffix)
{
if (str == nullptr || suffix == nullptr) {
return false;
}
const uint16_t lenstr = strlen(str);
const uint16_t lensuffix = strlen(suffix);
if (lensuffix > lenstr || lenstr == 0 || lensuffix == 0) {
return false;
}
return (strncmp(str + lenstr - lensuffix, suffix, lensuffix) == 0);
}
void HAUtils::byteArrayToStr(
char* dst,
const byte* src,
const uint16_t length
)
{
for (uint8_t i = 0; i < length; i++) {
dst[i*2] = pgm_read_byte(&HAHexMap[((char)src[i] & 0XF0) >> 4]);
dst[i*2+1] = pgm_read_byte(&HAHexMap[((char)src[i] & 0x0F)]);
}
dst[length * 2] = 0;
}
char* HAUtils::byteArrayToStr(
const byte* src,
const uint16_t length
)
{
char* dst = new char[(length * 2) + 1]; // include null terminator
byteArrayToStr(dst, src, length);
return dst;
}

View File

@@ -0,0 +1,52 @@
#ifndef AHA_HAUTILS_H
#define AHA_HAUTILS_H
#include <stdint.h>
/**
* This class provides some useful methods to make life easier.
*/
class HAUtils
{
public:
/**
* Checks whether the given `str` ends with the given `suffix`.
*
* @param str Input string to check.
* @param suffix Suffix to find
* @returns True if the given suffix is present at the end of the given string.
*/
static bool endsWith(
const char* str,
const char* suffix
);
/**
* Converts the given byte array into hex string.
* Each byte will be represented by two bytes, so the output size will be `length * 2`
*
* @param dst Destination where the string will be saved.
* @param src Bytes array to convert.
* @param length Length of the bytes array.
*/
static void byteArrayToStr(
char* dst,
const byte* src,
const uint16_t length
);
/**
* Converts the given byte array into hex string.
* This method allocates a new memory.
*
* @param src Bytes array to convert.
* @param length Length of the bytes array.
* @returns Newly allocated string containing the hex representation.
*/
static char* byteArrayToStr(
const byte* src,
const uint16_t length
);
};
#endif