mirror of
https://github.com/alexhopeoconnor/arduino-home-assistant.git
synced 2026-10-04 02:48:13 +10:00
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:
@@ -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/
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 =
|
||||
@@ -518,3 +643,123 @@ void HAMqtt::setState(ConnectionState state)
|
||||
_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
@@ -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
|
||||
|
||||
@@ -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(
|
||||
@@ -291,3 +376,224 @@ 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"};
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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,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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user