Per-entity availability, deferred discovery publish, suggested precision

- Add HAAvailabilityConfig for per-entity availability_topic and payloads
- Defer discovery until MQTT connected; publish availability after discovery
- HASensor/HANumber: optional suggested_display_precision in discovery
- HASerializer/HADictionary: extend for new keys; update device types and mocks
- Docs and examples reflect availability and precision usage
- Tests updated for discovery and availability behavior
- Ignore .cursor/ in repo root
This commit is contained in:
2026-05-05 17:47:55 +10:00
parent 8ba5482892
commit b9f4fd7f4b
62 changed files with 2705 additions and 283 deletions
+3
View File
@@ -6,3 +6,6 @@ tmp/
# Sphinx inventory (legacy); we ship Markdown-only under docs/
docs/*.inv
# Cursor IDE (local rules, not part of the library)
.cursor/
+7
View File
@@ -24,6 +24,7 @@ lib_deps =
* Two-way communication (state reporting and command execution)
* MQTT discovery (device is added to the Home Assistant panel automatically)
* Rich discovery metadata for entities, devices, origin, templates and availability
* MQTT Last Will and Testament
* Support for custom MQTT messages (publishing and subscribing)
* Auto reconnect with MQTT broker
@@ -48,6 +49,12 @@ If you need to manage discovery at runtime:
When device discovery mode is enabled, runtime discovery refreshes automatically clear any stale retained per-entity config before republishing the device discovery payload.
Recent discovery additions include:
* Shared entity metadata such as `enabled_by_default`, `entity_picture`, `qos`, `encoding`
* Device/origin metadata such as `model_id`, `hw_version`, `serial_number`, `suggested_area`, `via_device`, `connections`, `support_url`
* Availability payload overrides and multi-topic availability discovery metadata
## Supported HA types
| Home Assistant type | Supported |
+22
View File
@@ -13,6 +13,8 @@ Represents the physical board in Home Assistant: one device can expose multiple
**Optional metadata** (each costs some RAM/flash; skip on tiny MCUs unless needed):
- `setName`, `setSoftwareVersion`, `setManufacturer`, `setModel`, `setConfigurationUrl`
- `setModelId`, `setHardwareVersion`, `setSerialNumber`, `setSuggestedArea`, `setViaDevice`
- `addConnection("mac", "aa:bb:cc:dd:ee:ff")` or `setConnectionsJson("[[\"mac\",\"aa:bb:cc:dd:ee:ff\"]]")`
String setters take **pointers whose contents are not copied** — use literals or storage that outlives the call.
@@ -41,8 +43,28 @@ mqtt.setDataPrefix("myDataPrefix");
- **Default:** one retained discovery topic per entity (single-component discovery).
- **Optional:** call `HAMqtt::enableDeviceDiscovery()` to publish a single **device** discovery payload with components under `cmps` (see project README for migration notes).
Device discovery can also publish richer origin/device metadata, for example:
```cpp
device.setModelId("esp32-s3-devkit");
device.setHardwareVersion("rev-b");
device.setSerialNumber("SN-00042");
device.setSuggestedArea("Garage");
device.setViaDevice("main_gateway");
device.addConnection("mac", "AA:BB:CC:DD:EE:FF");
mqtt.setOriginSupportUrl("https://example.com/device-help");
```
For entity identifiers in Home Assistant, prefer **`setDefaultEntityId()`** over legacy **`setObjectId()`**.
Common entity discovery metadata can be configured on most entity types via:
- `setEnabledByDefault(bool)`
- `setEntityPicture(const char*)`
- `setQos(uint8_t)`
- `setEncoding(const char*)`
- `setEntityCategory(const char*)`
### Runtime discovery changes
After changing discovery-related settings at runtime:
+58
View File
@@ -46,12 +46,70 @@ mqtt.publish("customTopic", "payload", true); // retained
```cpp
device.enableSharedAvailability();
device.setPayloadAvailable("up");
device.setPayloadNotAvailable("down");
device.enableLastWill(); // broker publishes offline when TCP drops
// device.setAvailability(false); // optional: start as offline
```
**Per-entity availability:** call `someEntity.setAvailability(true/false)` on each type. Does not use LWT the same way as shared mode; see examples under `examples/availability/`.
Custom per-entity payloads are supported:
```cpp
sensor.setPayloadAvailable("ready");
sensor.setPayloadNotAvailable("lost");
```
For multi-topic availability discovery, add full MQTT topics and a mode:
```cpp
sensor.setAvailabilityMode("all");
sensor.addAvailabilityEntry("bridge/status");
sensor.addAvailabilityEntry("sensor/status", "{{ value_json.state }}");
```
## Discovery helpers by entity
Common entity discovery metadata is available on most entity classes:
```cpp
entity.setEnabledByDefault(false);
entity.setEntityPicture("https://example.com/entity.png");
entity.setQos(1);
entity.setEncoding("utf-8");
entity.setEntityCategory("diagnostic");
```
Read-only sensor presentation/template helpers:
```cpp
sensor.setSuggestedDisplayPrecision(2);
sensor.setValueTemplate("{{ value_json.temperature }}");
sensor.setJsonAttributesTemplate("{{ value_json.attrs | tojson }}");
sensor.setLastResetValueTemplate("{{ value_json.last_reset }}");
sensor.setDeviceClass("enum");
sensor.setOptions("idle;charging;discharging;fault");
```
Writable entity template/payload helpers:
```cpp
mySwitch.setPayloadOn("ENABLE");
mySwitch.setPayloadOff("DISABLE");
mySwitch.setStateOn("running");
mySwitch.setStateOff("stopped");
mySwitch.setValueTemplate("{{ value_json.state }}");
mySwitch.setCommandTemplate("{{ value_json.command }}");
myNumber.setPayloadReset("RESET");
myNumber.setCommandTemplate("{{ value | float | round(1) }}");
mySelect.setCommandTemplate("{{ value_json.choice }}");
myText.setCommandTemplate("{{ value_json.text }}");
myButton.setPayloadPress("PRESS");
```
## Compiler macros
Defined in `ArduinoHADefines.h` or via build flags.
@@ -29,10 +29,19 @@ void setup() {
// set device's details (optional)
device.setName("Arduino");
device.setSoftwareVersion("1.0.0");
mqtt.setOriginSupportUrl("https://example.com/device-help");
sensor.setCurrentState(lastInputState); // optional
sensor.setName("Door sensor"); // optional
sensor.setDeviceClass("door"); // optional
sensor.setPayloadAvailable("ready");
sensor.setPayloadNotAvailable("lost");
// Optional multi-topic discovery metadata for external health sources.
// Runtime availability publishing still uses sensor.setAvailability(...).
// sensor.setAvailabilityMode("all");
// sensor.addAvailabilityEntry("bridge/status");
// sensor.addAvailabilityEntry("sensor/health", "{{ value_json.state }}");
// This method enables availability for all device types registered on the device.
// For example, if you have 5 sensors on the same device, you can enable
+2
View File
@@ -33,6 +33,8 @@ void setup() {
// optional properties
buttonA.setIcon("mdi:fire");
buttonA.setName("Click me A");
// buttonA.setPayloadPress("PRESS_A");
// buttonA.setCommandTemplate("{{ value_json.command }}");
buttonB.setIcon("mdi:home");
buttonB.setName("Click me B");
+5
View File
@@ -33,6 +33,11 @@ void setup() {
// set icon (optional)
led.setIcon("mdi:lightbulb");
led.setName("My LED");
// led.setPayloadOn("ENABLE");
// led.setPayloadOff("DISABLE");
// led.setStateOn("running");
// led.setStateOff("stopped");
// led.setCommandTemplate("{{ value_json.command }}");
// handle switch state
led.onCommand(onSwitchCommand);
+3
View File
@@ -52,6 +52,9 @@ void setup() {
// number.setStep(0.5f); // minimum step: 0.001f
// number.setMode(HANumber::ModeBox);
// number.setMode(HANumber::ModeSlider);
// number.setValueTemplate("{{ value_json.level }}");
// number.setPayloadReset("RESET");
// number.setCommandTemplate("{{ value | float | round(1) }}");
// You can set retain flag for the HA commands
// number.setRetain(true);
+2
View File
@@ -30,6 +30,8 @@ void setup() {
analogSensor.setIcon("mdi:home");
analogSensor.setName("Analog voltage");
analogSensor.setUnitOfMeasurement("V");
analogSensor.setSuggestedDisplayPrecision(2);
// analogSensor.setValueTemplate("{{ value_json.voltage }}");
mqtt.begin(BROKER_ADDR);
}
+4
View File
@@ -23,6 +23,10 @@ void setup() {
// configure sensor (optional)
valve.setIcon("mdi:home");
valve.setName("Water valve");
// valve.setValueTemplate("{{ value_json.state }}");
// valve.setDeviceClass("enum");
// valve.setOptions("open;opening;closed");
// valve.setDefaultEntityId("sensor.water_valve");
mqtt.begin(BROKER_ADDR);
}
+225 -9
View File
@@ -2,15 +2,58 @@
#include "HADevice.h"
#include "HAMqtt.h"
#include "utils/HAUtils.h"
#include "utils/HADictionary.h"
#include "utils/HASerializer.h"
#include <string.h>
static bool appendEscapedJsonString(char*& cursor, char* end, const char* value)
{
if (!cursor || !value || cursor >= end) {
return false;
}
if (cursor + 1 >= end) {
return false;
}
*cursor++ = '"';
for (const char* p = value; *p != '\0'; p++) {
if ((*p == '"' || *p == '\\') && cursor + 2 >= end) {
return false;
}
if (*p == '"' || *p == '\\') {
*cursor++ = '\\';
}
if (cursor + 1 >= end) {
return false;
}
*cursor++ = *p;
}
if (cursor + 1 >= end) {
return false;
}
*cursor++ = '"';
*cursor = 0;
return true;
}
#define HADEVICE_INIT \
_ownsUniqueId(false), \
_serializer(new HASerializer(nullptr, 6)), \
_serializer(new HASerializer(nullptr, 16)), \
_availabilityTopic(nullptr), \
_sharedAvailability(false), \
_available(true), \
_extendedUniqueIds(false)
_extendedUniqueIds(false), \
_payloadAvailable(nullptr), \
_payloadNotAvailable(nullptr), \
_connectionsJson(), \
_hasConnections(false), \
_connectionsPropertyRegistered(false)
HADevice::HADevice() :
_uniqueId(nullptr),
@@ -90,6 +133,153 @@ void HADevice::setConfigurationUrl(const char* url)
);
}
void HADevice::setModelId(const char* modelId)
{
if (!modelId) {
return;
}
_serializer->set(AHATOFSTR(HADeviceModelIdProperty), modelId);
}
void HADevice::setHardwareVersion(const char* hardwareVersion)
{
if (!hardwareVersion) {
return;
}
_serializer->set(AHATOFSTR(HADeviceHwVersionProperty), hardwareVersion);
}
void HADevice::setSerialNumber(const char* serialNumber)
{
if (!serialNumber) {
return;
}
_serializer->set(AHATOFSTR(HADeviceSerialNumberProperty), serialNumber);
}
void HADevice::setSuggestedArea(const char* suggestedArea)
{
if (!suggestedArea) {
return;
}
_serializer->set(AHATOFSTR(HADeviceSuggestedAreaProperty), suggestedArea);
}
void HADevice::setViaDevice(const char* viaDevice)
{
if (!viaDevice) {
return;
}
_serializer->set(AHATOFSTR(HADeviceViaDeviceProperty), viaDevice);
}
bool HADevice::addConnection(const char* type, const char* value)
{
if (!type || !value || type[0] == '\0' || value[0] == '\0') {
return false;
}
char next[MaxConnectionsJsonLength];
if (_hasConnections) {
strncpy(next, _connectionsJson, MaxConnectionsJsonLength - 1);
next[MaxConnectionsJsonLength - 1] = 0;
} else {
strcpy(next, "[]");
}
const size_t len = strlen(next);
if (len < 2 || next[len - 1] != ']') {
return false;
}
char* cursor = next + len - 1;
char* end = next + MaxConnectionsJsonLength - 1;
if (cursor > next + 1) {
if (cursor + 1 >= end) {
return false;
}
*cursor++ = ',';
}
if (cursor + 1 >= end) {
return false;
}
*cursor++ = '[';
*cursor = 0;
if (!appendEscapedJsonString(cursor, end, type)) {
return false;
}
if (cursor + 1 >= end) {
return false;
}
*cursor++ = ',';
*cursor = 0;
if (!appendEscapedJsonString(cursor, end, value)) {
return false;
}
if (cursor + 2 >= end) {
return false;
}
*cursor++ = ']';
*cursor++ = ']';
*cursor = 0;
strncpy(_connectionsJson, next, MaxConnectionsJsonLength - 1);
_connectionsJson[MaxConnectionsJsonLength - 1] = 0;
_hasConnections = true;
if (!_connectionsPropertyRegistered) {
_serializer->set(
AHATOFSTR(HADeviceConnectionsProperty),
_connectionsJson,
HASerializer::JsonLiteralPropertyValue
);
_connectionsPropertyRegistered = true;
}
return true;
}
void HADevice::setConnectionsJson(const char* connectionsJson)
{
if (!connectionsJson || connectionsJson[0] == '\0') {
return;
}
strncpy(_connectionsJson, connectionsJson, MaxConnectionsJsonLength - 1);
_connectionsJson[MaxConnectionsJsonLength - 1] = 0;
_hasConnections = true;
if (!_connectionsPropertyRegistered) {
_serializer->set(
AHATOFSTR(HADeviceConnectionsProperty),
_connectionsJson,
HASerializer::JsonLiteralPropertyValue
);
_connectionsPropertyRegistered = true;
}
}
void HADevice::setPayloadAvailable(const char* payload)
{
_payloadAvailable = payload;
}
void HADevice::setPayloadNotAvailable(const char* payload)
{
_payloadNotAvailable = payload;
}
void HADevice::setAvailability(bool online)
{
_available = online;
@@ -131,9 +321,13 @@ void HADevice::enableLastWill()
return;
}
const char* lw = (_payloadNotAvailable && _payloadNotAvailable[0] != '\0')
? _payloadNotAvailable
: "offline";
mqtt->setLastWill(
_availabilityTopic,
"offline",
lw,
true
);
}
@@ -145,11 +339,33 @@ void HADevice::publishAvailability() const
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();
if (_available) {
if (_payloadAvailable && _payloadAvailable[0] != '\0') {
const uint16_t len = strlen(_payloadAvailable);
if (mqtt->beginPublish(_availabilityTopic, len, true)) {
mqtt->writePayload(_payloadAvailable, len);
mqtt->endPublish();
}
} else {
const uint16_t len = strlen_P(HAOnline);
if (mqtt->beginPublish(_availabilityTopic, len, true)) {
mqtt->writePayload(AHATOFSTR(HAOnline));
mqtt->endPublish();
}
}
} else {
if (_payloadNotAvailable && _payloadNotAvailable[0] != '\0') {
const uint16_t len = strlen(_payloadNotAvailable);
if (mqtt->beginPublish(_availabilityTopic, len, true)) {
mqtt->writePayload(_payloadNotAvailable, len);
mqtt->endPublish();
}
} else {
const uint16_t len = strlen_P(HAOffline);
if (mqtt->beginPublish(_availabilityTopic, len, true)) {
mqtt->writePayload(AHATOFSTR(HAOffline));
mqtt->endPublish();
}
}
}
}
+36
View File
@@ -131,6 +131,35 @@ public:
*/
void setConfigurationUrl(const char* url);
void setModelId(const char* modelId);
void setHardwareVersion(const char* hardwareVersion);
void setSerialNumber(const char* serialNumber);
void setSuggestedArea(const char* suggestedArea);
void setViaDevice(const char* viaDevice);
/**
* Adds one MQTT device connection entry (e.g. `("mac", "aa:bb:cc:dd:ee:ff")`).
* Connections are serialized as JSON arrays inside the discovery `dev` object.
*
* @returns Returns `false` when the internal JSON buffer would overflow.
*/
bool addConnection(const char* type, const char* value);
/**
* Sets the `connections` array as raw JSON (e.g. [[\"mac\",\"aa:bb:cc:dd:ee:ff\"]]).
* The payload is copied into an internal buffer.
*/
void setConnectionsJson(const char* connectionsJson);
void setPayloadAvailable(const char* payload);
void setPayloadNotAvailable(const char* payload);
inline const char* getPayloadAvailable() const
{ return _payloadAvailable; }
inline const char* getPayloadNotAvailable() const
{ return _payloadNotAvailable; }
/**
* 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.
@@ -178,6 +207,13 @@ private:
/// Specifies whether extended unique IDs feature is enabled.
bool _extendedUniqueIds;
const char* _payloadAvailable;
const char* _payloadNotAvailable;
static const uint16_t MaxConnectionsJsonLength = 192;
char _connectionsJson[MaxConnectionsJsonLength];
bool _hasConnections;
bool _connectionsPropertyRegistered;
};
#endif
+260 -15
View File
@@ -1,6 +1,7 @@
#include "HAMqtt.h"
#include <cstdio>
#include <cstring>
#ifndef ARDUINOHA_TEST
#include <PubSubClient.h>
@@ -22,6 +23,7 @@
_discoveryPrefix(DefaultDiscoveryPrefix), \
_dataPrefix(DefaultDataPrefix), \
_deviceDiscoveryEnabled(false), \
_originSupportUrl(nullptr), \
_username(nullptr), \
_password(nullptr), \
_lastConnectionAttemptAt(0), \
@@ -32,7 +34,12 @@
_lastWillTopic(nullptr), \
_lastWillMessage(nullptr), \
_lastWillRetain(false), \
_currentState(StateDisconnected)
_currentState(StateDisconnected), \
_messageDispatchDepth(0), \
_deferredQueue{}, \
_deferredHead(0), \
_deferredCount(0), \
_deferredBuilder()
static const char* DefaultDiscoveryPrefix = "homeassistant";
static const char* DefaultDataPrefix = "aha";
@@ -76,6 +83,8 @@ HAMqtt::HAMqtt(
HAMqtt::~HAMqtt()
{
clearDeferredBuilder();
clearDeferredQueue();
delete[] _devicesTypes;
#ifdef ARDUINOHA_TEST
@@ -177,6 +186,7 @@ bool HAMqtt::disconnect()
ARDUINOHA_DEBUG_PRINTLN(F("AHA: disconnecting"))
clearDeferredBuilder();
_initialized = false;
_lastConnectionAttemptAt = 0;
_mqtt->disconnect();
@@ -198,6 +208,10 @@ void HAMqtt::loop()
if (!result) {
connectToServer();
}
if (_messageDispatchDepth == 0 && isConnected()) {
flushDeferredPublishes();
}
}
bool HAMqtt::isConnected() const
@@ -233,17 +247,33 @@ void HAMqtt::addDeviceType(HABaseDeviceType* deviceType)
bool HAMqtt::publish(const char* topic, const char* payload, bool retained)
{
if (!isConnected()) {
if (!isConnected() || !topic || !payload) {
return false;
}
const size_t len = strlen(payload);
if (len > UINT16_MAX) {
return false;
}
const uint16_t payloadLength = static_cast<uint16_t>(len);
ARDUINOHA_DEBUG_PRINT(F("AHA: publishing "))
ARDUINOHA_DEBUG_PRINT(topic)
ARDUINOHA_DEBUG_PRINT(F(", len: "))
ARDUINOHA_DEBUG_PRINTLN(strlen(payload))
ARDUINOHA_DEBUG_PRINTLN(payloadLength)
_mqtt->beginPublish(topic, strlen(payload), retained);
_mqtt->write((const uint8_t*)(payload), strlen(payload));
if (isProcessingMessage()) {
return enqueueDeferredPublish(
topic,
reinterpret_cast<const uint8_t*>(payload),
payloadLength,
retained
);
}
_mqtt->beginPublish(topic, payloadLength, retained);
_mqtt->write(reinterpret_cast<const uint8_t*>(payload), payloadLength);
return _mqtt->endPublish();
}
@@ -258,7 +288,29 @@ bool HAMqtt::beginPublish(
ARDUINOHA_DEBUG_PRINT(F(", len: "))
ARDUINOHA_DEBUG_PRINTLN(payloadLength)
return _mqtt->beginPublish(topic, payloadLength, retained);
if (!isConnected() || !topic) {
return false;
}
if (!isProcessingMessage()) {
return _mqtt->beginPublish(topic, payloadLength, retained);
}
if (_deferredBuilder.active) {
return false;
}
const size_t topicLen = strlen(topic);
_deferredBuilder.topic = new char[topicLen + 1];
memcpy(_deferredBuilder.topic, topic, topicLen + 1);
_deferredBuilder.payload = payloadLength > 0 ? new uint8_t[payloadLength] : nullptr;
_deferredBuilder.expectedLength = payloadLength;
_deferredBuilder.writtenLength = 0;
_deferredBuilder.retained = retained;
_deferredBuilder.active = true;
_deferredBuilder.valid = true;
return true;
}
void HAMqtt::writePayload(const char* data, const uint16_t length)
@@ -268,17 +320,71 @@ void HAMqtt::writePayload(const char* data, const uint16_t length)
void HAMqtt::writePayload(const uint8_t* data, const uint16_t length)
{
if (isProcessingMessage() && _deferredBuilder.active) {
if (!_deferredBuilder.valid ||
(static_cast<uint32_t>(_deferredBuilder.writtenLength) + length) > _deferredBuilder.expectedLength) {
_deferredBuilder.valid = false;
return;
}
if (length > 0) {
memcpy(
_deferredBuilder.payload + _deferredBuilder.writtenLength,
data,
length
);
}
_deferredBuilder.writtenLength = static_cast<uint16_t>(_deferredBuilder.writtenLength + length);
return;
}
_mqtt->write(data, length);
}
void HAMqtt::writePayload(const __FlashStringHelper* src)
{
if (isProcessingMessage() && _deferredBuilder.active) {
PGM_P p = reinterpret_cast<PGM_P>(src);
const uint16_t chunkLen = static_cast<uint16_t>(strlen_P(p));
if (!_deferredBuilder.valid ||
(static_cast<uint32_t>(_deferredBuilder.writtenLength) + chunkLen) > _deferredBuilder.expectedLength) {
_deferredBuilder.valid = false;
return;
}
if (chunkLen > 0) {
memcpy_P(_deferredBuilder.payload + _deferredBuilder.writtenLength, p, chunkLen);
_deferredBuilder.writtenLength = static_cast<uint16_t>(_deferredBuilder.writtenLength + chunkLen);
}
return;
}
_mqtt->print(src);
}
bool HAMqtt::endPublish()
{
return _mqtt->endPublish();
if (!isProcessingMessage()) {
return _mqtt->endPublish();
}
if (!_deferredBuilder.active ||
!_deferredBuilder.valid ||
_deferredBuilder.writtenLength != _deferredBuilder.expectedLength) {
clearDeferredBuilder();
return false;
}
const bool ok = enqueueDeferredPublish(
_deferredBuilder.topic,
_deferredBuilder.payload,
_deferredBuilder.expectedLength,
_deferredBuilder.retained
);
clearDeferredBuilder();
return ok;
}
bool HAMqtt::subscribe(const char* topic)
@@ -296,6 +402,8 @@ void HAMqtt::processMessage(const char* topic, const uint8_t* payload, uint16_t
ARDUINOHA_DEBUG_PRINT(F(", len: "))
ARDUINOHA_DEBUG_PRINTLN(length)
_messageDispatchDepth++;
if (_messageCallback) {
_messageCallback(topic, payload, length);
}
@@ -303,6 +411,12 @@ void HAMqtt::processMessage(const char* topic, const uint8_t* payload, uint16_t
for (uint8_t i = 0; i < _devicesTypesNb; i++) {
_devicesTypes[i]->onMqttMessage(topic, payload, length);
}
_messageDispatchDepth--;
if (_messageDispatchDepth == 0) {
flushDeferredPublishes();
}
}
void HAMqtt::connectToServer()
@@ -398,15 +512,26 @@ bool HAMqtt::publishDeviceDiscovery()
return false;
}
char originPayload[64];
char originPayload[192];
originPayload[0] = 0;
snprintf(
originPayload,
sizeof(originPayload),
"{\"name\":\"%s\",\"sw\":\"%s\"}",
DeviceDiscoveryOriginName,
ARDUINOHA_LIBRARY_VERSION
);
if (_originSupportUrl && _originSupportUrl[0] != '\0') {
snprintf(
originPayload,
sizeof(originPayload),
"{\"name\":\"%s\",\"sw\":\"%s\",\"url\":\"%s\"}",
DeviceDiscoveryOriginName,
ARDUINOHA_LIBRARY_VERSION,
_originSupportUrl
);
} else {
snprintf(
originPayload,
sizeof(originPayload),
"{\"name\":\"%s\",\"sw\":\"%s\"}",
DeviceDiscoveryOriginName,
ARDUINOHA_LIBRARY_VERSION
);
}
const uint16_t originPayloadLength = strlen(originPayload);
const uint16_t topicLength =
@@ -517,4 +642,124 @@ void HAMqtt::setState(ConnectionState state)
if (_stateChangedCallback) {
_stateChangedCallback(_currentState);
}
}
bool HAMqtt::enqueueDeferredPublish(
const char* topic,
const uint8_t* payload,
uint16_t length,
bool retained
)
{
if (!topic || _deferredCount >= DeferredQueueCapacity) {
return false;
}
if (length > 0 && payload == nullptr) {
return false;
}
const uint8_t slot = static_cast<uint8_t>((_deferredHead + _deferredCount) % DeferredQueueCapacity);
DeferredPublishMessage& msg = _deferredQueue[slot];
const size_t topicLen = strlen(topic);
msg.topic = new char[topicLen + 1];
memcpy(msg.topic, topic, topicLen + 1);
if (length > 0) {
msg.payload = new uint8_t[length];
memcpy(msg.payload, payload, length);
} else {
msg.payload = nullptr;
}
msg.length = length;
msg.retained = retained;
_deferredCount++;
#ifdef ARDUINOHA_TEST
_deferredPublishEnqueueCountForTest++;
#endif
return true;
}
void HAMqtt::clearDeferredMessage(DeferredPublishMessage& msg)
{
delete[] msg.topic;
delete[] msg.payload;
msg.topic = nullptr;
msg.payload = nullptr;
msg.length = 0;
msg.retained = false;
}
void HAMqtt::clearDeferredQueue()
{
while (_deferredCount > 0) {
DeferredPublishMessage& msg = _deferredQueue[_deferredHead];
clearDeferredMessage(msg);
_deferredHead = static_cast<uint8_t>((_deferredHead + 1) % DeferredQueueCapacity);
_deferredCount--;
}
_deferredHead = 0;
}
void HAMqtt::clearDeferredBuilder()
{
delete[] _deferredBuilder.topic;
delete[] _deferredBuilder.payload;
_deferredBuilder.topic = nullptr;
_deferredBuilder.payload = nullptr;
_deferredBuilder.expectedLength = 0;
_deferredBuilder.writtenLength = 0;
_deferredBuilder.retained = false;
_deferredBuilder.active = false;
_deferredBuilder.valid = false;
}
bool HAMqtt::flushDeferredPublishes()
{
while (_deferredCount > 0) {
DeferredPublishMessage& msg = _deferredQueue[_deferredHead];
if (!isConnected()) {
#ifdef ARDUINOHA_TEST
_deferredFlushFailedForTest = true;
_lastDeferredFlushErrorForTest = DeferredFlushErrorNotConnected;
#endif
return false;
}
if (!_mqtt->beginPublish(msg.topic, msg.length, msg.retained)) {
#ifdef ARDUINOHA_TEST
_deferredFlushFailedForTest = true;
_lastDeferredFlushErrorForTest = DeferredFlushErrorBeginPublish;
#endif
return false;
}
if (msg.length > 0 && msg.payload != nullptr) {
_mqtt->write(msg.payload, msg.length);
}
if (!_mqtt->endPublish()) {
#ifdef ARDUINOHA_TEST
_deferredFlushFailedForTest = true;
_lastDeferredFlushErrorForTest = DeferredFlushErrorEndPublish;
#endif
return false;
}
clearDeferredMessage(msg);
_deferredHead = static_cast<uint8_t>((_deferredHead + 1) % DeferredQueueCapacity);
_deferredCount--;
}
#ifdef ARDUINOHA_TEST
_deferredFlushFailedForTest = false;
_lastDeferredFlushErrorForTest = DeferredFlushErrorNone;
#endif
return true;
}
+110
View File
@@ -136,6 +136,15 @@ public:
*/
bool publishDeviceDiscovery();
/**
* Sets optional `support_url` in the discovery `origin` object (must remain valid).
*/
inline void setOriginSupportUrl(const char* url)
{ _originSupportUrl = url; }
inline const char* getOriginSupportUrl() const
{ return _originSupportUrl; }
/**
* Returns instance of the device assigned to the HAMqtt class.
* It's the same object (pointer) that was passed to the HAMqtt constructor.
@@ -310,6 +319,10 @@ public:
* Message won't be published if the connection with the MQTT broker is not established.
* In this case method returns false.
*
* While handling an inbound MQTT message (see isProcessingMessage()), a successful
* return means the message was queued for send after dispatch completes, not that it
* was already transmitted. Outside that context, behavior is unchanged.
*
* @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.
@@ -402,12 +415,47 @@ public:
*/
void processMessage(const char* topic, const uint8_t* payload, uint16_t length);
/**
* True while handling an inbound MQTT message (user onMessage and device onMqttMessage).
* Publish attempts in this window are queued and flushed when dispatch completes.
*/
inline bool isProcessingMessage() const
{ return _messageDispatchDepth > 0; }
#ifdef ARDUINOHA_TEST
inline uint8_t getDevicesTypesNb() const
{ return _devicesTypesNb; }
inline HABaseDeviceType** getDevicesTypes() const
{ return _devicesTypes; }
inline uint16_t getDeferredPublishEnqueueCountForTest() const
{ return _deferredPublishEnqueueCountForTest; }
inline void resetDeferredPublishTestCounters()
{
_deferredPublishEnqueueCountForTest = 0;
_deferredFlushFailedForTest = false;
_lastDeferredFlushErrorForTest = 0;
}
inline uint8_t getPendingDeferredPublishesForTest() const
{ return _deferredCount; }
inline bool hasDeferredFlushFailureForTest() const
{ return _deferredFlushFailedForTest; }
inline uint8_t getLastDeferredFlushErrorForTest() const
{ return _lastDeferredFlushErrorForTest; }
inline bool didDeferredFlushFailDueToDisconnectForTest() const
{ return _lastDeferredFlushErrorForTest == 1; }
inline bool didDeferredFlushFailAtBeginPublishForTest() const
{ return _lastDeferredFlushErrorForTest == 2; }
inline bool didDeferredFlushFailAtEndPublishForTest() const
{ return _lastDeferredFlushErrorForTest == 3; }
#endif
private:
@@ -433,6 +481,51 @@ private:
*/
void setState(ConnectionState state);
struct DeferredPublishMessage {
char* topic;
uint8_t* payload;
uint16_t length;
bool retained;
};
struct DeferredPublishBuilder {
bool active;
bool valid;
char* topic;
uint8_t* payload;
uint16_t expectedLength;
uint16_t writtenLength;
bool retained;
};
enum DeferredFlushError {
DeferredFlushErrorNone = 0,
DeferredFlushErrorNotConnected,
DeferredFlushErrorBeginPublish,
DeferredFlushErrorEndPublish
};
static const uint8_t DeferredQueueCapacity = 8;
bool enqueueDeferredPublish(
const char* topic,
const uint8_t* payload,
uint16_t length,
bool retained
);
void clearDeferredMessage(DeferredPublishMessage& msg);
void clearDeferredQueue();
void clearDeferredBuilder();
/**
* Sends queued publishes in order. On transport failure, stops and leaves the
* remaining queue intact for a later retry (e.g. from loop()).
*/
bool flushDeferredPublishes();
#ifdef ARDUINOHA_TEST
PubSubClientMock* _mqtt;
#else
@@ -470,6 +563,8 @@ private:
/// Enables MQTT device discovery mode when set to true.
bool _deviceDiscoveryEnabled;
const char* _originSupportUrl;
/// The username used for the authentication. It's set in the HAMqtt::begin method.
const char* _username;
@@ -502,6 +597,21 @@ private:
/// The last known state of the MQTT connection.
ConnectionState _currentState;
/// Nesting depth for inbound message dispatch (processMessage).
uint8_t _messageDispatchDepth;
DeferredPublishMessage _deferredQueue[DeferredQueueCapacity];
uint8_t _deferredHead;
uint8_t _deferredCount;
DeferredPublishBuilder _deferredBuilder;
#ifdef ARDUINOHA_TEST
uint16_t _deferredPublishEnqueueCountForTest = 0;
bool _deferredFlushFailedForTest = false;
uint8_t _lastDeferredFlushErrorForTest = 0;
#endif
};
#endif
+312 -6
View File
@@ -2,7 +2,9 @@
#include "../HAMqtt.h"
#include "../HADevice.h"
#include "../utils/HAUtils.h"
#include "../utils/HADictionary.h"
#include "../utils/HASerializer.h"
#include <string.h>
HABaseDeviceType::HABaseDeviceType(
const __FlashStringHelper* componentName,
@@ -15,6 +17,16 @@ HABaseDeviceType::HABaseDeviceType(
_defaultEntityId(nullptr),
_entityCategory(nullptr),
_serializer(nullptr),
_hasEnabledByDefault(false),
_enabledByDefault(true),
_entityPicture(nullptr),
_hasQos(false),
_qosNumeric(),
_encoding(nullptr),
_payloadAvailable(nullptr),
_payloadNotAvailable(nullptr),
_availabilityMode(nullptr),
_availabilityList(),
_availability(AvailabilityDefault)
{
if (mqtt()) {
@@ -166,13 +178,86 @@ void HABaseDeviceType::publishAvailability()
return;
}
publishOnDataTopic(
AHATOFSTR(HAAvailabilityTopic),
_availability == AvailabilityOnline
const bool online = (_availability == AvailabilityOnline);
if (_availabilityList.count() > 0) {
char defaultBuf[12];
const __FlashStringHelper* flashPayload = online
? AHATOFSTR(HAOnline)
: AHATOFSTR(HAOffline),
true
);
: AHATOFSTR(HAOffline);
for (uint8_t i = 0; i < _availabilityList.count(); i++) {
const HAAvailabilityConfig::Entry& e = _availabilityList.getEntry(i);
const char* payload = nullptr;
if (online) {
if (e.payloadAvailable && e.payloadAvailable[0] != '\0') {
payload = e.payloadAvailable;
} else if (_payloadAvailable && _payloadAvailable[0] != '\0') {
payload = _payloadAvailable;
} else {
strncpy_P(
defaultBuf,
reinterpret_cast<PGM_P>(AHAFROMFSTR(flashPayload)),
sizeof(defaultBuf) - 1
);
defaultBuf[sizeof(defaultBuf) - 1] = 0;
payload = defaultBuf;
}
} else {
if (e.payloadNotAvailable && e.payloadNotAvailable[0] != '\0') {
payload = e.payloadNotAvailable;
} else if (_payloadNotAvailable && _payloadNotAvailable[0] != '\0') {
payload = _payloadNotAvailable;
} else {
strncpy_P(
defaultBuf,
reinterpret_cast<PGM_P>(AHAFROMFSTR(flashPayload)),
sizeof(defaultBuf) - 1
);
defaultBuf[sizeof(defaultBuf) - 1] = 0;
payload = defaultBuf;
}
}
publishAbsolute(e.topic, payload, true);
}
return;
}
const char* ramPayload = online
? effectivePayloadAvailable()
: effectivePayloadNotAvailable();
if (ramPayload) {
publishOnDataTopic(AHATOFSTR(HAAvailabilityTopic), ramPayload, true);
} else {
publishOnDataTopic(
AHATOFSTR(HAAvailabilityTopic),
online ? AHATOFSTR(HAOnline) : AHATOFSTR(HAOffline),
true
);
}
}
bool HABaseDeviceType::publishAbsolute(
const char* fullTopic,
const char* payload,
bool retained
)
{
if (!fullTopic || !payload || !mqtt()) {
return false;
}
const uint16_t len = strlen(payload);
if (!mqtt()->beginPublish(fullTopic, len, retained)) {
return false;
}
mqtt()->writePayload(payload, len);
return mqtt()->endPublish();
}
bool HABaseDeviceType::publishOnDataTopic(
@@ -290,4 +375,225 @@ HASerializer* HABaseDeviceType::buildDeviceDiscoverySerializer()
bool HABaseDeviceType::supportsDeviceDiscovery() const
{
return false;
}
void HABaseDeviceType::setEnabledByDefault(bool enabled)
{
_enabledByDefault = enabled;
_hasEnabledByDefault = true;
}
void HABaseDeviceType::clearEnabledByDefault()
{
_hasEnabledByDefault = false;
}
void HABaseDeviceType::setEntityPicture(const char* url)
{
_entityPicture = url;
}
void HABaseDeviceType::clearEntityPicture()
{
_entityPicture = nullptr;
}
void HABaseDeviceType::setQos(uint8_t qos)
{
if (qos > 2) {
qos = 2;
}
_qosNumeric = HANumeric(static_cast<uint8_t>(qos), 0);
_hasQos = true;
}
void HABaseDeviceType::clearQos()
{
_hasQos = false;
_qosNumeric.reset();
}
void HABaseDeviceType::setEncoding(const char* encoding)
{
_encoding = encoding;
}
void HABaseDeviceType::clearEncoding()
{
_encoding = nullptr;
}
void HABaseDeviceType::setPayloadAvailable(const char* payload)
{
_payloadAvailable = payload;
}
void HABaseDeviceType::setPayloadNotAvailable(const char* payload)
{
_payloadNotAvailable = payload;
}
void HABaseDeviceType::setAvailabilityMode(const char* mode)
{
_availabilityMode = mode;
}
bool HABaseDeviceType::addAvailabilityEntry(
const char* topic,
const char* valueTemplate,
const char* payloadAvailable,
const char* payloadNotAvailable
)
{
return _availabilityList.add(
topic,
valueTemplate,
payloadAvailable,
payloadNotAvailable
);
}
void HABaseDeviceType::clearAvailabilityEntries()
{
_availabilityList.clear();
}
void HABaseDeviceType::applyCommonEntityProperties(
HASerializer* serializer,
bool includeEncoding
) const
{
if (!serializer) {
return;
}
if (_hasEnabledByDefault) {
serializer->set(
AHATOFSTR(HAEnabledByDefaultProperty),
&_enabledByDefault,
HASerializer::BoolPropertyType
);
}
if (nonEmptyString(_entityPicture)) {
serializer->set(AHATOFSTR(HAEntityPictureProperty), _entityPicture);
}
if (_hasQos) {
serializer->set(
AHATOFSTR(HAQosProperty),
&_qosNumeric,
HASerializer::NumberPropertyType
);
}
if (includeEncoding && nonEmptyString(_encoding)) {
serializer->set(AHATOFSTR(HAEncodingProperty), _encoding);
}
}
const char* HABaseDeviceType::effectivePayloadAvailable() const
{
if (_payloadAvailable && _payloadAvailable[0] != '\0') {
return _payloadAvailable;
}
HAMqtt* m = mqtt();
if (!m) {
return nullptr;
}
const HADevice* d = m->getDevice();
if (d && d->isSharedAvailabilityEnabled()) {
return d->getPayloadAvailable();
}
return nullptr;
}
const char* HABaseDeviceType::effectivePayloadNotAvailable() const
{
if (_payloadNotAvailable && _payloadNotAvailable[0] != '\0') {
return _payloadNotAvailable;
}
HAMqtt* m = mqtt();
if (!m) {
return nullptr;
}
const HADevice* d = m->getDevice();
if (d && d->isSharedAvailabilityEnabled()) {
return d->getPayloadNotAvailable();
}
return nullptr;
}
void HABaseDeviceType::configureAvailabilityEntries(HASerializer* serializer) const
{
if (!serializer || !mqtt()) {
return;
}
const HADevice* device = mqtt()->getDevice();
if (!device) {
return;
}
const bool isSharedAvailability = device->isSharedAvailabilityEnabled();
const bool availabilityActive = isAvailabilityConfigured();
if (!isSharedAvailability && !availabilityActive) {
return;
}
const char* pa = effectivePayloadAvailable();
if (nonEmptyString(pa)) {
serializer->set(
AHATOFSTR(HAPayloadAvailableProperty),
pa,
HASerializer::ConstCharPropertyValue
);
}
const char* pn = effectivePayloadNotAvailable();
if (nonEmptyString(pn)) {
serializer->set(
AHATOFSTR(HAPayloadNotAvailableProperty),
pn,
HASerializer::ConstCharPropertyValue
);
}
if (nonEmptyString(_availabilityMode)) {
serializer->set(
AHATOFSTR(HAAvailabilityModeProperty),
_availabilityMode,
HASerializer::ConstCharPropertyValue
);
}
const bool useList =
!isSharedAvailability
&& availabilityActive
&& _availabilityList.count() > 0;
if (useList) {
auto* entry = serializer->addEntry();
entry->type = HASerializer::AvailabilityArrayEntryType;
entry->subtype = 0;
entry->property = AHATOFSTR(HAAvailabilityListProperty);
entry->value = const_cast<HAAvailabilityConfig*>(&_availabilityList);
return;
}
auto* e = serializer->addEntry();
e->type = HASerializer::TopicEntryType;
e->subtype = 0;
e->property = AHATOFSTR(HAAvailabilityTopic);
e->value = isSharedAvailability
? const_cast<char*>(device->getAvailabilityTopic())
: nullptr;
}
+66
View File
@@ -3,6 +3,8 @@
#include <Arduino.h>
#include "../ArduinoHADefines.h"
#include "../utils/HAAvailabilityConfig.h"
#include "../utils/HANumeric.h"
class HAMqtt;
class HASerializer;
@@ -127,6 +129,36 @@ public:
inline const char* getEntityCategory() const
{ return _entityCategory; }
void setEnabledByDefault(bool enabled);
void clearEnabledByDefault();
void setEntityPicture(const char* url);
void clearEntityPicture();
void setQos(uint8_t qos);
void clearQos();
void setEncoding(const char* encoding);
void clearEncoding();
void setPayloadAvailable(const char* payload);
void setPayloadNotAvailable(const char* payload);
void setAvailabilityMode(const char* mode);
/**
* Adds a full-topic availability entry for discovery (`avty` list) and publishing.
* Only used when this entity does not use shared device availability.
*/
bool addAvailabilityEntry(
const char* topic,
const char* valueTemplate = nullptr,
const char* payloadAvailable = nullptr,
const char* payloadNotAvailable = nullptr
);
void clearAvailabilityEntries();
/**
* Sets availability of the device type.
* Setting the initial availability enables availability reporting for this device type.
@@ -186,6 +218,19 @@ protected:
*/
virtual void buildSerializer() { };
/**
* Writes shared MQTT discovery fields (enabled_by_default, entity_picture, qos, encoding).
*/
void applyCommonEntityProperties(
HASerializer* serializer,
bool includeEncoding = true
) const;
/**
* Populates availability-related discovery entries. Called from HASerializer::WithAvailability.
*/
void configureAvailabilityEntries(HASerializer* serializer) const;
/**
* This method is called each time the MQTT connection is acquired.
* Each device type should publish its configuration and availability.
@@ -266,6 +311,11 @@ protected:
bool isProgmemData = false
);
/**
* Publishes a RAM payload on an already fully-qualified MQTT topic.
*/
bool publishAbsolute(const char* fullTopic, const char* payload, bool retained = false);
/**
* Adds the preferred entity ID property to the serializer.
* `default_entity_id` takes precedence and the legacy `object_id` is only
@@ -310,6 +360,17 @@ protected:
/// HASerializer that belongs to this device type. It can be nullptr.
HASerializer* _serializer;
bool _hasEnabledByDefault;
bool _enabledByDefault;
const char* _entityPicture;
bool _hasQos;
HANumeric _qosNumeric;
const char* _encoding;
const char* _payloadAvailable;
const char* _payloadNotAvailable;
const char* _availabilityMode;
HAAvailabilityConfig _availabilityList;
private:
enum Availability {
AvailabilityDefault = 0,
@@ -319,7 +380,12 @@ private:
/// The current availability of this device type. AvailabilityDefault means that the initial availability was never set.
Availability _availability;
const char* effectivePayloadAvailable() const;
const char* effectivePayloadNotAvailable() const;
friend class HAMqtt;
friend class HASerializer;
};
#endif
+113 -9
View File
@@ -2,12 +2,18 @@
#ifndef EX_ARDUINOHA_BINARY_SENSOR
#include "../HAMqtt.h"
#include "../utils/HADictionary.h"
#include "../utils/HASerializer.h"
HABinarySensor::HABinarySensor(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentBinarySensor), uniqueId),
_class(nullptr),
_icon(nullptr),
_payloadOn(nullptr),
_payloadOff(nullptr),
_forceUpdate(false),
_offDelay(),
_valueTemplate(nullptr),
_currentState(false)
{
@@ -19,12 +25,9 @@ bool HABinarySensor::setState(const bool state, const bool force)
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
const bool published = publishState(state);
_currentState = state;
return published;
}
void HABinarySensor::setExpireAfter(uint16_t expireAfter)
@@ -36,20 +39,68 @@ void HABinarySensor::setExpireAfter(uint16_t expireAfter)
}
}
void HABinarySensor::setOffDelay(uint16_t seconds)
{
if (seconds > 0) {
_offDelay.setBaseValue(seconds);
} else {
_offDelay.reset();
}
}
void HABinarySensor::clearOffDelay()
{
_offDelay.reset();
}
void HABinarySensor::setValueTemplate(const char* valueTemplate)
{
_valueTemplate = valueTemplate;
}
void HABinarySensor::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 10); // 10 - max properties nb
_serializer = new HASerializer(this, 20);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
if (nonEmptyString(_payloadOn)) {
_serializer->set(AHATOFSTR(HAPayloadOnProperty), _payloadOn);
}
if (nonEmptyString(_payloadOff)) {
_serializer->set(AHATOFSTR(HAPayloadOffProperty), _payloadOff);
}
if (_forceUpdate) {
_serializer->set(
AHATOFSTR(HAForceUpdateProperty),
&_forceUpdate,
HASerializer::BoolPropertyType
);
}
if (_offDelay.isSet()) {
_serializer->set(
AHATOFSTR(HAOffDelayProperty),
&_offDelay,
HASerializer::NumberPropertyType
);
}
if (nonEmptyString(_valueTemplate)) {
_serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (_expireAfter.isSet()) {
_serializer->set(
AHATOFSTR(HAExpireAfterProperty),
@@ -69,7 +120,7 @@ HASerializer* HABinarySensor::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 10);
HASerializer* serializer = new HASerializer(this, 20);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentBinarySensor),
@@ -78,10 +129,39 @@ HASerializer* HABinarySensor::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
if (nonEmptyString(_payloadOn)) {
serializer->set(AHATOFSTR(HAPayloadOnProperty), _payloadOn);
}
if (nonEmptyString(_payloadOff)) {
serializer->set(AHATOFSTR(HAPayloadOffProperty), _payloadOff);
}
if (_forceUpdate) {
serializer->set(
AHATOFSTR(HAForceUpdateProperty),
&_forceUpdate,
HASerializer::BoolPropertyType
);
}
if (_offDelay.isSet()) {
serializer->set(
AHATOFSTR(HAOffDelayProperty),
&_offDelay,
HASerializer::NumberPropertyType
);
}
if (nonEmptyString(_valueTemplate)) {
serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (_expireAfter.isSet()) {
serializer->set(
AHATOFSTR(HAExpireAfterProperty),
@@ -110,9 +190,33 @@ void HABinarySensor::onMqttConnected()
bool HABinarySensor::publishState(const bool state)
{
if (state) {
if (nonEmptyString(_payloadOn)) {
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
_payloadOn,
true
);
}
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
AHATOFSTR(HAStateOn),
true
);
}
if (nonEmptyString(_payloadOff)) {
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
_payloadOff,
true
);
}
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
AHATOFSTR(state ? HAStateOn : HAStateOff),
AHATOFSTR(HAStateOff),
true
);
}
+20
View File
@@ -72,6 +72,20 @@ public:
inline void setIcon(const char* icon)
{ _icon = icon; }
inline void setPayloadOn(const char* payload)
{ _payloadOn = payload; }
inline void setPayloadOff(const char* payload)
{ _payloadOff = payload; }
inline void setForceUpdate(bool forceUpdate)
{ _forceUpdate = forceUpdate; }
void setOffDelay(uint16_t seconds);
void clearOffDelay();
void setValueTemplate(const char* valueTemplate);
protected:
virtual void buildSerializer() override;
virtual HASerializer* buildDeviceDiscoverySerializer() override;
@@ -97,6 +111,12 @@ private:
/// It defines the number of seconds after the sensor’s state expires, if it’s not updated. By default the sensors state never expires.
HANumeric _expireAfter;
const char* _payloadOn;
const char* _payloadOff;
bool _forceUpdate;
HANumeric _offDelay;
const char* _valueTemplate;
/// Current state of the sensor. By default it's false.
bool _currentState;
+44 -2
View File
@@ -2,32 +2,55 @@
#ifndef EX_ARDUINOHA_BUTTON
#include "../HAMqtt.h"
#include "../utils/HADictionary.h"
#include "../utils/HASerializer.h"
#include <string.h>
HAButton::HAButton(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentButton), uniqueId),
_class(nullptr),
_icon(nullptr),
_retain(false),
_payloadPress(nullptr),
_commandTemplate(nullptr),
_commandCallback(nullptr)
{
}
void HAButton::setPayloadPress(const char* payload)
{
_payloadPress = payload;
}
void HAButton::setCommandTemplate(const char* commandTemplate)
{
_commandTemplate = commandTemplate;
}
void HAButton::buildSerializer()
{
if (_serializer || !uniqueId()) {
return;
}
_serializer = new HASerializer(this, 10); // 10 - max properties nb
_serializer = new HASerializer(this, 18);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
if (nonEmptyString(_payloadPress)) {
_serializer->set(AHATOFSTR(HAPayloadPressProperty), _payloadPress);
}
if (nonEmptyString(_commandTemplate)) {
_serializer->set(AHATOFSTR(HACommandTemplateProperty), _commandTemplate);
}
// optional property
if (_retain) {
_serializer->set(
@@ -48,7 +71,7 @@ HASerializer* HAButton::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 10);
HASerializer* serializer = new HASerializer(this, 18);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentButton),
@@ -57,10 +80,19 @@ HASerializer* HAButton::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
if (nonEmptyString(_payloadPress)) {
serializer->set(AHATOFSTR(HAPayloadPressProperty), _payloadPress);
}
if (nonEmptyString(_commandTemplate)) {
serializer->set(AHATOFSTR(HACommandTemplateProperty), _commandTemplate);
}
if (_retain) {
serializer->set(
AHATOFSTR(HARetainProperty),
@@ -108,6 +140,16 @@ void HAButton::onMqttMessage(
uniqueId(),
AHATOFSTR(HACommandTopic)
)) {
if (
nonEmptyString(_payloadPress)
&& (
length != strlen(_payloadPress)
|| memcmp(payload, _payloadPress, length) != 0
)
) {
return;
}
if (_commandCallback) {
_commandCallback(this);
}
+6
View File
@@ -54,6 +54,9 @@ public:
inline void setRetain(const bool retain)
{ _retain = retain; }
void setPayloadPress(const char* payload);
void setCommandTemplate(const char* commandTemplate);
/**
* 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.
@@ -104,6 +107,9 @@ private:
/// The retain flag for the HA commands.
bool _retain;
const char* _payloadPress;
const char* _commandTemplate;
/// The command callback that will be called once clicking the button in HA panel.
HABUTTON_CALLBACK(_commandCallback);
+4 -2
View File
@@ -27,10 +27,11 @@ void HACamera::buildSerializer()
return;
}
_serializer = new HASerializer(this, 9); // 9 - max properties nb
_serializer = new HASerializer(this, 18);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer, false);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
_serializer->set(
@@ -49,7 +50,7 @@ HASerializer* HACamera::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 9);
HASerializer* serializer = new HASerializer(this, 18);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentCamera),
@@ -58,6 +59,7 @@ HASerializer* HACamera::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer, false);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
serializer->set(
+14 -10
View File
@@ -29,12 +29,13 @@ bool HACover::setState(const CoverState state, const bool force)
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
if (state == StateUnknown) {
return false;
}
return false;
const bool published = publishState(state);
_currentState = state;
return published;
}
bool HACover::setPosition(const int16_t position, const bool force)
@@ -43,12 +44,13 @@ bool HACover::setPosition(const int16_t position, const bool force)
return true;
}
if (publishPosition(position)) {
_currentPosition = position;
return true;
if (position == DefaultPosition || !(_features & PositionFeature)) {
return false;
}
return false;
const bool published = publishPosition(position);
_currentPosition = position;
return published;
}
void HACover::buildSerializer()
@@ -57,10 +59,11 @@ void HACover::buildSerializer()
return;
}
_serializer = new HASerializer(this, 13); // 13 - max properties nb
_serializer = new HASerializer(this, 22);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
@@ -97,7 +100,7 @@ HASerializer* HACover::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 13);
HASerializer* serializer = new HASerializer(this, 22);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentCover),
@@ -106,6 +109,7 @@ HASerializer* HACover::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
+9 -6
View File
@@ -19,12 +19,13 @@ bool HADeviceTracker::setState(const TrackerState state, const bool force)
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
if (state == StateUnknown) {
return false;
}
return false;
const bool published = publishState(state);
_currentState = state;
return published;
}
void HADeviceTracker::buildSerializer()
@@ -33,10 +34,11 @@ void HADeviceTracker::buildSerializer()
return;
}
_serializer = new HASerializer(this, 9); // 9 - max properties nb
_serializer = new HASerializer(this, 18);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
_serializer->set(
@@ -55,7 +57,7 @@ HASerializer* HADeviceTracker::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 9);
HASerializer* serializer = new HASerializer(this, 18);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentDeviceTracker),
@@ -64,6 +66,7 @@ HASerializer* HADeviceTracker::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
serializer->set(
+12 -12
View File
@@ -31,12 +31,9 @@ bool HAFan::setState(const bool state, const bool force)
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
const bool published = publishState(state);
_currentState = state;
return published;
}
bool HAFan::setSpeed(const uint16_t speed, const bool force)
@@ -45,12 +42,13 @@ bool HAFan::setSpeed(const uint16_t speed, const bool force)
return true;
}
if (publishSpeed(speed)) {
_currentSpeed = speed;
return true;
if (!(_features & SpeedsFeature)) {
return false;
}
return false;
const bool published = publishSpeed(speed);
_currentSpeed = speed;
return published;
}
void HAFan::buildSerializer()
@@ -59,10 +57,11 @@ void HAFan::buildSerializer()
return;
}
_serializer = new HASerializer(this, 15); // 15 - max properties nb
_serializer = new HASerializer(this, 24);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
@@ -115,7 +114,7 @@ HASerializer* HAFan::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 15);
HASerializer* serializer = new HASerializer(this, 24);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentFan),
@@ -124,6 +123,7 @@ HASerializer* HAFan::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
+25 -44
View File
@@ -89,12 +89,9 @@ bool HAHVAC::setCurrentTemperature(const HANumeric& temperature, const bool forc
return true;
}
if (publishCurrentTemperature(temperature)) {
_currentTemperature = temperature;
return true;
}
return false;
const bool published = publishCurrentTemperature(temperature);
_currentTemperature = temperature;
return published;
}
bool HAHVAC::setAction(const Action action, const bool force)
@@ -103,12 +100,9 @@ bool HAHVAC::setAction(const Action action, const bool force)
return true;
}
if (publishAction(action)) {
_action = action;
return true;
}
return false;
const bool published = publishAction(action);
_action = action;
return published;
}
bool HAHVAC::setAuxState(const bool state, const bool force)
@@ -117,12 +111,9 @@ bool HAHVAC::setAuxState(const bool state, const bool force)
return true;
}
if (publishAuxState(state)) {
_auxState = state;
return true;
}
return false;
const bool published = publishAuxState(state);
_auxState = state;
return published;
}
bool HAHVAC::setFanMode(const FanMode mode, const bool force)
@@ -131,12 +122,9 @@ bool HAHVAC::setFanMode(const FanMode mode, const bool force)
return true;
}
if (publishFanMode(mode)) {
_fanMode = mode;
return true;
}
return false;
const bool published = publishFanMode(mode);
_fanMode = mode;
return published;
}
bool HAHVAC::setSwingMode(const SwingMode mode, const bool force)
@@ -145,12 +133,9 @@ bool HAHVAC::setSwingMode(const SwingMode mode, const bool force)
return true;
}
if (publishSwingMode(mode)) {
_swingMode = mode;
return true;
}
return false;
const bool published = publishSwingMode(mode);
_swingMode = mode;
return published;
}
bool HAHVAC::setMode(const Mode mode, const bool force)
@@ -159,12 +144,9 @@ bool HAHVAC::setMode(const Mode mode, const bool force)
return true;
}
if (publishMode(mode)) {
_mode = mode;
return true;
}
return false;
const bool published = publishMode(mode);
_mode = mode;
return published;
}
bool HAHVAC::setTargetTemperature(const HANumeric& temperature, const bool force)
@@ -177,12 +159,9 @@ bool HAHVAC::setTargetTemperature(const HANumeric& temperature, const bool force
return true;
}
if (publishTargetTemperature(temperature)) {
_targetTemperature = temperature;
return true;
}
return false;
const bool published = publishTargetTemperature(temperature);
_targetTemperature = temperature;
return published;
}
void HAHVAC::buildSerializer()
@@ -191,10 +170,11 @@ void HAHVAC::buildSerializer()
return;
}
_serializer = new HASerializer(this, 29); // 29 - max properties nb
_serializer = new HASerializer(this, 40);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
@@ -369,7 +349,7 @@ HASerializer* HAHVAC::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 29);
HASerializer* serializer = new HASerializer(this, 40);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentClimate),
@@ -378,6 +358,7 @@ HASerializer* HAHVAC::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
+22 -20
View File
@@ -78,12 +78,9 @@ bool HALight::setState(const bool state, const bool force)
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
const bool published = publishState(state);
_currentState = state;
return published;
}
bool HALight::setBrightness(const uint8_t brightness, const bool force)
@@ -92,12 +89,13 @@ bool HALight::setBrightness(const uint8_t brightness, const bool force)
return true;
}
if (publishBrightness(brightness)) {
_currentBrightness = brightness;
return true;
if (!(_features & BrightnessFeature)) {
return false;
}
return false;
const bool published = publishBrightness(brightness);
_currentBrightness = brightness;
return published;
}
bool HALight::setColorTemperature(const uint16_t temperature, const bool force)
@@ -106,12 +104,13 @@ bool HALight::setColorTemperature(const uint16_t temperature, const bool force)
return true;
}
if (publishColorTemperature(temperature)) {
_currentColorTemperature = temperature;
return true;
if (!(_features & ColorTemperatureFeature)) {
return false;
}
return false;
const bool published = publishColorTemperature(temperature);
_currentColorTemperature = temperature;
return published;
}
bool HALight::setRGBColor(const RGBColor& color, const bool force)
@@ -120,12 +119,13 @@ bool HALight::setRGBColor(const RGBColor& color, const bool force)
return true;
}
if (publishRGBColor(color)) {
_currentRGBColor = color;
return true;
if (!(_features & RGBFeature) || !color.isSet) {
return false;
}
return false;
const bool published = publishRGBColor(color);
_currentRGBColor = color;
return published;
}
void HALight::buildSerializer()
@@ -134,10 +134,11 @@ void HALight::buildSerializer()
return;
}
_serializer = new HASerializer(this, 20); // 20 - max properties nb
_serializer = new HASerializer(this, 30);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
@@ -208,7 +209,7 @@ HASerializer* HALight::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 20);
HASerializer* serializer = new HASerializer(this, 30);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentLight),
@@ -217,6 +218,7 @@ HASerializer* HALight::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
+9 -6
View File
@@ -24,12 +24,13 @@ bool HALock::setState(const LockState state, const bool force)
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
if (state == StateUnknown) {
return false;
}
return false;
const bool published = publishState(state);
_currentState = state;
return published;
}
void HALock::buildSerializer()
@@ -38,10 +39,11 @@ void HALock::buildSerializer()
return;
}
_serializer = new HASerializer(this, 11); // 11 - max properties nb
_serializer = new HASerializer(this, 20);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
@@ -73,7 +75,7 @@ HASerializer* HALock::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 11);
HASerializer* serializer = new HASerializer(this, 20);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentLock),
@@ -82,6 +84,7 @@ HASerializer* HALock::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
+88 -20
View File
@@ -2,7 +2,9 @@
#ifndef EX_ARDUINOHA_NUMBER
#include "../HAMqtt.h"
#include "../utils/HADictionary.h"
#include "../utils/HASerializer.h"
#include <string.h>
HANumber::HANumber(const char* uniqueId, const NumberPrecision precision) :
HABaseDeviceType(AHATOFSTR(HAComponentNumber), uniqueId),
@@ -17,23 +19,38 @@ HANumber::HANumber(const char* uniqueId, const NumberPrecision precision) :
_maxValue(),
_step(),
_currentState(),
_commandCallback(nullptr)
_commandCallback(nullptr),
_commandTemplate(nullptr),
_valueTemplate(nullptr),
_payloadReset(nullptr)
{
}
void HANumber::setValueTemplate(const char* valueTemplate)
{
_valueTemplate = valueTemplate;
}
void HANumber::setCommandTemplate(const char* commandTemplate)
{
_commandTemplate = commandTemplate;
}
void HANumber::setPayloadReset(const char* payloadReset)
{
_payloadReset = payloadReset;
}
bool HANumber::setState(const HANumeric& state, const bool force)
{
if (!force && state == _currentState) {
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
const bool published = publishState(state);
_currentState = state;
return published;
}
void HANumber::updateMinMaxStep(const float min, const float max, const float step)
@@ -50,10 +67,11 @@ void HANumber::buildSerializer()
return;
}
_serializer = new HASerializer(this, 17); // 17 - max properties nb
_serializer = new HASerializer(this, 24);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
@@ -63,11 +81,27 @@ void HANumber::buildSerializer()
getModeProperty(),
HASerializer::ProgmemPropertyValue
);
_serializer->set(
AHATOFSTR(HACommandTemplateProperty),
getCommandTemplate(),
HASerializer::ProgmemPropertyValue
);
if (nonEmptyString(_valueTemplate)) {
_serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (nonEmptyString(_payloadReset)) {
_serializer->set(AHATOFSTR(HAPayloadResetProperty), _payloadReset);
}
if (nonEmptyString(_commandTemplate)) {
_serializer->set(
AHATOFSTR(HACommandTemplateProperty),
_commandTemplate
);
} else if (getBuiltInCommandTemplate()) {
_serializer->set(
AHATOFSTR(HACommandTemplateProperty),
getBuiltInCommandTemplate(),
HASerializer::ProgmemPropertyValue
);
}
if (_minValue.isSet()) {
_serializer->set(
@@ -121,7 +155,7 @@ HASerializer* HANumber::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 17);
HASerializer* serializer = new HASerializer(this, 24);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentNumber),
@@ -130,6 +164,7 @@ HASerializer* HANumber::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
@@ -139,11 +174,27 @@ HASerializer* HANumber::buildDeviceDiscoverySerializer()
getModeProperty(),
HASerializer::ProgmemPropertyValue
);
serializer->set(
AHATOFSTR(HACommandTemplateProperty),
getCommandTemplate(),
HASerializer::ProgmemPropertyValue
);
if (nonEmptyString(_valueTemplate)) {
serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (nonEmptyString(_payloadReset)) {
serializer->set(AHATOFSTR(HAPayloadResetProperty), _payloadReset);
}
if (nonEmptyString(_commandTemplate)) {
serializer->set(
AHATOFSTR(HACommandTemplateProperty),
_commandTemplate
);
} else if (getBuiltInCommandTemplate()) {
serializer->set(
AHATOFSTR(HACommandTemplateProperty),
getBuiltInCommandTemplate(),
HASerializer::ProgmemPropertyValue
);
}
if (_minValue.isSet()) {
serializer->set(
@@ -263,6 +314,23 @@ void HANumber::handleCommand(const uint8_t* cmd, const uint16_t length)
return;
}
if (
_payloadReset
&& _payloadReset[0] != '\0'
&& length == strlen(_payloadReset)
&& memcmp(cmd, _payloadReset, length) == 0
) {
if (_commandCallback) {
_commandCallback(HANumeric(), this);
}
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
if (_commandStdCallback) {
_commandStdCallback(HANumeric(), this);
}
#endif
return;
}
if (memcmp_P(cmd, HAStateNone, length) == 0) {
if (_commandCallback) {
_commandCallback(HANumeric(), this);
@@ -302,7 +370,7 @@ const __FlashStringHelper* HANumber::getModeProperty() const
}
}
const __FlashStringHelper* HANumber::getCommandTemplate()
const __FlashStringHelper* HANumber::getBuiltInCommandTemplate() const
{
switch (_precision) {
case PrecisionP1:
+10 -2
View File
@@ -190,6 +190,10 @@ public:
*/
void updateMinMaxStep(const float min, const float max, const float step);
void setValueTemplate(const char* valueTemplate);
void setCommandTemplate(const char* commandTemplate);
void setPayloadReset(const char* payloadReset);
/**
* 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.
@@ -254,9 +258,9 @@ private:
const __FlashStringHelper* getModeProperty() const;
/**
* Returns progmem string representing value template for the command.
* Built-in command template used for float precision (when no custom template is set).
*/
const __FlashStringHelper* getCommandTemplate();
const __FlashStringHelper* getBuiltInCommandTemplate() const;
/// The precision of the number. By default it's `HANumber::PrecisionP0`.
const NumberPrecision _precision;
@@ -294,6 +298,10 @@ private:
/// The callback that will be called when the command is received from the HA.
HANUMBER_CALLBACK(_commandCallback);
const char* _commandTemplate;
const char* _valueTemplate;
const char* _payloadReset;
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
/// The std::function callback that will be called when the command is received from the HA.
std::function<void(HANumeric, HANumber*)> _commandStdCallback;
+4 -2
View File
@@ -22,10 +22,11 @@ void HAScene::buildSerializer()
return;
}
_serializer = new HASerializer(this, 9); // 9 - max properties nb
_serializer = new HASerializer(this, 18);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
@@ -55,7 +56,7 @@ HASerializer* HAScene::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 9);
HASerializer* serializer = new HASerializer(this, 18);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentScene),
@@ -64,6 +65,7 @@ HASerializer* HAScene::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
+41 -26
View File
@@ -2,6 +2,8 @@
#ifndef EX_ARDUINOHA_SELECT
#include "../HAMqtt.h"
#include "../utils/HAUtils.h"
#include "../utils/HADictionary.h"
#include "../utils/HASerializer.h"
HASelect::HASelect(const char* uniqueId) :
@@ -11,6 +13,8 @@ HASelect::HASelect(const char* uniqueId) :
_icon(nullptr),
_retain(false),
_optimistic(false),
_valueTemplate(nullptr),
_commandTemplate(nullptr),
_commandCallback(nullptr)
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
, _commandStdCallback()
@@ -35,13 +39,23 @@ HASelect::~HASelect()
}
}
void HASelect::setValueTemplate(const char* valueTemplate)
{
_valueTemplate = valueTemplate;
}
void HASelect::setCommandTemplate(const char* commandTemplate)
{
_commandTemplate = commandTemplate;
}
void HASelect::setOptions(const char* options)
{
if (!options || _options) { // options can be set only once
return;
}
const uint16_t optionsNb = countOptionsInString(options);
const uint16_t optionsNb = HAUtils::countSemicolonSeparatedOptions(options);
if (optionsNb == 0) {
return;
}
@@ -83,12 +97,13 @@ bool HASelect::setState(const int8_t state, const bool force)
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
if (!_options || state >= _options->getItemsNb()) {
return false;
}
return false;
const bool published = publishState(state);
_currentState = state;
return published;
}
const char* HASelect::getCurrentOption() const
@@ -102,10 +117,11 @@ void HASelect::buildSerializer()
return;
}
_serializer = new HASerializer(this, 12); // 12 - max properties nb
_serializer = new HASerializer(this, 20);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
_serializer->set(
@@ -114,6 +130,14 @@ void HASelect::buildSerializer()
HASerializer::ArrayPropertyType
);
if (nonEmptyString(_valueTemplate)) {
_serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (nonEmptyString(_commandTemplate)) {
_serializer->set(AHATOFSTR(HACommandTemplateProperty), _commandTemplate);
}
if (_retain) {
_serializer->set(
AHATOFSTR(HARetainProperty),
@@ -142,7 +166,7 @@ HASerializer* HASelect::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 12);
HASerializer* serializer = new HASerializer(this, 20);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentSelect),
@@ -151,6 +175,7 @@ HASerializer* HASelect::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
serializer->set(
@@ -159,6 +184,14 @@ HASerializer* HASelect::buildDeviceDiscoverySerializer()
HASerializer::ArrayPropertyType
);
if (nonEmptyString(_valueTemplate)) {
serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (nonEmptyString(_commandTemplate)) {
serializer->set(AHATOFSTR(HACommandTemplateProperty), _commandTemplate);
}
if (_retain) {
serializer->set(
AHATOFSTR(HARetainProperty),
@@ -256,25 +289,7 @@ bool HASelect::publishState(const int8_t state)
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 (uint16_t i = 0; i < optionsLen; i++) {
if (options[i] == ';') {
if (optionsNb == 255) {
break;
}
optionsNb++;
}
}
return optionsNb;
return HAUtils::countSemicolonSeparatedOptions(options);
}
#endif
+6
View File
@@ -105,6 +105,9 @@ public:
inline void setOptimistic(const bool optimistic)
{ _optimistic = optimistic; }
void setValueTemplate(const char* valueTemplate);
void setCommandTemplate(const char* commandTemplate);
/**
* 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.
@@ -180,6 +183,9 @@ private:
/// The optimistic mode of the select (`true` - enabled, `false` - disabled).
bool _optimistic;
const char* _valueTemplate;
const char* _commandTemplate;
/// The command callback that will be called when option is changed via the HA panel.
HASELECT_CALLBACK(_commandCallback);
+163 -3
View File
@@ -2,7 +2,11 @@
#ifndef EX_ARDUINOHA_SENSOR
#include "../HAMqtt.h"
#include "../utils/HAUtils.h"
#include "../utils/HADictionary.h"
#include "../utils/HASerializer.h"
#include "../utils/HASerializerArray.h"
#include <string.h>
HASensor::HASensor(const char* uniqueId, const uint16_t features) :
HABaseDeviceType(AHATOFSTR(HAComponentSensor), uniqueId),
@@ -12,11 +16,99 @@ HASensor::HASensor(const char* uniqueId, const uint16_t features) :
_forceUpdate(false),
_icon(nullptr),
_unitOfMeasurement(nullptr),
_expireAfter()
_expireAfter(),
_options(nullptr),
_suggestedDisplayPrecision(),
_valueTemplate(nullptr),
_jsonAttributesTemplate(nullptr),
_lastResetValueTemplate(nullptr)
{
}
HASensor::~HASensor()
{
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 HASensor::setSuggestedDisplayPrecision(uint8_t precision)
{
_suggestedDisplayPrecision = HANumeric(static_cast<uint8_t>(precision), 0);
}
void HASensor::clearSuggestedDisplayPrecision()
{
_suggestedDisplayPrecision.reset();
}
void HASensor::setOptions(const char* options)
{
if (!options || _options) {
return;
}
const uint16_t optionsNb = HAUtils::countSemicolonSeparatedOptions(options);
if (optionsNb == 0) {
return;
}
const uint16_t optionsLen = strlen(options) + 1;
_options = new HASerializerArray(optionsNb, false);
if (optionsNb == 1) {
_options->add(options);
return;
}
uint16_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];
option[optionLen] = 0;
memcpy(option, &options[i - optionLen], optionLen);
if (!_options->add(option)) {
delete[] option;
break;
}
optionLen = 0;
continue;
}
optionLen++;
}
}
void HASensor::setValueTemplate(const char* valueTemplate)
{
_valueTemplate = valueTemplate;
}
void HASensor::setJsonAttributesTemplate(const char* jsonAttributesTemplate)
{
_jsonAttributesTemplate = jsonAttributesTemplate;
}
void HASensor::setLastResetValueTemplate(const char* lastResetValueTemplate)
{
_lastResetValueTemplate = lastResetValueTemplate;
}
bool HASensor::setValue(const char* value)
{
if (!value) {
@@ -46,16 +138,44 @@ void HASensor::buildSerializer()
return;
}
_serializer = new HASerializer(this, 14); // 14 - max properties nb
_serializer = new HASerializer(this, 24);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _deviceClass);
_serializer->set(AHATOFSTR(HAStateClassProperty), _stateClass);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
_serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), nonEmptyString(_unitOfMeasurement));
if (_suggestedDisplayPrecision.isSet()) {
_serializer->set(
AHATOFSTR(HASuggestedDisplayPrecisionProperty),
&_suggestedDisplayPrecision,
HASerializer::NumberPropertyType
);
}
if (nonEmptyString(_valueTemplate)) {
_serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (nonEmptyString(_lastResetValueTemplate)) {
_serializer->set(
AHATOFSTR(HALastResetValueTemplateProperty),
_lastResetValueTemplate
);
}
if (_options) {
_serializer->set(
AHATOFSTR(HASensorOptionsProperty),
_options,
HASerializer::ArrayPropertyType
);
}
if (_forceUpdate) {
_serializer->set(
AHATOFSTR(HAForceUpdateProperty),
@@ -74,6 +194,12 @@ void HASensor::buildSerializer()
if (_features & JsonAttributesFeature) {
_serializer->topic(AHATOFSTR(HAJsonAttributesTopic));
if (nonEmptyString(_jsonAttributesTemplate)) {
_serializer->set(
AHATOFSTR(HAJsonAttributesTemplateProperty),
_jsonAttributesTemplate
);
}
}
_serializer->set(HASerializer::WithDevice);
@@ -87,7 +213,7 @@ HASerializer* HASensor::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 14);
HASerializer* serializer = new HASerializer(this, 24);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentSensor),
@@ -96,12 +222,40 @@ HASerializer* HASensor::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HADeviceClassProperty), _deviceClass);
serializer->set(AHATOFSTR(HAStateClassProperty), _stateClass);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), nonEmptyString(_unitOfMeasurement));
if (_suggestedDisplayPrecision.isSet()) {
serializer->set(
AHATOFSTR(HASuggestedDisplayPrecisionProperty),
&_suggestedDisplayPrecision,
HASerializer::NumberPropertyType
);
}
if (nonEmptyString(_valueTemplate)) {
serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (nonEmptyString(_lastResetValueTemplate)) {
serializer->set(
AHATOFSTR(HALastResetValueTemplateProperty),
_lastResetValueTemplate
);
}
if (_options) {
serializer->set(
AHATOFSTR(HASensorOptionsProperty),
_options,
HASerializer::ArrayPropertyType
);
}
if (_forceUpdate) {
serializer->set(
AHATOFSTR(HAForceUpdateProperty),
@@ -120,6 +274,12 @@ HASerializer* HASensor::buildDeviceDiscoverySerializer()
if (_features & JsonAttributesFeature) {
serializer->topic(AHATOFSTR(HAJsonAttributesTopic));
if (nonEmptyString(_jsonAttributesTemplate)) {
serializer->set(
AHATOFSTR(HAJsonAttributesTemplateProperty),
_jsonAttributesTemplate
);
}
}
serializer->set(HASerializer::WithAvailability);
+22
View File
@@ -4,6 +4,8 @@
#include "HABaseDeviceType.h"
#include "../utils/HANumeric.h"
class HASerializerArray;
#ifndef EX_ARDUINOHA_SENSOR
/**
@@ -29,6 +31,8 @@ public:
*/
HASensor(const char* uniqueId, const uint16_t features = DefaultFeatures);
virtual ~HASensor();
/**
* Publishes the MQTT message with the given value.
* Unlike the other device types, the HASensor doesn't store the previous value that was set.
@@ -99,6 +103,18 @@ public:
inline void setUnitOfMeasurement(const char* unitOfMeasurement)
{ _unitOfMeasurement = unitOfMeasurement; }
void setSuggestedDisplayPrecision(uint8_t precision);
void clearSuggestedDisplayPrecision();
/**
* Semicolon-separated options for device_class `enum`. Set only once.
*/
void setOptions(const char* options);
void setValueTemplate(const char* valueTemplate);
void setJsonAttributesTemplate(const char* jsonAttributesTemplate);
void setLastResetValueTemplate(const char* lastResetValueTemplate);
protected:
virtual void buildSerializer() override final;
virtual HASerializer* buildDeviceDiscoverySerializer() override;
@@ -127,6 +143,12 @@ private:
/// It defines the number of seconds after the sensor’s state expires, if it’s not updated. By default the sensors state never expires.
HANumeric _expireAfter;
HASerializerArray* _options;
HANumeric _suggestedDisplayPrecision;
const char* _valueTemplate;
const char* _jsonAttributesTemplate;
const char* _lastResetValueTemplate;
};
#endif
+3 -6
View File
@@ -25,12 +25,9 @@ bool HASensorNumber::setValue(const HANumeric& value, const bool force)
return true;
}
if (publishValue(value)) {
_currentValue = value;
return true;
}
return false;
const bool published = publishValue(value);
_currentValue = value;
return published;
}
void HASensorNumber::onMqttConnected()
+132 -12
View File
@@ -2,7 +2,25 @@
#ifndef EX_ARDUINOHA_SWITCH
#include "../HAMqtt.h"
#include "../utils/HADictionary.h"
#include "../utils/HASerializer.h"
#include <string.h>
static bool payloadEquals(
const uint8_t* data,
const uint16_t length,
const char* ram,
const __FlashStringHelper* flash
)
{
if (ram) {
const size_t n = strlen(ram);
return (length == n) && (memcmp(data, ram, length) == 0);
}
const size_t n = strlen_P(AHAFROMFSTR(flash));
return (length == n) && (memcmp_P(data, AHAFROMFSTR(flash), n) == 0);
}
HASwitch::HASwitch(const char* uniqueId) :
HABaseDeviceType(AHATOFSTR(HAComponentSwitch), uniqueId),
@@ -11,23 +29,36 @@ HASwitch::HASwitch(const char* uniqueId) :
_retain(false),
_optimistic(false),
_currentState(false),
_payloadOn(nullptr),
_payloadOff(nullptr),
_stateOn(nullptr),
_stateOff(nullptr),
_valueTemplate(nullptr),
_commandTemplate(nullptr),
_commandCallback(nullptr)
{
}
void HASwitch::setValueTemplate(const char* valueTemplate)
{
_valueTemplate = valueTemplate;
}
void HASwitch::setCommandTemplate(const char* commandTemplate)
{
_commandTemplate = commandTemplate;
}
bool HASwitch::setState(const bool state, const bool force)
{
if (!force && state == _currentState) {
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
const bool published = publishState(state);
_currentState = state;
return published;
}
void HASwitch::buildSerializer()
@@ -36,14 +67,39 @@ void HASwitch::buildSerializer()
return;
}
_serializer = new HASerializer(this, 12); // 12 - max properties nb
_serializer = new HASerializer(this, 24);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
if (nonEmptyString(_payloadOn)) {
_serializer->set(AHATOFSTR(HAPayloadOnProperty), _payloadOn);
}
if (nonEmptyString(_payloadOff)) {
_serializer->set(AHATOFSTR(HAPayloadOffProperty), _payloadOff);
}
if (nonEmptyString(_stateOn)) {
_serializer->set(AHATOFSTR(HAStateOnDiscoveryProperty), _stateOn);
}
if (nonEmptyString(_stateOff)) {
_serializer->set(AHATOFSTR(HAStateOffDiscoveryProperty), _stateOff);
}
if (nonEmptyString(_valueTemplate)) {
_serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (nonEmptyString(_commandTemplate)) {
_serializer->set(AHATOFSTR(HACommandTemplateProperty), _commandTemplate);
}
// optional property
if (_retain) {
_serializer->set(
@@ -73,7 +129,7 @@ HASerializer* HASwitch::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 12);
HASerializer* serializer = new HASerializer(this, 24);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentSwitch),
@@ -82,10 +138,35 @@ HASerializer* HASwitch::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HADeviceClassProperty), _class);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
if (nonEmptyString(_payloadOn)) {
serializer->set(AHATOFSTR(HAPayloadOnProperty), _payloadOn);
}
if (nonEmptyString(_payloadOff)) {
serializer->set(AHATOFSTR(HAPayloadOffProperty), _payloadOff);
}
if (nonEmptyString(_stateOn)) {
serializer->set(AHATOFSTR(HAStateOnDiscoveryProperty), _stateOn);
}
if (nonEmptyString(_stateOff)) {
serializer->set(AHATOFSTR(HAStateOffDiscoveryProperty), _stateOff);
}
if (nonEmptyString(_valueTemplate)) {
serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (nonEmptyString(_commandTemplate)) {
serializer->set(AHATOFSTR(HACommandTemplateProperty), _commandTemplate);
}
if (_retain) {
serializer->set(
AHATOFSTR(HARetainProperty),
@@ -132,8 +213,6 @@ void HASwitch::onMqttMessage(
const uint16_t length
)
{
(void)payload;
const bool hasCommandCallback =
_commandCallback
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
@@ -146,7 +225,24 @@ void HASwitch::onMqttMessage(
uniqueId(),
AHATOFSTR(HACommandTopic)
)) {
bool state = length == strlen_P(HAStateOn);
const bool isOn = payloadEquals(
payload,
length,
_payloadOn,
AHATOFSTR(HAStateOn)
);
const bool isOff = payloadEquals(
payload,
length,
_payloadOff,
AHATOFSTR(HAStateOff)
);
if (!isOn && !isOff) {
return;
}
const bool state = isOn;
if (_commandCallback) {
_commandCallback(state, this);
}
@@ -160,9 +256,33 @@ void HASwitch::onMqttMessage(
bool HASwitch::publishState(const bool state)
{
if (state) {
if (nonEmptyString(_stateOn)) {
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
_stateOn,
true
);
}
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
AHATOFSTR(HAStateOn),
true
);
}
if (nonEmptyString(_stateOff)) {
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
_stateOff,
true
);
}
return publishOnDataTopic(
AHATOFSTR(HAStateTopic),
AHATOFSTR(state ? HAStateOn : HAStateOff),
AHATOFSTR(HAStateOff),
true
);
}
+22
View File
@@ -103,6 +103,21 @@ public:
inline void setOptimistic(const bool optimistic)
{ _optimistic = optimistic; }
inline void setPayloadOn(const char* payload)
{ _payloadOn = payload; }
inline void setPayloadOff(const char* payload)
{ _payloadOff = payload; }
inline void setStateOn(const char* state)
{ _stateOn = state; }
inline void setStateOff(const char* state)
{ _stateOff = state; }
void setValueTemplate(const char* valueTemplate);
void setCommandTemplate(const char* commandTemplate);
/**
* 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.
@@ -168,6 +183,13 @@ private:
/// The current state of the switch. By default it's `false`.
bool _currentState;
const char* _payloadOn;
const char* _payloadOff;
const char* _stateOn;
const char* _stateOff;
const char* _valueTemplate;
const char* _commandTemplate;
/// The callback that will be called when switch command is received from the HA.
HASWITCH_CALLBACK(_commandCallback);
+36 -8
View File
@@ -2,6 +2,7 @@
#ifndef EX_ARDUINOHA_TEXT
#include "../HAMqtt.h"
#include "../utils/HADictionary.h"
#include "../utils/HASerializer.h"
HAText::HAText(const char* uniqueId) :
@@ -13,6 +14,8 @@ HAText::HAText(const char* uniqueId) :
_minValue(),
_maxValue(),
_pattern(nullptr),
_valueTemplate(nullptr),
_commandTemplate(nullptr),
_currentState(nullptr),
_commandCallback(nullptr)
#if defined(ARDUINOHA_ENABLE_STDFUNCTION)
@@ -22,6 +25,16 @@ HAText::HAText(const char* uniqueId) :
}
void HAText::setValueTemplate(const char* valueTemplate)
{
_valueTemplate = valueTemplate;
}
void HAText::setCommandTemplate(const char* commandTemplate)
{
_commandTemplate = commandTemplate;
}
bool HAText::setState(const char* state, const bool force)
{
if (!state) {
@@ -36,12 +49,9 @@ bool HAText::setState(const char* state, const bool force)
return true;
}
if (publishState(state)) {
_currentState = state;
return true;
}
return false;
const bool published = publishState(state);
_currentState = state;
return published;
}
void HAText::buildSerializer()
@@ -50,10 +60,11 @@ void HAText::buildSerializer()
return;
}
_serializer = new HASerializer(this, 15); // 15 - max properties nb
_serializer = new HASerializer(this, 22);
_serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(_serializer);
_serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(_serializer);
_serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
_serializer->set(AHATOFSTR(HAIconProperty), _icon);
_serializer->set(
@@ -63,6 +74,14 @@ void HAText::buildSerializer()
);
_serializer->set(AHATOFSTR(HAPatternProperty), _pattern);
if (nonEmptyString(_valueTemplate)) {
_serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (nonEmptyString(_commandTemplate)) {
_serializer->set(AHATOFSTR(HACommandTemplateProperty), _commandTemplate);
}
if (_minValue.isSet()) {
_serializer->set(
AHATOFSTR(HAMinProperty),
@@ -107,7 +126,7 @@ HASerializer* HAText::buildDeviceDiscoverySerializer()
return nullptr;
}
HASerializer* serializer = new HASerializer(this, 15);
HASerializer* serializer = new HASerializer(this, 22);
serializer->set(
AHATOFSTR(HAPlatformProperty),
AHATOFSTR(HAComponentText),
@@ -116,6 +135,7 @@ HASerializer* HAText::buildDeviceDiscoverySerializer()
serializer->set(AHATOFSTR(HANameProperty), _name);
setEntityIdProperty(serializer);
serializer->set(HASerializer::WithUniqueId);
applyCommonEntityProperties(serializer);
serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory));
serializer->set(AHATOFSTR(HAIconProperty), _icon);
serializer->set(
@@ -125,6 +145,14 @@ HASerializer* HAText::buildDeviceDiscoverySerializer()
);
serializer->set(AHATOFSTR(HAPatternProperty), _pattern);
if (nonEmptyString(_valueTemplate)) {
serializer->set(AHATOFSTR(HAValueTemplateProperty), _valueTemplate);
}
if (nonEmptyString(_commandTemplate)) {
serializer->set(AHATOFSTR(HACommandTemplateProperty), _commandTemplate);
}
if (_minValue.isSet()) {
serializer->set(
AHATOFSTR(HAMinProperty),
+6
View File
@@ -121,6 +121,9 @@ public:
inline void setPattern(const char* pattern)
{ _pattern = pattern; }
void setValueTemplate(const char* valueTemplate);
void setCommandTemplate(const char* commandTemplate);
/**
* Registers callback that will be called each time text command from HA is received.
* Please note that it's not possible to register multiple callbacks for the same text entity.
@@ -197,6 +200,9 @@ private:
/// Regular expression pattern accepted by Home Assistant. It can be nullptr.
const char* _pattern;
const char* _valueTemplate;
const char* _commandTemplate;
/// The current state of the text. It can be nullptr if state wasn't set.
const char* _currentState;
+9 -3
View File
@@ -14,7 +14,8 @@ PubSubClientMock::PubSubClientMock() :
_subscriptions(nullptr),
_subscriptionsNb(0),
_insideCallback(false),
_publishCallsFromCallbackNb(0),
_failNextBeginPublish(false),
_failNextEndPublish(false),
callback(nullptr)
{
@@ -121,8 +122,8 @@ bool PubSubClientMock::beginPublish(
return false;
}
if (_insideCallback) {
_publishCallsFromCallbackNb++;
if (_failNextBeginPublish) {
_failNextBeginPublish = false;
return false;
}
@@ -178,6 +179,11 @@ int PubSubClientMock::endPublish()
return 0;
}
if (_failNextEndPublish) {
_failNextEndPublish = false;
return 0;
}
size_t messageSize = _pendingMessage->bufferSize;
uint8_t index = _flushedMessagesNb;
+6 -5
View File
@@ -169,11 +169,11 @@ public:
inline bool isInsideCallback() const
{ return _insideCallback; }
inline uint16_t getPublishCallsFromCallbackNb() const
{ return _publishCallsFromCallbackNb; }
inline void failNextBeginPublish()
{ _failNextBeginPublish = true; }
inline void resetPublishCallsFromCallbackNb()
{ _publishCallsFromCallbackNb = 0; }
inline void failNextEndPublish()
{ _failNextEndPublish = true; }
void clearFlushedMessages();
void clearSubscriptions();
@@ -194,7 +194,8 @@ private:
MqttConnection _connection;
MqttWill _lastWill;
bool _insideCallback;
uint16_t _publishCallsFromCallbackNb;
bool _failNextBeginPublish;
bool _failNextEndPublish;
MQTT_CALLBACK_SIGNATURE;
};
+194
View File
@@ -0,0 +1,194 @@
#include <Arduino.h>
#include <string.h>
#include "HAAvailabilityConfig.h"
#include "HADictionary.h"
static uint16_t jsonEscapedStringSize(const char* s)
{
if (!s) {
return 0;
}
return 2 * strlen_P(HASerializerJsonEscapeChar) + strlen(s);
}
static void appendEscapedString(char* buf, const char* s)
{
strcat_P(buf, HASerializerJsonEscapeChar);
strcat(buf, s);
strcat_P(buf, HASerializerJsonEscapeChar);
}
HAAvailabilityConfig::HAAvailabilityConfig() :
_count(0)
{
memset(_entries, 0, sizeof(_entries));
}
HAAvailabilityConfig::~HAAvailabilityConfig()
{
clear();
}
char* HAAvailabilityConfig::duplicateString(const char* value) const
{
if (!value || value[0] == '\0') {
return nullptr;
}
const size_t len = strlen(value);
char* copy = new char[len + 1];
memcpy(copy, value, len + 1);
return copy;
}
void HAAvailabilityConfig::clearEntry(Entry& entry)
{
delete[] entry.topic;
delete[] entry.valueTemplate;
delete[] entry.payloadAvailable;
delete[] entry.payloadNotAvailable;
entry.topic = nullptr;
entry.valueTemplate = nullptr;
entry.payloadAvailable = nullptr;
entry.payloadNotAvailable = nullptr;
}
bool HAAvailabilityConfig::add(
const char* topic,
const char* valueTemplate,
const char* payloadAvailable,
const char* payloadNotAvailable
)
{
if (!topic || topic[0] == '\0' || _count >= MaxEntries) {
return false;
}
Entry entry = {};
entry.topic = duplicateString(topic);
if (!entry.topic) {
return false;
}
entry.valueTemplate = duplicateString(valueTemplate);
entry.payloadAvailable = duplicateString(payloadAvailable);
entry.payloadNotAvailable = duplicateString(payloadNotAvailable);
_entries[_count] = entry;
_count++;
return true;
}
void HAAvailabilityConfig::clear()
{
for (uint8_t i = 0; i < _count; i++) {
clearEntry(_entries[i]);
}
_count = 0;
}
uint16_t HAAvailabilityConfig::calculateJsonSize() const
{
uint16_t size =
strlen_P(HASerializerJsonArrayPrefix) +
strlen_P(HASerializerJsonArraySuffix);
if (_count == 0) {
return size;
}
size += (_count - 1) * strlen_P(HASerializerJsonPropertiesSeparator);
for (uint8_t i = 0; i < _count; i++) {
const Entry& e = _entries[i];
// {"t":"topic" ... }
size += strlen_P(HASerializerJsonDataPrefix);
size += strlen_P(HASerializerJsonDataSuffix);
// "t":"..."
size +=
strlen_P(HASerializerJsonPropertyPrefix) +
strlen_P(HATopic) +
strlen_P(HASerializerJsonPropertySuffix) +
jsonEscapedStringSize(e.topic);
if (e.valueTemplate && e.valueTemplate[0] != '\0') {
size += strlen_P(HASerializerJsonPropertiesSeparator);
size +=
strlen_P(HASerializerJsonPropertyPrefix) +
strlen_P(HAValueTemplateProperty) +
strlen_P(HASerializerJsonPropertySuffix) +
jsonEscapedStringSize(e.valueTemplate);
}
if (e.payloadAvailable && e.payloadAvailable[0] != '\0') {
size += strlen_P(HASerializerJsonPropertiesSeparator);
size +=
strlen_P(HASerializerJsonPropertyPrefix) +
strlen_P(HAPayloadAvailableProperty) +
strlen_P(HASerializerJsonPropertySuffix) +
jsonEscapedStringSize(e.payloadAvailable);
}
if (e.payloadNotAvailable && e.payloadNotAvailable[0] != '\0') {
size += strlen_P(HASerializerJsonPropertiesSeparator);
size +=
strlen_P(HASerializerJsonPropertyPrefix) +
strlen_P(HAPayloadNotAvailableProperty) +
strlen_P(HASerializerJsonPropertySuffix) +
jsonEscapedStringSize(e.payloadNotAvailable);
}
}
return size;
}
bool HAAvailabilityConfig::serialize(char* output) const
{
if (!output) {
return false;
}
output[0] = 0;
strcat_P(output, HASerializerJsonArrayPrefix);
for (uint8_t i = 0; i < _count; i++) {
if (i > 0) {
strcat_P(output, HASerializerJsonPropertiesSeparator);
}
strcat_P(output, HASerializerJsonDataPrefix);
strcat_P(output, HASerializerJsonPropertyPrefix);
strcat_P(output, HATopic);
strcat_P(output, HASerializerJsonPropertySuffix);
appendEscapedString(output, _entries[i].topic);
const Entry& e = _entries[i];
if (e.valueTemplate && e.valueTemplate[0] != '\0') {
strcat_P(output, HASerializerJsonPropertiesSeparator);
strcat_P(output, HASerializerJsonPropertyPrefix);
strcat_P(output, HAValueTemplateProperty);
strcat_P(output, HASerializerJsonPropertySuffix);
appendEscapedString(output, e.valueTemplate);
}
if (e.payloadAvailable && e.payloadAvailable[0] != '\0') {
strcat_P(output, HASerializerJsonPropertiesSeparator);
strcat_P(output, HASerializerJsonPropertyPrefix);
strcat_P(output, HAPayloadAvailableProperty);
strcat_P(output, HASerializerJsonPropertySuffix);
appendEscapedString(output, e.payloadAvailable);
}
if (e.payloadNotAvailable && e.payloadNotAvailable[0] != '\0') {
strcat_P(output, HASerializerJsonPropertiesSeparator);
strcat_P(output, HASerializerJsonPropertyPrefix);
strcat_P(output, HAPayloadNotAvailableProperty);
strcat_P(output, HASerializerJsonPropertySuffix);
appendEscapedString(output, e.payloadNotAvailable);
}
strcat_P(output, HASerializerJsonDataSuffix);
}
strcat_P(output, HASerializerJsonArraySuffix);
return true;
}
+62
View File
@@ -0,0 +1,62 @@
#ifndef AHA_HAAVAILABILITYCONFIG_H
#define AHA_HAAVAILABILITYCONFIG_H
#include <stdint.h>
/**
* Holds up to four MQTT availability entries for discovery JSON (`avty`).
* Topic strings must remain valid for the lifetime of the config (same pattern as other ArduinoHA setters).
*/
class HAAvailabilityConfig
{
public:
static const uint8_t MaxEntries = 4;
struct Entry {
const char* topic;
const char* valueTemplate;
const char* payloadAvailable;
const char* payloadNotAvailable;
};
HAAvailabilityConfig();
~HAAvailabilityConfig();
/**
* Adds an availability entry. `topic` must be a full MQTT topic string.
* @return false when full or topic is null/empty.
*/
bool add(
const char* topic,
const char* valueTemplate = nullptr,
const char* payloadAvailable = nullptr,
const char* payloadNotAvailable = nullptr
);
void clear();
inline uint8_t count() const
{ return _count; }
inline const Entry& getEntry(const uint8_t index) const
{ return _entries[index]; }
/**
* Size of the JSON array only, including `[` and `]`, excluding property name and separators.
*/
uint16_t calculateJsonSize() const;
/**
* Writes the JSON array into `output` (must be at least calculateJsonSize()+1 bytes).
*/
bool serialize(char* output) const;
private:
void clearEntry(Entry& entry);
char* duplicateString(const char* value) const;
Entry _entries[MaxEntries];
uint8_t _count;
};
#endif
+25
View File
@@ -83,7 +83,32 @@ const char HASwingModesProperty[] PROGMEM = {"swing_modes"};
const char HAModesProperty[] PROGMEM = {"modes"};
const char HATemperatureCommandTemplateProperty[] PROGMEM = {"temp_cmd_tpl"};
const char HAPayloadOnProperty[] PROGMEM = {"pl_on"};
const char HAPayloadOffProperty[] PROGMEM = {"pl_off"};
const char HAPayloadAvailableProperty[] PROGMEM = {"pl_avail"};
const char HAPayloadNotAvailableProperty[] PROGMEM = {"pl_not_avail"};
const char HAAvailabilityListProperty[] PROGMEM = {"avty"};
const char HAAvailabilityModeProperty[] PROGMEM = {"avty_mode"};
const char HAExpireAfterProperty[] PROGMEM = {"exp_aft"};
const char HAOffDelayProperty[] PROGMEM = {"off_dly"};
const char HAEnabledByDefaultProperty[] PROGMEM = {"en"};
const char HAEntityPictureProperty[] PROGMEM = {"ent_pic"};
const char HAQosProperty[] PROGMEM = {"qos"};
const char HAStateOnDiscoveryProperty[] PROGMEM = {"stat_on"};
const char HAStateOffDiscoveryProperty[] PROGMEM = {"stat_off"};
const char HASuggestedDisplayPrecisionProperty[] PROGMEM = {"sug_dsp_prc"};
const char HAJsonAttributesTemplateProperty[] PROGMEM = {"json_attr_tpl"};
const char HASensorOptionsProperty[] PROGMEM = {"ops"};
const char HALastResetValueTemplateProperty[] PROGMEM = {"lrst_val_tpl"};
const char HAPayloadResetProperty[] PROGMEM = {"pl_rst"};
const char HAPayloadPressProperty[] PROGMEM = {"pl_prs"};
const char HADeviceModelIdProperty[] PROGMEM = {"mdl_id"};
const char HADeviceHwVersionProperty[] PROGMEM = {"hw"};
const char HADeviceSerialNumberProperty[] PROGMEM = {"sn"};
const char HADeviceSuggestedAreaProperty[] PROGMEM = {"sa"};
const char HADeviceViaDeviceProperty[] PROGMEM = {"via_device"};
const char HADeviceConnectionsProperty[] PROGMEM = {"cns"};
const char HAOriginSupportUrlProperty[] PROGMEM = {"url"};
// topics
const char HAConfigTopic[] PROGMEM = {"config"};
+26
View File
@@ -83,7 +83,33 @@ extern const char HASwingModesProperty[];
extern const char HAModesProperty[];
extern const char HATemperatureCommandTemplateProperty[];
extern const char HAPayloadOnProperty[];
extern const char HAPayloadOffProperty[];
extern const char HAPayloadAvailableProperty[];
extern const char HAPayloadNotAvailableProperty[];
extern const char HAAvailabilityListProperty[];
extern const char HAAvailabilityModeProperty[];
extern const char HAExpireAfterProperty[];
extern const char HAOffDelayProperty[];
extern const char HAEnabledByDefaultProperty[];
extern const char HAEntityPictureProperty[];
extern const char HAQosProperty[];
extern const char HAStateOnDiscoveryProperty[];
extern const char HAStateOffDiscoveryProperty[];
extern const char HASuggestedDisplayPrecisionProperty[];
extern const char HAJsonAttributesTemplateProperty[];
extern const char HASensorOptionsProperty[];
extern const char HALastResetValueTemplateProperty[];
extern const char HAPayloadResetProperty[];
extern const char HAPayloadPressProperty[];
// device (discovery `dev` object)
extern const char HADeviceModelIdProperty[];
extern const char HADeviceHwVersionProperty[];
extern const char HADeviceSerialNumberProperty[];
extern const char HADeviceSuggestedAreaProperty[];
extern const char HADeviceViaDeviceProperty[];
extern const char HADeviceConnectionsProperty[];
extern const char HAOriginSupportUrlProperty[];
// topics
extern const char HAConfigTopic[];
+72 -13
View File
@@ -11,6 +11,7 @@
#include "../HAMqtt.h"
#include "../utils/HAUtils.h"
#include "../utils/HANumeric.h"
#include "../utils/HAAvailabilityConfig.h"
#include "../device-types/HABaseDeviceType.h"
uint16_t HASerializer::calculateConfigTopicLength(
@@ -196,20 +197,9 @@ void HASerializer::set(const FlagType flag)
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
if (_deviceType) {
_deviceType->configureAvailabilityEntries(this);
}
SerializerEntry* entry = addEntry();
entry->type = TopicEntryType;
entry->property = AHATOFSTR(HAAvailabilityTopic);
entry->value = isSharedAvailability
? mqtt->getDevice()->getAvailabilityTopic()
: nullptr;
}
}
@@ -290,6 +280,9 @@ uint16_t HASerializer::calculateEntrySize(const SerializerEntry* entry) const
case TopicEntryType:
return calculateTopicEntrySize(entry);
case AvailabilityArrayEntryType:
return calculateAvailabilityArrayEntrySize(entry);
case FlagEntryType:
return calculateFlagSize(
static_cast<FlagType>(entry->subtype)
@@ -332,6 +325,25 @@ uint16_t HASerializer::calculateTopicEntrySize(
return size;
}
uint16_t HASerializer::calculateAvailabilityArrayEntrySize(
const SerializerEntry* entry
) const
{
if (!entry->value) {
return 0;
}
const HAAvailabilityConfig* cfg = static_cast<const HAAvailabilityConfig*>(
entry->value
);
return
strlen_P(HASerializerJsonPropertyPrefix) +
strlen_P(AHAFROMFSTR(entry->property)) +
strlen_P(HASerializerJsonPropertySuffix) +
cfg->calculateJsonSize();
}
uint16_t HASerializer::calculateFlagSize(const FlagType flag) const
{
const HAMqtt* mqtt = HAMqtt::instance();
@@ -400,6 +412,11 @@ uint16_t HASerializer::calculatePropertyValueSize(
return array->calculateSize();
}
case JsonLiteralPropertyValue: {
const char* value = static_cast<const char*>(entry->value);
return value ? strlen(value) : 0;
}
default:
return 0;
}
@@ -421,6 +438,9 @@ bool HASerializer::flushEntry(const SerializerEntry* entry) const
case TopicEntryType:
return flushTopic(entry);
case AvailabilityArrayEntryType:
return flushAvailabilityArray(entry);
case FlagEntryType:
return flushFlag(entry);
@@ -480,6 +500,16 @@ bool HASerializer::flushEntryValue(const SerializerEntry* entry) const
return true;
}
case JsonLiteralPropertyValue: {
const char* value = static_cast<const char*>(entry->value);
if (!value) {
return false;
}
mqtt->writePayload(value, strlen(value));
return true;
}
default:
return false;
}
@@ -523,6 +553,35 @@ bool HASerializer::flushTopic(const SerializerEntry* entry) const
return true;
}
bool HASerializer::flushAvailabilityArray(const SerializerEntry* entry) const
{
HAMqtt* mqtt = HAMqtt::instance();
if (!entry->value) {
return false;
}
const HAAvailabilityConfig* cfg = static_cast<const HAAvailabilityConfig*>(
entry->value
);
mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix));
mqtt->writePayload(entry->property);
mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertySuffix));
const uint16_t jsonSize = cfg->calculateJsonSize();
if (jsonSize >= 512) {
return false;
}
char buf[512];
if (!cfg->serialize(buf)) {
return false;
}
mqtt->writePayload(buf, jsonSize);
return true;
}
bool HASerializer::flushFlag(const SerializerEntry* entry) const
{
HAMqtt* mqtt = HAMqtt::instance();
+13 -2
View File
@@ -23,7 +23,8 @@ public:
UnknownEntryType = 0,
PropertyEntryType,
TopicEntryType,
FlagEntryType
FlagEntryType,
AvailabilityArrayEntryType
};
/// The type of a flag for a FlagEntryType.
@@ -40,7 +41,9 @@ public:
ProgmemPropertyValue,
BoolPropertyType,
NumberPropertyType,
ArrayPropertyType
ArrayPropertyType,
/// Raw JSON fragment (no quoting); value is a null-terminated RAM string.
JsonLiteralPropertyValue
};
/// Representation of a single entry in the object.
@@ -198,6 +201,8 @@ public:
*/
bool flush() const;
friend class HABaseDeviceType;
private:
/// Pointer to the device type that owns the serializer.
HABaseDeviceType* _deviceType;
@@ -229,6 +234,10 @@ private:
*/
uint16_t calculateTopicEntrySize(const SerializerEntry* entry) const;
uint16_t calculateAvailabilityArrayEntrySize(
const SerializerEntry* entry
) const;
/**
* Calculates the size of the entry of type `FlagEntryType`.
*/
@@ -261,6 +270,8 @@ private:
*/
bool flushTopic(const SerializerEntry* entry) const;
bool flushAvailabilityArray(const SerializerEntry* entry) const;
/**
* Flushes the entry of type `FlagEntryType` to the MQTT.
*/
+27
View File
@@ -3,6 +3,7 @@
#endif
#include <Arduino.h>
#include <string.h>
#include "HAUtils.h"
#include "HADictionary.h"
@@ -46,3 +47,29 @@ char* HAUtils::byteArrayToStr(
return dst;
}
uint8_t HAUtils::countSemicolonSeparatedOptions(const char* options)
{
if (!options) {
return 0;
}
uint8_t optionsNb = 1;
const uint16_t optionsLen = strlen(options);
if (optionsLen == 0) {
return 0;
}
for (uint16_t i = 0; i < optionsLen; i++) {
if (options[i] == ';') {
if (optionsNb == 255) {
break;
}
optionsNb++;
}
}
return optionsNb;
}
+6
View File
@@ -47,6 +47,12 @@ public:
const byte* src,
const uint16_t length
);
/**
* Counts semicolon-separated segments (e.g. select/sensor options string).
* Empty string yields 0; a string without semicolons yields 1.
*/
static uint8_t countSemicolonSeparatedOptions(const char* options);
};
#endif
+4 -2
View File
@@ -19,6 +19,7 @@ static TestCase tests[] = {
TEST_ENTRY(test_BinarySensorTest_publish_initial_state),
TEST_ENTRY(test_BinarySensorTest_publish_state_debounce),
TEST_ENTRY(test_BinarySensorTest_publish_state_debounce_skip),
TEST_ENTRY(test_BinarySensorTest_disconnected_state_updates_local_shadow),
TEST_ENTRY(test_BinarySensorTest_publish_state_off),
TEST_ENTRY(test_BinarySensorTest_publish_state_on),
TEST_ENTRY(test_ButtonTest_availability),
@@ -57,6 +58,7 @@ static TestCase tests[] = {
TEST_ENTRY(test_SensorNumberTest_publish_p3_zero_signed),
TEST_ENTRY(test_SensorNumberTest_publish_p3_zero_unsigned),
TEST_ENTRY(test_SensorNumberTest_publish_precision_mismatch),
TEST_ENTRY(test_SensorNumberTest_disconnected_value_updates_local_shadow),
TEST_ENTRY(test_SensorNumberTest_publish_uint16),
TEST_ENTRY(test_SensorNumberTest_publish_uint32),
TEST_ENTRY(test_SensorNumberTest_publish_uint8),
@@ -85,7 +87,7 @@ static TestCase tests[] = {
TEST_ENTRY(test_SwitchTest_command_off),
TEST_ENTRY(test_SwitchTest_command_on),
TEST_ENTRY(test_SwitchTest_command_subscription),
TEST_ENTRY(test_SwitchTest_callback_publish_attempt_is_rejected),
TEST_ENTRY(test_SwitchTest_callback_publish_is_deferred_until_after_dispatch),
TEST_ENTRY(test_SwitchTest_current_state_setter),
TEST_ENTRY(test_SwitchTest_default_entity_id_setter),
TEST_ENTRY(test_SwitchTest_default_params),
@@ -121,7 +123,7 @@ static TestCase tests[] = {
TEST_ENTRY(test_TextTest_publish_nothing_if_retained),
TEST_ENTRY(test_TextTest_publish_state),
TEST_ENTRY(test_TextTest_publish_state_debounce),
TEST_ENTRY(test_TextTest_callback_publish_attempt_is_rejected),
TEST_ENTRY(test_TextTest_callback_publish_is_deferred_until_after_dispatch),
TEST_ENTRY(test_TextTest_retain_setter),
};
+4 -2
View File
@@ -29,6 +29,7 @@ extern void test_BinarySensorTest_object_id_setter(void);
extern void test_BinarySensorTest_publish_initial_state(void);
extern void test_BinarySensorTest_publish_state_debounce(void);
extern void test_BinarySensorTest_publish_state_debounce_skip(void);
extern void test_BinarySensorTest_disconnected_state_updates_local_shadow(void);
extern void test_BinarySensorTest_publish_state_off(void);
extern void test_BinarySensorTest_publish_state_on(void);
extern void test_ButtonTest_availability(void);
@@ -67,6 +68,7 @@ extern void test_SensorNumberTest_publish_p3_smaller(void);
extern void test_SensorNumberTest_publish_p3_zero_signed(void);
extern void test_SensorNumberTest_publish_p3_zero_unsigned(void);
extern void test_SensorNumberTest_publish_precision_mismatch(void);
extern void test_SensorNumberTest_disconnected_value_updates_local_shadow(void);
extern void test_SensorNumberTest_publish_uint16(void);
extern void test_SensorNumberTest_publish_uint32(void);
extern void test_SensorNumberTest_publish_uint8(void);
@@ -113,7 +115,7 @@ extern void test_SwitchTest_publish_nothing_if_retained(void);
extern void test_SwitchTest_publish_state_off(void);
extern void test_SwitchTest_publish_state_on(void);
extern void test_SwitchTest_retain_setter(void);
extern void test_SwitchTest_callback_publish_attempt_is_rejected(void);
extern void test_SwitchTest_callback_publish_is_deferred_until_after_dispatch(void);
extern void test_TextTest_availability(void);
extern void test_TextTest_command_callback(void);
extern void test_TextTest_command_subscription(void);
@@ -132,6 +134,6 @@ extern void test_TextTest_publish_nothing_if_retained(void);
extern void test_TextTest_publish_state(void);
extern void test_TextTest_publish_state_debounce(void);
extern void test_TextTest_retain_setter(void);
extern void test_TextTest_callback_publish_attempt_is_rejected(void);
extern void test_TextTest_callback_publish_is_deferred_until_after_dispatch(void);
#endif
@@ -281,3 +281,13 @@ void test_BinarySensorTest_publish_state_debounce_skip(void) {
TEST_ASSERT_TRUE(result);
}
void test_BinarySensorTest_disconnected_state_updates_local_shadow(void) {
initMqttTest(testDeviceId)
HABinarySensor sensor(testUniqueId);
TEST_ASSERT_FALSE(sensor.setState(true));
TEST_ASSERT_TRUE(sensor.getCurrentState());
TEST_ASSERT_EQUAL(0, mock->getFlushedMessagesNb());
}
@@ -676,3 +676,14 @@ void test_SensorNumberTest_publish_precision_mismatch(void) {
TEST_ASSERT_EQUAL(mock->getFlushedMessagesNb(), 0);
}
void test_SensorNumberTest_disconnected_value_updates_local_shadow(void) {
initMqttTest(testDeviceId)
HASensorNumber sensor(testUniqueId, HASensorNumber::PrecisionP1);
TEST_ASSERT_FALSE(sensor.setValue(27.5f));
TEST_ASSERT_TRUE(sensor.getCurrentValue().isSet());
AHA_ASSERT_NEAR_FLOAT(27.5f, sensor.getCurrentValue().toFloat(), 0.1f);
TEST_ASSERT_EQUAL(0, mock->getFlushedMessagesNb());
}
@@ -42,9 +42,9 @@ void onCommandReceived(bool state, HASwitch* caller)
lastCommandCallbackCall.caller = caller;
}
void onCommandPublishAttempt(bool state, HASwitch* caller)
void onCommandDeferredPublish(bool state, HASwitch* caller)
{
TEST_ASSERT_FALSE(caller->setState(state));
TEST_ASSERT_TRUE(caller->setState(state));
}
void test_SwitchTest_invalid_unique_id(void) {
@@ -393,17 +393,19 @@ void test_SwitchTest_command_off(void) {
assertCommandCallbackCalled(false, &testSwitch)
}
void test_SwitchTest_callback_publish_attempt_is_rejected(void) {
void test_SwitchTest_callback_publish_is_deferred_until_after_dispatch(void) {
prepareTest
mock->connectDummy();
mqtt.resetDeferredPublishTestCounters();
HASwitch testSwitch(testUniqueId);
testSwitch.onCommand(onCommandPublishAttempt);
testSwitch.onCommand(onCommandDeferredPublish);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("ON"));
TEST_ASSERT_EQUAL(1, mock->getPublishCallsFromCallbackNb());
TEST_ASSERT_EQUAL(0, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL_UINT16(1, mqtt.getDeferredPublishEnqueueCountForTest());
TEST_ASSERT_EQUAL_UINT8(1, mock->getFlushedMessagesNb());
AHA_ASSERT_MQTT_MESSAGE(mock, 0, AHATOFSTR(StateTopic), "ON", true);
}
void test_SwitchTest_different_switch_command(void) {
+8 -6
View File
@@ -46,9 +46,9 @@ void onCommandReceived(const char* value, HAText* caller)
lastCommandCallbackCall.caller = caller;
}
void onCommandPublishAttempt(const char* value, HAText* caller)
void onCommandDeferredPublish(const char* value, HAText* caller)
{
TEST_ASSERT_FALSE(caller->setState(value));
TEST_ASSERT_TRUE(caller->setState(value));
}
void test_TextTest_invalid_unique_id(void) {
@@ -328,17 +328,19 @@ void test_TextTest_command_callback(void) {
assertCommandCallbackCalled("hello", &text)
}
void test_TextTest_callback_publish_attempt_is_rejected(void) {
void test_TextTest_callback_publish_is_deferred_until_after_dispatch(void) {
prepareTest
mock->connectDummy();
mqtt.resetDeferredPublishTestCounters();
HAText text(testUniqueId);
text.onCommand(onCommandPublishAttempt);
text.onCommand(onCommandDeferredPublish);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("hello"));
TEST_ASSERT_EQUAL(1, mock->getPublishCallsFromCallbackNb());
TEST_ASSERT_EQUAL(0, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL_UINT16(1, mqtt.getDeferredPublishEnqueueCountForTest());
TEST_ASSERT_EQUAL_UINT8(1, mock->getFlushedMessagesNb());
AHA_ASSERT_MQTT_MESSAGE(mock, 0, AHATOFSTR(StateTopic), "hello", true);
}
void test_TextTest_different_text_command(void) {
+2 -2
View File
@@ -13,7 +13,7 @@ static TestCase tests[] = {
TEST_ENTRY(test_NumberTest_command_number_unsigned),
TEST_ENTRY(test_NumberTest_command_number_zero),
TEST_ENTRY(test_NumberTest_command_subscription),
TEST_ENTRY(test_NumberTest_callback_publish_attempt_is_rejected),
TEST_ENTRY(test_NumberTest_callback_publish_is_deferred_until_after_dispatch),
TEST_ENTRY(test_NumberTest_current_state_setter_getter_float),
TEST_ENTRY(test_NumberTest_current_state_setter_getter_int16),
TEST_ENTRY(test_NumberTest_current_state_setter_getter_int32),
@@ -227,7 +227,7 @@ static TestCase tests[] = {
TEST_ENTRY(test_SelectTest_publish_state_only),
TEST_ENTRY(test_SelectTest_retain_setter),
TEST_ENTRY(test_SelectTest_single_option),
TEST_ENTRY(test_SelectTest_callback_publish_attempt_is_rejected),
TEST_ENTRY(test_SelectTest_callback_publish_is_deferred_until_after_dispatch),
};
static const size_t TEST_COUNT = sizeof(tests) / sizeof(tests[0]);
+2 -2
View File
@@ -23,7 +23,7 @@ extern void test_NumberTest_command_number_signed(void);
extern void test_NumberTest_command_number_unsigned(void);
extern void test_NumberTest_command_number_zero(void);
extern void test_NumberTest_command_subscription(void);
extern void test_NumberTest_callback_publish_attempt_is_rejected(void);
extern void test_NumberTest_callback_publish_is_deferred_until_after_dispatch(void);
extern void test_NumberTest_current_state_setter_getter_float(void);
extern void test_NumberTest_current_state_setter_getter_int16(void);
extern void test_NumberTest_current_state_setter_getter_int32(void);
@@ -237,6 +237,6 @@ extern void test_SelectTest_publish_state_none(void);
extern void test_SelectTest_publish_state_only(void);
extern void test_SelectTest_retain_setter(void);
extern void test_SelectTest_single_option(void);
extern void test_SelectTest_callback_publish_attempt_is_rejected(void);
extern void test_SelectTest_callback_publish_is_deferred_until_after_dispatch(void);
#endif
@@ -42,9 +42,9 @@ void onCommandReceived(HANumeric number, HANumber* caller)
lastCommandCallbackCall.caller = caller;
}
void onCommandPublishAttempt(HANumeric value, HANumber* caller)
void onCommandDeferredPublish(HANumeric value, HANumber* caller)
{
TEST_ASSERT_FALSE(caller->setState(value));
TEST_ASSERT_TRUE(caller->setState(value));
}
void test_NumberTest_invalid_unique_id(void) {
@@ -1015,17 +1015,19 @@ void test_NumberTest_command_number_float_p3(void) {
assertCommandCallbackCalled(HANumeric(-1.234f, 3), &number)
}
void test_NumberTest_callback_publish_attempt_is_rejected(void) {
void test_NumberTest_callback_publish_is_deferred_until_after_dispatch(void) {
prepareTest
mock->connectDummy();
mqtt.resetDeferredPublishTestCounters();
HANumber number(testUniqueId);
number.onCommand(onCommandPublishAttempt);
number.onCommand(onCommandDeferredPublish);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("1234"));
TEST_ASSERT_EQUAL(1, mock->getPublishCallsFromCallbackNb());
TEST_ASSERT_EQUAL(0, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL_UINT16(1, mqtt.getDeferredPublishEnqueueCountForTest());
TEST_ASSERT_EQUAL_UINT8(1, mock->getFlushedMessagesNb());
AHA_ASSERT_MQTT_MESSAGE(mock, 0, AHATOFSTR(StateTopic), "1234", true);
}
void test_NumberTest_command_number_invalid(void) {
@@ -41,9 +41,9 @@ void onCommandReceived(int8_t index, HASelect* caller)
lastCommandCallbackCall.caller = caller;
}
void onCommandPublishAttempt(int8_t index, HASelect* caller)
void onCommandDeferredPublish(int8_t index, HASelect* caller)
{
TEST_ASSERT_FALSE(caller->setState(index));
TEST_ASSERT_TRUE(caller->setState(index));
}
void test_SelectTest_invalid_unique_id(void) {
@@ -482,18 +482,20 @@ void test_SelectTest_command_option_non_existing(void) {
assertCommandCallbackNotCalled()
}
void test_SelectTest_callback_publish_attempt_is_rejected(void) {
void test_SelectTest_callback_publish_is_deferred_until_after_dispatch(void) {
prepareTest
mock->connectDummy();
mqtt.resetDeferredPublishTestCounters();
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.onCommand(onCommandPublishAttempt);
select.onCommand(onCommandDeferredPublish);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("B"));
TEST_ASSERT_EQUAL(1, mock->getPublishCallsFromCallbackNb());
TEST_ASSERT_EQUAL(0, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL_UINT16(1, mqtt.getDeferredPublishEnqueueCountForTest());
TEST_ASSERT_EQUAL_UINT8(1, mock->getFlushedMessagesNb());
AHA_ASSERT_MQTT_MESSAGE(mock, 0, AHATOFSTR(StateTopic), "B", true);
}
void test_SelectTest_different_select_command(void) {
+7 -1
View File
@@ -7,7 +7,13 @@ static TestCase tests[] = {
TEST_ENTRY(test_MqttTest_reconnect_interval_default_value),
TEST_ENTRY(test_MqttTest_reconnect_interval_setter),
TEST_ENTRY(test_MqttTest_reconnect_interval_throttles_attempts),
TEST_ENTRY(test_MqttTest_publish_attempt_from_message_callback_is_rejected),
TEST_ENTRY(test_MqttTest_publish_from_message_callback_is_deferred_and_flushed),
TEST_ENTRY(test_MqttTest_deferred_publish_order_is_preserved),
TEST_ENTRY(test_MqttTest_processing_message_flag_only_wraps_inbound_dispatch),
TEST_ENTRY(test_MqttTest_streaming_publish_from_message_callback_is_deferred_and_flushed),
TEST_ENTRY(test_MqttTest_deferred_publish_is_kept_across_disconnect_and_retried_from_loop),
TEST_ENTRY(test_MqttTest_failed_deferred_flush_keeps_queue_and_retries_in_order),
TEST_ENTRY(test_MqttTest_mixed_deferred_publish_order_is_preserved),
TEST_ENTRY(test_DeviceTest_default_unique_id),
TEST_ENTRY(test_DeviceTest_unique_id_constructor_char),
+7 -1
View File
@@ -17,7 +17,13 @@ extern void test_MqttTest_maximum_number_of_device_types(void);
extern void test_MqttTest_reconnect_interval_default_value(void);
extern void test_MqttTest_reconnect_interval_setter(void);
extern void test_MqttTest_reconnect_interval_throttles_attempts(void);
extern void test_MqttTest_publish_attempt_from_message_callback_is_rejected(void);
extern void test_MqttTest_publish_from_message_callback_is_deferred_and_flushed(void);
extern void test_MqttTest_deferred_publish_order_is_preserved(void);
extern void test_MqttTest_processing_message_flag_only_wraps_inbound_dispatch(void);
extern void test_MqttTest_streaming_publish_from_message_callback_is_deferred_and_flushed(void);
extern void test_MqttTest_deferred_publish_is_kept_across_disconnect_and_retried_from_loop(void);
extern void test_MqttTest_failed_deferred_flush_keeps_queue_and_retries_in_order(void);
extern void test_MqttTest_mixed_deferred_publish_order_is_preserved(void);
extern void test_DeviceTest_default_unique_id(void);
extern void test_DeviceTest_unique_id_constructor_char(void);
+251 -6
View File
@@ -5,7 +5,9 @@
static const char* testDeviceId = "testDevice";
static const char* testUniqueId = "uniqueId";
static HAMqtt* activeMqtt = nullptr;
static PubSubClientMock* activeMock = nullptr;
static bool messageCallbackCalled = false;
static bool processingFlagSeenDuringCallback = false;
const char ComponentNameStr[] PROGMEM = {"componentName"};
@@ -22,7 +24,7 @@ protected:
}
};
void onMessagePublishAttempt(const char* topic, const uint8_t* payload, uint16_t length)
void onMessageDeferredPublish(const char* topic, const uint8_t* payload, uint16_t length)
{
(void)topic;
(void)payload;
@@ -30,7 +32,78 @@ void onMessagePublishAttempt(const char* topic, const uint8_t* payload, uint16_t
messageCallbackCalled = true;
TEST_ASSERT_NOT_NULL(activeMqtt);
TEST_ASSERT_FALSE(activeMqtt->publish("testData/testDevice/echo", "1", false));
TEST_ASSERT_TRUE(activeMqtt->publish("testData/testDevice/echo", "1", false));
}
void onMessageQueueOrderedPublishes(const char* topic, const uint8_t* payload, uint16_t length)
{
(void)topic;
(void)payload;
(void)length;
TEST_ASSERT_NOT_NULL(activeMqtt);
TEST_ASSERT_TRUE(activeMqtt->publish("testData/testDevice/a", "1", false));
TEST_ASSERT_TRUE(activeMqtt->publish("testData/testDevice/b", "2", false));
}
void onMessageInspectProcessingState(const char* topic, const uint8_t* payload, uint16_t length)
{
(void)topic;
(void)payload;
(void)length;
TEST_ASSERT_NOT_NULL(activeMqtt);
processingFlagSeenDuringCallback = activeMqtt->isProcessingMessage();
}
void onMessageDeferredStreamingPublish(const char* topic, const uint8_t* payload, uint16_t length)
{
(void)topic;
(void)payload;
(void)length;
TEST_ASSERT_NOT_NULL(activeMqtt);
TEST_ASSERT_TRUE(activeMqtt->beginPublish("testData/testDevice/stream", 2, false));
activeMqtt->writePayload("O", 1);
activeMqtt->writePayload("K", 1);
TEST_ASSERT_TRUE(activeMqtt->endPublish());
}
void onMessageDeferredPublishThenDisconnect(const char* topic, const uint8_t* payload, uint16_t length)
{
(void)topic;
(void)payload;
(void)length;
TEST_ASSERT_NOT_NULL(activeMqtt);
TEST_ASSERT_NOT_NULL(activeMock);
TEST_ASSERT_TRUE(activeMqtt->publish("testData/testDevice/retry", "1", false));
activeMock->disconnect();
}
void onMessageDeferredPublishesForFailedFlush(const char* topic, const uint8_t* payload, uint16_t length)
{
(void)topic;
(void)payload;
(void)length;
TEST_ASSERT_NOT_NULL(activeMqtt);
TEST_ASSERT_TRUE(activeMqtt->publish("testData/testDevice/a", "1", false));
TEST_ASSERT_TRUE(activeMqtt->publish("testData/testDevice/b", "2", false));
}
void onMessageMixedDeferredPublishesPreserveOrder(const char* topic, const uint8_t* payload, uint16_t length)
{
(void)topic;
(void)payload;
(void)length;
TEST_ASSERT_NOT_NULL(activeMqtt);
TEST_ASSERT_TRUE(activeMqtt->publish("testData/testDevice/a", "1", false));
TEST_ASSERT_TRUE(activeMqtt->beginPublish("testData/testDevice/b", 1, false));
activeMqtt->writePayload("2", 1);
TEST_ASSERT_TRUE(activeMqtt->endPublish());
TEST_ASSERT_TRUE(activeMqtt->publish("testData/testDevice/c", "3", false));
}
void test_MqttTest_maximum_number_of_device_types(void)
@@ -84,7 +157,7 @@ void test_MqttTest_reconnect_interval_throttles_attempts(void)
TEST_ASSERT_EQUAL_UINT16(2, mock->getConnectCallsNb());
}
void test_MqttTest_publish_attempt_from_message_callback_is_rejected(void)
void test_MqttTest_publish_from_message_callback_is_deferred_and_flushed(void)
{
PubSubClientMock* mock = new PubSubClientMock();
HADevice device(testDeviceId);
@@ -95,14 +168,186 @@ void test_MqttTest_publish_attempt_from_message_callback_is_rejected(void)
activeMqtt = &mqtt;
messageCallbackCalled = false;
mock->resetPublishCallsFromCallbackNb();
mqtt.onMessage(onMessagePublishAttempt);
mqtt.resetDeferredPublishTestCounters();
mqtt.onMessage(onMessageDeferredPublish);
mock->fakeMessage("testData/testDevice/input", "1");
TEST_ASSERT_TRUE(messageCallbackCalled);
TEST_ASSERT_EQUAL_UINT16(1, mock->getPublishCallsFromCallbackNb());
TEST_ASSERT_EQUAL_UINT16(1, mqtt.getDeferredPublishEnqueueCountForTest());
TEST_ASSERT_EQUAL_UINT8(1, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL_STRING("testData/testDevice/echo", mock->getFlushedMessages()[0]->topic);
TEST_ASSERT_EQUAL_STRING("1", mock->getFlushedMessages()[0]->buffer);
activeMqtt = nullptr;
}
void test_MqttTest_deferred_publish_order_is_preserved(void)
{
PubSubClientMock* mock = new PubSubClientMock();
HADevice device(testDeviceId);
HAMqtt mqtt(mock, device);
mqtt.setDataPrefix("testData");
mqtt.begin("testHost", "testUser", "testPass");
mock->connectDummy();
activeMqtt = &mqtt;
mqtt.resetDeferredPublishTestCounters();
mqtt.onMessage(onMessageQueueOrderedPublishes);
mock->fakeMessage("testData/testDevice/input", "x");
TEST_ASSERT_EQUAL_UINT16(2, mqtt.getDeferredPublishEnqueueCountForTest());
TEST_ASSERT_EQUAL_UINT8(2, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL_STRING("testData/testDevice/a", mock->getFlushedMessages()[0]->topic);
TEST_ASSERT_EQUAL_STRING("1", mock->getFlushedMessages()[0]->buffer);
TEST_ASSERT_EQUAL_STRING("testData/testDevice/b", mock->getFlushedMessages()[1]->topic);
TEST_ASSERT_EQUAL_STRING("2", mock->getFlushedMessages()[1]->buffer);
activeMqtt = nullptr;
}
void test_MqttTest_processing_message_flag_only_wraps_inbound_dispatch(void)
{
PubSubClientMock* mock = new PubSubClientMock();
HADevice device(testDeviceId);
HAMqtt mqtt(mock, device);
mqtt.setDataPrefix("testData");
mqtt.begin("testHost", "testUser", "testPass");
mock->connectDummy();
activeMqtt = &mqtt;
processingFlagSeenDuringCallback = false;
TEST_ASSERT_FALSE(mqtt.isProcessingMessage());
mqtt.onMessage(onMessageInspectProcessingState);
mock->fakeMessage("testData/testDevice/input", "1");
TEST_ASSERT_TRUE(processingFlagSeenDuringCallback);
TEST_ASSERT_FALSE(mqtt.isProcessingMessage());
activeMqtt = nullptr;
}
void test_MqttTest_streaming_publish_from_message_callback_is_deferred_and_flushed(void)
{
PubSubClientMock* mock = new PubSubClientMock();
HADevice device(testDeviceId);
HAMqtt mqtt(mock, device);
mqtt.setDataPrefix("testData");
mqtt.begin("testHost", "testUser", "testPass");
mock->connectDummy();
activeMqtt = &mqtt;
mqtt.resetDeferredPublishTestCounters();
mqtt.onMessage(onMessageDeferredStreamingPublish);
mock->fakeMessage("testData/testDevice/input", "1");
TEST_ASSERT_EQUAL_UINT16(1, mqtt.getDeferredPublishEnqueueCountForTest());
TEST_ASSERT_EQUAL_UINT8(0, mqtt.getPendingDeferredPublishesForTest());
TEST_ASSERT_FALSE(mqtt.hasDeferredFlushFailureForTest());
TEST_ASSERT_EQUAL_UINT8(1, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL_STRING("testData/testDevice/stream", mock->getFlushedMessages()[0]->topic);
TEST_ASSERT_EQUAL_STRING("OK", mock->getFlushedMessages()[0]->buffer);
activeMqtt = nullptr;
}
void test_MqttTest_deferred_publish_is_kept_across_disconnect_and_retried_from_loop(void)
{
PubSubClientMock* mock = new PubSubClientMock();
HADevice device(testDeviceId);
HAMqtt mqtt(mock, device);
mqtt.setDataPrefix("testData");
mqtt.begin("testHost", "testUser", "testPass");
mock->connectDummy();
activeMqtt = &mqtt;
activeMock = mock;
mqtt.resetDeferredPublishTestCounters();
mqtt.onMessage(onMessageDeferredPublishThenDisconnect);
mock->fakeMessage("testData/testDevice/input", "1");
TEST_ASSERT_EQUAL_UINT16(1, mqtt.getDeferredPublishEnqueueCountForTest());
TEST_ASSERT_EQUAL_UINT8(1, mqtt.getPendingDeferredPublishesForTest());
TEST_ASSERT_TRUE(mqtt.hasDeferredFlushFailureForTest());
TEST_ASSERT_TRUE(mqtt.didDeferredFlushFailDueToDisconnectForTest());
TEST_ASSERT_EQUAL_UINT8(0, mock->getFlushedMessagesNb());
mock->connectDummy();
mqtt.loop();
TEST_ASSERT_EQUAL_UINT8(0, mqtt.getPendingDeferredPublishesForTest());
TEST_ASSERT_FALSE(mqtt.hasDeferredFlushFailureForTest());
TEST_ASSERT_EQUAL_UINT8(1, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL_STRING("testData/testDevice/retry", mock->getFlushedMessages()[0]->topic);
TEST_ASSERT_EQUAL_STRING("1", mock->getFlushedMessages()[0]->buffer);
activeMock = nullptr;
activeMqtt = nullptr;
}
void test_MqttTest_failed_deferred_flush_keeps_queue_and_retries_in_order(void)
{
PubSubClientMock* mock = new PubSubClientMock();
HADevice device(testDeviceId);
HAMqtt mqtt(mock, device);
mqtt.setDataPrefix("testData");
mqtt.begin("testHost", "testUser", "testPass");
mock->connectDummy();
activeMqtt = &mqtt;
mqtt.resetDeferredPublishTestCounters();
mqtt.onMessage(onMessageDeferredPublishesForFailedFlush);
mock->failNextEndPublish();
mock->fakeMessage("testData/testDevice/input", "1");
TEST_ASSERT_EQUAL_UINT16(2, mqtt.getDeferredPublishEnqueueCountForTest());
TEST_ASSERT_EQUAL_UINT8(2, mqtt.getPendingDeferredPublishesForTest());
TEST_ASSERT_TRUE(mqtt.hasDeferredFlushFailureForTest());
TEST_ASSERT_TRUE(mqtt.didDeferredFlushFailAtEndPublishForTest());
TEST_ASSERT_EQUAL_UINT8(0, mock->getFlushedMessagesNb());
mqtt.loop();
TEST_ASSERT_EQUAL_UINT8(0, mqtt.getPendingDeferredPublishesForTest());
TEST_ASSERT_FALSE(mqtt.hasDeferredFlushFailureForTest());
TEST_ASSERT_EQUAL_UINT8(2, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL_STRING("testData/testDevice/a", mock->getFlushedMessages()[0]->topic);
TEST_ASSERT_EQUAL_STRING("1", mock->getFlushedMessages()[0]->buffer);
TEST_ASSERT_EQUAL_STRING("testData/testDevice/b", mock->getFlushedMessages()[1]->topic);
TEST_ASSERT_EQUAL_STRING("2", mock->getFlushedMessages()[1]->buffer);
activeMqtt = nullptr;
}
void test_MqttTest_mixed_deferred_publish_order_is_preserved(void)
{
PubSubClientMock* mock = new PubSubClientMock();
HADevice device(testDeviceId);
HAMqtt mqtt(mock, device);
mqtt.setDataPrefix("testData");
mqtt.begin("testHost", "testUser", "testPass");
mock->connectDummy();
activeMqtt = &mqtt;
mqtt.resetDeferredPublishTestCounters();
mqtt.onMessage(onMessageMixedDeferredPublishesPreserveOrder);
mock->fakeMessage("testData/testDevice/input", "1");
TEST_ASSERT_EQUAL_UINT16(3, mqtt.getDeferredPublishEnqueueCountForTest());
TEST_ASSERT_EQUAL_UINT8(0, mqtt.getPendingDeferredPublishesForTest());
TEST_ASSERT_EQUAL_UINT8(3, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL_STRING("testData/testDevice/a", mock->getFlushedMessages()[0]->topic);
TEST_ASSERT_EQUAL_STRING("1", mock->getFlushedMessages()[0]->buffer);
TEST_ASSERT_EQUAL_STRING("testData/testDevice/b", mock->getFlushedMessages()[1]->topic);
TEST_ASSERT_EQUAL_STRING("2", mock->getFlushedMessages()[1]->buffer);
TEST_ASSERT_EQUAL_STRING("testData/testDevice/c", mock->getFlushedMessages()[2]->topic);
TEST_ASSERT_EQUAL_STRING("3", mock->getFlushedMessages()[2]->buffer);
activeMqtt = nullptr;
}