From 80ae7ae592dbc2fffeb7daa244feb48e69ef212f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Chyrzy=C5=84ski?= Date: Mon, 5 Feb 2024 16:20:40 +0100 Subject: [PATCH] Add support for extended unique IDs (#217) * implement unique ID serialization as flag * finished extended unique IDs feature * add tests * update docs * add funding.yml --- .github/FUNDING.yml | 1 + CHANGELOG.md | 1 + docs/_static/custom.css | 4 + docs/_static/documentation_options.js | 2 +- docs/documents/api/core/ha-device.html | 18 +++ docs/documents/api/utils/ha-serializer.html | 5 + .../library/device-configuration.html | 21 ++-- docs/documents/library/device-types.html | 109 ++++++++++++++-- docs/documents/library/index.html | 1 + docs/genindex.html | 12 +- docs/index.html | 1 + docs/objects.inv | Bin 31972 -> 32109 bytes docs/searchindex.js | 2 +- docsrc/README.md | 7 +- docsrc/source/conf.py | 2 +- .../library/device-configuration.rst | 21 ++-- .../source/documents/library/device-types.rst | 117 ++++++++++++++++-- docsrc/source/static/custom.css | 4 + src/HADevice.cpp | 3 +- src/HADevice.h | 16 +++ src/device-types/HABinarySensor.cpp | 2 +- src/device-types/HAButton.cpp | 2 +- src/device-types/HACamera.cpp | 2 +- src/device-types/HACover.cpp | 2 +- src/device-types/HADeviceTracker.cpp | 2 +- src/device-types/HAFan.cpp | 2 +- src/device-types/HAHVAC.cpp | 2 +- src/device-types/HALight.cpp | 2 +- src/device-types/HALock.cpp | 2 +- src/device-types/HANumber.cpp | 2 +- src/device-types/HAScene.cpp | 2 +- src/device-types/HASelect.cpp | 2 +- src/device-types/HASensor.cpp | 2 +- src/device-types/HASwitch.cpp | 2 +- src/utils/HASerializer.cpp | 41 +++++- src/utils/HASerializer.h | 3 +- tests/BinarySensorTest/BinarySensorTest.ino | 18 +++ tests/ButtonTest/ButtonTest.ino | 18 +++ tests/CameraTest/CameraTest.ino | 18 +++ tests/CoverTest/CoverTest.ino | 22 +++- tests/DeviceTest/DeviceTest.ino | 17 ++- tests/DeviceTrackerTest/DeviceTrackerTest.ino | 18 +++ tests/FanTest/FanTest.ino | 20 +++ tests/HVACTest/HVACTest.ino | 19 +++ tests/LightTest/LightTest.ino | 20 +++ tests/LockTest/LockTest.ino | 22 +++- tests/NumberTest/NumberTest.ino | 20 +++ tests/SceneTest/SceneTest.ino | 18 +++ tests/SelectTest/SelectTest.ino | 24 ++++ tests/SensorTest/SensorTest.ino | 18 +++ tests/SwitchTest/SwitchTest.ino | 22 +++- 51 files changed, 647 insertions(+), 66 deletions(-) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..8b14654 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: dawidchyrzynski \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 075c31d..119eb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **New features:** * Added support for setting MQTT's keep alive ([https://github.com/dawidchyrzynski/arduino-home-assistant/issues/153](#153)) * Added support for the `state_class` property in the `HASensor` ([#179](https://github.com/dawidchyrzynski/arduino-home-assistant/pull/179) by [@https://github.com/Starzu]) +* Implemented extended unique ID support for all device types. This allows you to prefix each device type's unique ID with the device ID, ensuring smooth deployment of identical code on multiple devices without encountering unique ID conflicts [#212](https://github.com/dawidchyrzynski/arduino-home-assistant/issues/212#issuecomment-1919832684) **Fixes:** * Fixed a bug with the maximum number of device types ([#190](https://github.com/dawidchyrzynski/arduino-home-assistant/issues/190) by [@martaisty](https://github.com/martaisty)) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 6d79a89..fcc262d 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -41,6 +41,10 @@ table.examples-table tbody td + td { .searchbox .caption-text { display: none } +.code-block-caption { + margin-top: 30px; + font-weight: bold; +} form { display: flex; flex-direction: row; diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index fe42da8..69dcd90 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,6 +1,6 @@ var DOCUMENTATION_OPTIONS = { URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), - VERSION: '2.0.0', + VERSION: '2.1.0', LANGUAGE: 'None', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/documents/api/core/ha-device.html b/docs/documents/api/core/ha-device.html index 9645952..dfa2fdb 100644 --- a/docs/documents/api/core/ha-device.html +++ b/docs/documents/api/core/ha-device.html @@ -216,6 +216,12 @@

Returns true if the shared availability is enabled for the device.

+
+
+inline bool isExtendedUniqueIdsEnabled() const
+

Returns true if the extended unique IDs feature is enabled for the device.

+
+
inline const char *getAvailabilityTopic() const
@@ -228,6 +234,12 @@

Returns online/offline state of the device.

+
+
+inline void enableExtendedUniqueIds()
+

Enables the use of extended unique IDs for all registered device types. The unique ID of each device type will be prefixed with the device’s ID once enabled.

+
+
bool setUniqueId(const byte *uniqueId, const uint16_t length)
@@ -358,6 +370,12 @@

Specifies whether the device is available (online / offline).

+
+
+bool _extendedUniqueIds
+

Specifies whether extended unique IDs feature is enabled.

+
+ diff --git a/docs/documents/api/utils/ha-serializer.html b/docs/documents/api/utils/ha-serializer.html index 99293a4..4027bfc 100644 --- a/docs/documents/api/utils/ha-serializer.html +++ b/docs/documents/api/utils/ha-serializer.html @@ -199,6 +199,11 @@ enumerator WithAvailability
+
+
+enumerator WithUniqueId
+
+
diff --git a/docs/documents/library/device-configuration.html b/docs/documents/library/device-configuration.html index 56a5432..4161010 100644 --- a/docs/documents/library/device-configuration.html +++ b/docs/documents/library/device-configuration.html @@ -149,11 +149,11 @@

Device configuration

-

HADevice represents the physical device where the library is installed. -Logically it’s a group of types like sensors, switches, lights and so on. -In the Home Assistant, it’s listed with properties that may be configured using the library’s API.

-

Each property except the unique ID is optional. -Setting optional properties increases flash and RAM usage so it’s not recommended to set them on lower-spec MCUs.

+

HADevice represents the physical device on which the library is used. +Essentially, it’s a group of types such as sensors, switches, lights, and more. +Within Home Assistant, it appears with properties that can be configured using the library’s API.

+

Every property, except for the unique ID, is optional. +Enabling optional properties can lead to increased flash and RAM usage, therefore it is not advisable to set them on lower-spec MCUs.

The supported properties are:

  • unique ID*

  • @@ -164,10 +164,11 @@ Setting optional properties increases flash and RAM usage so it’s not recommen

Unique ID

-

The ID of a device needs to be unique in a scope of a Home Assistant instance. -The safest solution is to use the MAC address of an Ethernet or Wi-Fi chip but you can also implement your own solution.

-

There are three different ways to set the ID of the device. -You can pick one depending on your needs.

+

The unique ID serves as an internal identifier for devices within Home Assistant. +With this ID, Home Assistant can monitor the device’s parameters and the entities it exposes. +The unique ID must be distinct within the scope of the Home Assistant instance. +The recommended approach is to use the MAC address of an Ethernet or Wi-Fi chip.

+

There are three distinct methods for setting the device ID, allowing you to choose the one that best suits your requirements.

1) Providing string (const char*) to the HADevice constructor

Try to keep the ID simple (alphanumeric characters) and short.

@@ -227,7 +228,7 @@ You can pick one depending on your needs.

Device properties

Each property has its corresponding setter method in the HADevice class. -Please note that all these methods accept const char pointer whose content is not copied.

+Please note that all of these methods accept a const char pointer whose content is not copied.

#include <ArduinoHA.h>
 
 HADevice device("myUniqueId");
diff --git a/docs/documents/library/device-types.html b/docs/documents/library/device-types.html
index 35c75d2..274bd0f 100644
--- a/docs/documents/library/device-types.html
+++ b/docs/documents/library/device-types.html
@@ -149,15 +149,110 @@
             
   

Device types

-

Device type represents a single entity in the Home Assistant panel. -It can be a sensor, lock, camera or anything that’s listed in the table below.

-

Your physical device (for example ESP-01 board) can have multiple device types assigned. -They will be displayed as child entities in the HA panel.

+

Device type represents a single entity within the Home Assistant panel, which could be a sensor, lock, camera, or any other item listed in the table below.

+

Your physical device, such as an ESP-01 board, can have multiple device types assigned to it. +These types will then appear as child entities in the Home Assistant panel.

+
+

Identifiers

+

Home Assistant utilizes three distinct identifiers, which might initially appear confusing. +Grasping the purpose of each is crucial for a clear understanding of the library’s API.

+
+

Entity ID

+

Home Assistant automatically generates an entity ID for each device type registered by your device. +This ID is primarily utilized by dashboards and automations within Home Assistant.

+

When the entity is discovered by Home Assistant for the first time, the name field is automatically employed to generate the entity ID. +Once registered, you can modify it to any desired value using the Home Assistant User Interface.

+

Home Assistant internally relies on the unique ID, so changing the entity ID or name in the Home Assistant UI does not break the integration between your device and Home Assistant.

+
+
+

Object ID

+

The object ID is an optional identifier that you can assign to the device type. +Its sole purpose is for generating the entity ID described above.

+

By default, Home Assistant generates the entity ID based on the entity’s name. +However, when the object ID is provided, Home Assistant uses it to generate the entity ID.

+

Consequently, you can use the entity’s name as a user-friendly label and the object ID as an internal identifier.

+
+
+

Unique ID

+

The unique ID serves as an internal identifier for the entity within the Home Assistant instance. +Once the entity with a specific unique ID is created, it cannot be altered, as this identifier is not accessible through the user interface.

+

Home Assistant utilizes this identifier internally to store the parameters of the entity in the database. +Given that the unique ID must be unique across the entire Home Assistant instance. +Multiple devices cannot expose entities with the same unique ID.

+

By default, the library uses the unique ID provided in the device type’s constructor. +However, when you reuse the same codebase on multiple devices, conflicts may arise. +To address this issue, you can enable the extended unique ID feature in the HADevice instance. +This feature incorporates the device’s unique ID as a prefix for the device type’s ID.

+
+
Default behavior
+
#include <Ethernet.h>
+#include <ArduinoHA.h>
+
+byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A};
+
+EthernetClient client;
+HADevice device(mac, sizeof(mac)); // the unique ID of the device will be 0010fa6e384a
+HAMqtt mqtt(client, device);
+
+// "myValve" is unique ID of the sensor. You should define your own ID.
+HASensor valve("myValve");
+
+void setup() {
+    // ...
+
+    valve.setIcon("mdi:home");
+    valve.setName("Water valve");
+
+    // the unique ID of the valve in HA will be "myValve"
+
+    // ...
+}
+
+void loop() {
+    // ...
+}
+
+
+
+
+
Extended unique IDs
+
#include <Ethernet.h>
+#include <ArduinoHA.h>
+
+byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A};
+
+EthernetClient client;
+HADevice device(mac, sizeof(mac)); // the unique ID of the device will be 0010fa6e384a
+HAMqtt mqtt(client, device);
+
+// "myValve" is unique ID of the sensor. You should define your own ID.
+HASensor valve("myValve");
+
+void setup() {
+    // ...
+
+    device.enableExtendedUniqueIds(); // <------------ enables extended unique IDs
+    valve.setIcon("mdi:home");
+    valve.setName("Water valve");
+
+    // the unique ID of the valve in HA will be "0010fa6e384a_myValve"
+
+    // ...
+}
+
+void loop() {
+    // ...
+}
+
+
+
+
+

Limitations

-

Registering a new device type requires some flash and RAM memory to be utilized. -On less powerful units like Arduino Uno, you may quickly hit the limit of resources, so keeping the device simple is recommended. -Hitting the resource limit will result in random reboots of the device.

+

Registering a new device type involves utilizing a certain amount of flash and RAM memory. +On less powerful units, such as the Arduino Uno, you may rapidly reach the resource limit. +Therefore, it is advisable to keep the device simple to avoid hitting the resource limit, which could lead to random reboots of the device.

By default, the maximum number of device types is 6. You can increase the limit using the HAMqtt class constructor as follows:

#include <Ethernet.h>
diff --git a/docs/documents/library/index.html b/docs/documents/library/index.html
index 2513fa6..36b7d26 100644
--- a/docs/documents/library/index.html
+++ b/docs/documents/library/index.html
@@ -173,6 +173,7 @@ Solid understanding of foundations will allow you to utilize full potential of t
 
 
 
  • Device types diff --git a/docs/genindex.html b/docs/genindex.html index d57835b..7ea91fe 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -376,6 +376,8 @@
  • HADevice::_availabilityTopic (C++ member)
  • HADevice::_available (C++ member) +
  • +
  • HADevice::_extendedUniqueIds (C++ member)
  • HADevice::_ownsUniqueId (C++ member)
  • @@ -384,6 +386,8 @@
  • HADevice::_sharedAvailability (C++ member)
  • HADevice::_uniqueId (C++ member) +
  • +
  • HADevice::enableExtendedUniqueIds (C++ function)
  • HADevice::enableLastWill (C++ function)
  • @@ -398,6 +402,8 @@
  • HADevice::HADevice (C++ function), [1], [2]
  • HADevice::isAvailable (C++ function) +
  • +
  • HADevice::isExtendedUniqueIdsEnabled (C++ function)
  • HADevice::isSharedAvailabilityEnabled (C++ function)
  • @@ -860,11 +866,11 @@
  • HAHVAC::TemperatureUnit::DefaultUnit (C++ enumerator)
  • HAHVAC::TemperatureUnit::FahrenheitUnit (C++ enumerator) -
  • -
  • HAHVAC::~HAHVAC (C++ function)
  • Device types diff --git a/docs/objects.inv b/docs/objects.inv index 2657584f0707faad57ea4244fc31a1f61c7c3e14..0741f4b541ae80d977b7489f4591166141f39706 100644 GIT binary patch delta 31712 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 CzopUu delta 31578 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 represents the physical device where the library is installed. -Logically it's a group of types like sensors, switches, lights and so on. -In the Home Assistant, it's listed with properties that may be configured using the library's API. +:doc:`HADevice ` represents the physical device on which the library is used. +Essentially, it's a group of types such as sensors, switches, lights, and more. +Within Home Assistant, it appears with properties that can be configured using the library's API. -Each property except the unique ID is optional. -Setting optional properties increases flash and RAM usage so it's not recommended to set them on lower-spec MCUs. +Every property, except for the unique ID, is optional. +Enabling optional properties can lead to increased flash and RAM usage, therefore it is not advisable to set them on lower-spec MCUs. The supported properties are: @@ -19,11 +19,12 @@ The supported properties are: Unique ID --------- -The ID of a device needs to be unique in a scope of a Home Assistant instance. -The safest solution is to use the MAC address of an Ethernet or Wi-Fi chip but you can also implement your own solution. +The unique ID serves as an internal identifier for devices within Home Assistant. +With this ID, Home Assistant can monitor the device's parameters and the entities it exposes. +The unique ID must be distinct within the scope of the Home Assistant instance. +The recommended approach is to use the MAC address of an Ethernet or Wi-Fi chip. -There are three different ways to set the ID of the device. -You can pick one depending on your needs. +There are three distinct methods for setting the device ID, allowing you to choose the one that best suits your requirements. 1) Providing string (const char*) to the :doc:`HADevice ` constructor ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -88,7 +89,7 @@ Device properties ----------------- Each property has its corresponding setter method in the :doc:`HADevice ` class. -Please note that all these methods accept const char pointer whose **content is not copied**. +Please note that all of these methods accept a const char pointer whose **content is not copied**. :: diff --git a/docsrc/source/documents/library/device-types.rst b/docsrc/source/documents/library/device-types.rst index fabeafb..39ef39d 100644 --- a/docsrc/source/documents/library/device-types.rst +++ b/docsrc/source/documents/library/device-types.rst @@ -2,18 +2,121 @@ Device types ============ -Device type represents a single entity in the Home Assistant panel. -It can be a sensor, lock, camera or anything that's listed in the table below. +Device type represents a single entity within the Home Assistant panel, which could be a sensor, lock, camera, or any other item listed in the table below. -Your physical device (for example ESP-01 board) can have multiple device types assigned. -They will be displayed as child entities in the HA panel. +Your physical device, such as an ESP-01 board, can have multiple device types assigned to it. +These types will then appear as child entities in the Home Assistant panel. + +Identifiers +----------- + +Home Assistant utilizes three distinct identifiers, which might initially appear confusing. +Grasping the purpose of each is crucial for a clear understanding of the library's API. + +Entity ID +^^^^^^^^^ + +Home Assistant automatically generates an entity ID for each device type registered by your device. +This ID is primarily utilized by dashboards and automations within Home Assistant. + +When the entity is discovered by Home Assistant for the first time, the name field is automatically employed to generate the entity ID. +Once registered, you can modify it to any desired value using the Home Assistant User Interface. + +Home Assistant internally relies on the unique ID, so changing the entity ID or name in the Home Assistant UI does not break the integration between your device and Home Assistant. + +Object ID +^^^^^^^^^ + +The object ID is an optional identifier that you can assign to the device type. +Its sole purpose is for generating the entity ID described above. + +By default, Home Assistant generates the entity ID based on the entity's name. +However, when the object ID is provided, Home Assistant uses it to generate the entity ID. + +Consequently, you can use the entity's name as a user-friendly label and the object ID as an internal identifier. + +Unique ID +^^^^^^^^^ + +The unique ID serves as an internal identifier for the entity within the Home Assistant instance. +Once the entity with a specific unique ID is created, it cannot be altered, as this identifier is not accessible through the user interface. + +Home Assistant utilizes this identifier internally to store the parameters of the entity in the database. +Given that the unique ID must be unique across the entire Home Assistant instance. +Multiple devices cannot expose entities with the same unique ID. + +By default, the library uses the unique ID provided in the device type's constructor. +However, when you reuse the same codebase on multiple devices, conflicts may arise. +To address this issue, you can enable the extended unique ID feature in the HADevice instance. +This feature incorporates the device's unique ID as a prefix for the device type's ID. + +.. code-block:: cpp + :caption: Default behavior + + #include + #include + + byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; + + EthernetClient client; + HADevice device(mac, sizeof(mac)); // the unique ID of the device will be 0010fa6e384a + HAMqtt mqtt(client, device); + + // "myValve" is unique ID of the sensor. You should define your own ID. + HASensor valve("myValve"); + + void setup() { + // ... + + valve.setIcon("mdi:home"); + valve.setName("Water valve"); + + // the unique ID of the valve in HA will be "myValve" + + // ... + } + + void loop() { + // ... + } + +.. code-block:: cpp + :caption: Extended unique IDs + + #include + #include + + byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; + + EthernetClient client; + HADevice device(mac, sizeof(mac)); // the unique ID of the device will be 0010fa6e384a + HAMqtt mqtt(client, device); + + // "myValve" is unique ID of the sensor. You should define your own ID. + HASensor valve("myValve"); + + void setup() { + // ... + + device.enableExtendedUniqueIds(); // <------------ enables extended unique IDs + valve.setIcon("mdi:home"); + valve.setName("Water valve"); + + // the unique ID of the valve in HA will be "0010fa6e384a_myValve" + + // ... + } + + void loop() { + // ... + } Limitations ----------- -Registering a new device type requires some flash and RAM memory to be utilized. -On less powerful units like Arduino Uno, you may quickly hit the limit of resources, so keeping the device simple is recommended. -Hitting the resource limit will result in random reboots of the device. +Registering a new device type involves utilizing a certain amount of flash and RAM memory. +On less powerful units, such as the Arduino Uno, you may rapidly reach the resource limit. +Therefore, it is advisable to keep the device simple to avoid hitting the resource limit, which could lead to random reboots of the device. By default, the maximum number of device types is 6. You can increase the limit using the :doc:`HAMqtt ` class constructor as follows: diff --git a/docsrc/source/static/custom.css b/docsrc/source/static/custom.css index 6d79a89..fcc262d 100644 --- a/docsrc/source/static/custom.css +++ b/docsrc/source/static/custom.css @@ -41,6 +41,10 @@ table.examples-table tbody td + td { .searchbox .caption-text { display: none } +.code-block-caption { + margin-top: 30px; + font-weight: bold; +} form { display: flex; flex-direction: row; diff --git a/src/HADevice.cpp b/src/HADevice.cpp index b7d1cc7..3e191d8 100644 --- a/src/HADevice.cpp +++ b/src/HADevice.cpp @@ -9,7 +9,8 @@ _serializer(new HASerializer(nullptr, 5)), \ _availabilityTopic(nullptr), \ _sharedAvailability(false), \ - _available(true) // device will be available by default + _available(true), \ + _extendedUniqueIds(false) HADevice::HADevice() : _uniqueId(nullptr), diff --git a/src/HADevice.h b/src/HADevice.h index f51c919..4a42fe1 100644 --- a/src/HADevice.h +++ b/src/HADevice.h @@ -60,6 +60,12 @@ public: inline bool isSharedAvailabilityEnabled() const { return _sharedAvailability; } + /** + * Returns true if the extended unique IDs feature is enabled for the device. + */ + inline bool isExtendedUniqueIdsEnabled() const + { return _extendedUniqueIds; } + /** * Returns availability topic generated by the HADevice::enableSharedAvailability method. * It can be nullptr if the shared availability is not enabled. @@ -73,6 +79,13 @@ public: inline bool isAvailable() const { return _available; } + /** + * Enables the use of extended unique IDs for all registered device types. + * The unique ID of each device type will be prefixed with the device's ID once enabled. + */ + inline void enableExtendedUniqueIds() + { _extendedUniqueIds = true; } + /** * Sets unique ID of the device based on the given byte array. * Each byte is converted into a hex string representation, so the final length of the unique ID will be twice as given. @@ -155,6 +168,9 @@ private: /// Specifies whether the device is available (online / offline). bool _available; + + /// Specifies whether extended unique IDs feature is enabled. + bool _extendedUniqueIds; }; #endif diff --git a/src/device-types/HABinarySensor.cpp b/src/device-types/HABinarySensor.cpp index d83b52f..eb27708 100644 --- a/src/device-types/HABinarySensor.cpp +++ b/src/device-types/HABinarySensor.cpp @@ -35,7 +35,7 @@ void HABinarySensor::buildSerializer() _serializer = new HASerializer(this, 7); // 7 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); _serializer->set(AHATOFSTR(HAIconProperty), _icon); _serializer->set(HASerializer::WithDevice); diff --git a/src/device-types/HAButton.cpp b/src/device-types/HAButton.cpp index ee459f6..fd81b86 100644 --- a/src/device-types/HAButton.cpp +++ b/src/device-types/HAButton.cpp @@ -22,7 +22,7 @@ void HAButton::buildSerializer() _serializer = new HASerializer(this, 8); // 8 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); _serializer->set(AHATOFSTR(HAIconProperty), _icon); diff --git a/src/device-types/HACamera.cpp b/src/device-types/HACamera.cpp index 1e50c34..a813f65 100644 --- a/src/device-types/HACamera.cpp +++ b/src/device-types/HACamera.cpp @@ -29,7 +29,7 @@ void HACamera::buildSerializer() _serializer = new HASerializer(this, 7); // 7 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HAIconProperty), _icon); _serializer->set( AHATOFSTR(HAEncodingProperty), diff --git a/src/device-types/HACover.cpp b/src/device-types/HACover.cpp index 11e294a..c1edd33 100644 --- a/src/device-types/HACover.cpp +++ b/src/device-types/HACover.cpp @@ -56,7 +56,7 @@ void HACover::buildSerializer() _serializer = new HASerializer(this, 11); // 11 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); _serializer->set(AHATOFSTR(HAIconProperty), _icon); diff --git a/src/device-types/HADeviceTracker.cpp b/src/device-types/HADeviceTracker.cpp index dd60876..1e24608 100644 --- a/src/device-types/HADeviceTracker.cpp +++ b/src/device-types/HADeviceTracker.cpp @@ -35,7 +35,7 @@ void HADeviceTracker::buildSerializer() _serializer = new HASerializer(this, 7); // 7 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HAIconProperty), _icon); _serializer->set( AHATOFSTR(HASourceTypeProperty), diff --git a/src/device-types/HAFan.cpp b/src/device-types/HAFan.cpp index 90626e8..da2f6cb 100644 --- a/src/device-types/HAFan.cpp +++ b/src/device-types/HAFan.cpp @@ -57,7 +57,7 @@ void HAFan::buildSerializer() _serializer = new HASerializer(this, 13); // 13 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HAIconProperty), _icon); if (_retain) { diff --git a/src/device-types/HAHVAC.cpp b/src/device-types/HAHVAC.cpp index 4b8db71..35eee51 100644 --- a/src/device-types/HAHVAC.cpp +++ b/src/device-types/HAHVAC.cpp @@ -185,7 +185,7 @@ void HAHVAC::buildSerializer() _serializer = new HASerializer(this, 27); // 27 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HAIconProperty), _icon); if (_retain) { diff --git a/src/device-types/HALight.cpp b/src/device-types/HALight.cpp index d8fbc14..795cfd3 100644 --- a/src/device-types/HALight.cpp +++ b/src/device-types/HALight.cpp @@ -130,7 +130,7 @@ void HALight::buildSerializer() _serializer = new HASerializer(this, 18); // 18 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HAIconProperty), _icon); if (_retain) { diff --git a/src/device-types/HALock.cpp b/src/device-types/HALock.cpp index 41d7082..8661547 100644 --- a/src/device-types/HALock.cpp +++ b/src/device-types/HALock.cpp @@ -37,7 +37,7 @@ void HALock::buildSerializer() _serializer = new HASerializer(this, 9); // 9 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HAIconProperty), _icon); if (_retain) { diff --git a/src/device-types/HANumber.cpp b/src/device-types/HANumber.cpp index e34f3f6..003e49f 100644 --- a/src/device-types/HANumber.cpp +++ b/src/device-types/HANumber.cpp @@ -45,7 +45,7 @@ void HANumber::buildSerializer() _serializer = new HASerializer(this, 15); // 15 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); _serializer->set(AHATOFSTR(HAIconProperty), _icon); _serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), _unitOfMeasurement); diff --git a/src/device-types/HAScene.cpp b/src/device-types/HAScene.cpp index 085cecf..e303b0b 100644 --- a/src/device-types/HAScene.cpp +++ b/src/device-types/HAScene.cpp @@ -21,7 +21,7 @@ void HAScene::buildSerializer() _serializer = new HASerializer(this, 7); // 7 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HAIconProperty), _icon); // optional property diff --git a/src/device-types/HASelect.cpp b/src/device-types/HASelect.cpp index a389c03..6771047 100644 --- a/src/device-types/HASelect.cpp +++ b/src/device-types/HASelect.cpp @@ -93,7 +93,7 @@ void HASelect::buildSerializer() _serializer = new HASerializer(this, 10); // 10 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HAIconProperty), _icon); _serializer->set( AHATOFSTR(HAOptionsProperty), diff --git a/src/device-types/HASensor.cpp b/src/device-types/HASensor.cpp index 7372e0f..64608a2 100644 --- a/src/device-types/HASensor.cpp +++ b/src/device-types/HASensor.cpp @@ -28,7 +28,7 @@ void HASensor::buildSerializer() _serializer = new HASerializer(this, 10); // 10 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _deviceClass); _serializer->set(AHATOFSTR(HAStateClassProperty), _stateClass); _serializer->set(AHATOFSTR(HAIconProperty), _icon); diff --git a/src/device-types/HASwitch.cpp b/src/device-types/HASwitch.cpp index fde8f3e..4f8b0ca 100644 --- a/src/device-types/HASwitch.cpp +++ b/src/device-types/HASwitch.cpp @@ -38,7 +38,7 @@ void HASwitch::buildSerializer() _serializer = new HASerializer(this, 10); // 10 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAUniqueIdProperty), _uniqueId); + _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); _serializer->set(AHATOFSTR(HAIconProperty), _icon); diff --git a/src/utils/HASerializer.cpp b/src/utils/HASerializer.cpp index 18912da..832426d 100644 --- a/src/utils/HASerializer.cpp +++ b/src/utils/HASerializer.cpp @@ -189,10 +189,10 @@ void HASerializer::set( void HASerializer::set(const FlagType flag) { - if (flag == WithDevice) { + if (flag == WithDevice || flag == WithUniqueId) { SerializerEntry* entry = addEntry(); entry->type = FlagEntryType; - entry->subtype = static_cast(WithDevice); + entry->subtype = static_cast(flag); entry->property = nullptr; entry->value = nullptr; } else if (flag == WithAvailability) { @@ -348,6 +348,21 @@ uint16_t HASerializer::calculateFlagSize(const FlagType flag) const strlen_P(HADeviceProperty) + strlen_P(HASerializerJsonPropertySuffix) + deviceLength; + } else if (flag == WithUniqueId && _deviceType) { + uint16_t uniqueIdLength = strlen(_deviceType->uniqueId()); + + if (device->isExtendedUniqueIdsEnabled()) { + uniqueIdLength += strlen(device->getUniqueId()) + 1; // with separator + } + + return + // property name + strlen_P(HASerializerJsonPropertyPrefix) + + strlen_P(HAUniqueIdProperty) + + strlen_P(HASerializerJsonPropertySuffix) + + // property value + 2 * strlen_P(HASerializerJsonEscapeChar) + + uniqueIdLength; } return 0; @@ -515,11 +530,33 @@ bool HASerializer::flushFlag(const SerializerEntry* entry) const const FlagType flag = static_cast(entry->subtype); if (flag == WithDevice && device) { + // property name mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix)); mqtt->writePayload(AHATOFSTR(HADeviceProperty)); mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertySuffix)); + // property value return device->getSerializer()->flush(); + } else if (flag == WithUniqueId && _deviceType) { + // property name + mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix)); + mqtt->writePayload(AHATOFSTR(HAUniqueIdProperty)); + mqtt->writePayload(AHATOFSTR(HASerializerJsonPropertySuffix)); + + // value + const char* uniqueId = _deviceType->uniqueId(); + mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar)); + + if (device->isExtendedUniqueIdsEnabled()) { + const char* deviceUniqueId = device->getUniqueId(); + mqtt->writePayload(deviceUniqueId, strlen(deviceUniqueId)); + mqtt->writePayload(AHATOFSTR(HASerializerUnderscore)); + } + + mqtt->writePayload(uniqueId, strlen(uniqueId)); + mqtt->writePayload(AHATOFSTR(HASerializerJsonEscapeChar)); + + return true; } return false; diff --git a/src/utils/HASerializer.h b/src/utils/HASerializer.h index fe9cdf1..ea5a85e 100644 --- a/src/utils/HASerializer.h +++ b/src/utils/HASerializer.h @@ -29,7 +29,8 @@ public: /// The type of a flag for a FlagEntryType. enum FlagType { WithDevice = 1, - WithAvailability + WithAvailability, + WithUniqueId }; /// Available data types of entries. diff --git a/tests/BinarySensorTest/BinarySensorTest.ino b/tests/BinarySensorTest/BinarySensorTest.ino index d7833bc..c91ae90 100644 --- a/tests/BinarySensorTest/BinarySensorTest.ino +++ b/tests/BinarySensorTest/BinarySensorTest.ino @@ -36,6 +36,24 @@ AHA_TEST(BinarySensorTest, default_params) { ) } +AHA_TEST(BinarySensorTest, extended_unique_id) { + initMqttTest(testDeviceId) + + device.enableExtendedUniqueIds(); + HABinarySensor sensor(testUniqueId); + assertEntityConfig( + mock, + sensor, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueSensor\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueSensor/stat_t\"" + "}" + ) + ) +} + AHA_TEST(BinarySensorTest, availability) { initMqttTest(testDeviceId) diff --git a/tests/ButtonTest/ButtonTest.ino b/tests/ButtonTest/ButtonTest.ino index dc720e0..d75ef0d 100644 --- a/tests/ButtonTest/ButtonTest.ino +++ b/tests/ButtonTest/ButtonTest.ino @@ -65,6 +65,24 @@ AHA_TEST(ButtonTest, default_params) { ) } +AHA_TEST(ButtonTest, extended_unique_id) { + prepareTest + + device.enableExtendedUniqueIds(); + HAButton button(testUniqueId); + assertEntityConfig( + mock, + button, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueButton\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"cmd_t\":\"testData/testDevice/uniqueButton/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(ButtonTest, command_subscription) { prepareTest diff --git a/tests/CameraTest/CameraTest.ino b/tests/CameraTest/CameraTest.ino index acba555..7392a85 100644 --- a/tests/CameraTest/CameraTest.ino +++ b/tests/CameraTest/CameraTest.ino @@ -36,6 +36,24 @@ AHA_TEST(CameraTest, default_params) { ) } +AHA_TEST(CameraTest, extended_unique_id) { + initMqttTest(testDeviceId) + + device.enableExtendedUniqueIds(); + HACamera camera(testUniqueId); + assertEntityConfig( + mock, + camera, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueCamera\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"t\":\"testData/testDevice/uniqueCamera/t\"" + "}" + ) + ) +} + AHA_TEST(CameraTest, availability) { initMqttTest(testDeviceId) diff --git a/tests/CoverTest/CoverTest.ino b/tests/CoverTest/CoverTest.ino index caed747..a2035ca 100644 --- a/tests/CoverTest/CoverTest.ino +++ b/tests/CoverTest/CoverTest.ino @@ -72,6 +72,26 @@ AHA_TEST(CoverTest, default_params) { assertEqual(1, mock->getFlushedMessagesNb()); // only config should be pushed } +AHA_TEST(CoverTest, extended_unique_id) { + prepareTest + + device.enableExtendedUniqueIds(); + HACover cover(testUniqueId); + assertEntityConfig( + mock, + cover, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueCover\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueCover/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueCover/cmd_t\"" + "}" + ) + ) + assertEqual(1, mock->getFlushedMessagesNb()); // only config should be pushed +} + AHA_TEST(CoverTest, default_params_with_position) { prepareTest @@ -295,7 +315,7 @@ AHA_TEST(CoverTest, publish_state_closing) { mock->connectDummy(); HACover cover(testUniqueId); - + assertTrue(cover.setState(HACover::StateClosing)); assertSingleMqttMessage(AHATOFSTR(StateTopic), "closing", true) } diff --git a/tests/DeviceTest/DeviceTest.ino b/tests/DeviceTest/DeviceTest.ino index 09f5429..103de25 100644 --- a/tests/DeviceTest/DeviceTest.ino +++ b/tests/DeviceTest/DeviceTest.ino @@ -260,6 +260,21 @@ AHA_TEST(DeviceTest, availability_publish_online) { assertSingleMqttMessage(AHATOFSTR(AvailabilityTopic), "online", true) } +AHA_TEST(DeviceTest, extended_unique_ids_disabled) { + prepareMqttTest + + assertFalse(device.isExtendedUniqueIdsEnabled()); +} + +AHA_TEST(DeviceTest, enable_extended_unique_ids) { + prepareMqttTest + + device.enableExtendedUniqueIds(); + + assertTrue(device.isExtendedUniqueIdsEnabled()); + assertNoMqttMessage() +} + AHA_TEST(DeviceTest, lwt_disabled) { prepareMqttTest @@ -281,7 +296,7 @@ AHA_TEST(DeviceTest, lwt_enabled) { AHA_TEST(DeviceTest, full_serialization) { initMqttTest("myDeviceId"); - + device.setManufacturer("myManufacturer"); device.setModel("myModel"); device.setName("myName"); diff --git a/tests/DeviceTrackerTest/DeviceTrackerTest.ino b/tests/DeviceTrackerTest/DeviceTrackerTest.ino index 3c45eaa..d7a8404 100644 --- a/tests/DeviceTrackerTest/DeviceTrackerTest.ino +++ b/tests/DeviceTrackerTest/DeviceTrackerTest.ino @@ -38,6 +38,24 @@ AHA_TEST(DeviceTrackerTest, default_params) { ) } +AHA_TEST(DeviceTrackerTest, extended_unique_id) { + initMqttTest(testDeviceId) + + device.enableExtendedUniqueIds(); + HADeviceTracker tracker(testUniqueId); + assertEntityConfig( + mock, + tracker, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueTracker\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueTracker/stat_t\"" + "}" + ) + ) +} + AHA_TEST(DeviceTrackerTest, source_type_gps) { initMqttTest(testDeviceId) diff --git a/tests/FanTest/FanTest.ino b/tests/FanTest/FanTest.ino index b0e8270..3f7d50c 100644 --- a/tests/FanTest/FanTest.ino +++ b/tests/FanTest/FanTest.ino @@ -102,6 +102,26 @@ AHA_TEST(FanTest, default_params) { assertEqual(2, mock->getFlushedMessagesNb()); // config + default state } +AHA_TEST(FanTest, extended_unique_id) { + prepareTest + + device.enableExtendedUniqueIds(); + HAFan fan(testUniqueId); + assertEntityConfig( + mock, + fan, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueFan\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueFan/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueFan/cmd_t\"" + "}" + ) + ) + assertEqual(2, mock->getFlushedMessagesNb()); // config + default state +} + AHA_TEST(FanTest, default_params_with_speed) { prepareTest diff --git a/tests/HVACTest/HVACTest.ino b/tests/HVACTest/HVACTest.ino index 382bb93..6508d6c 100644 --- a/tests/HVACTest/HVACTest.ino +++ b/tests/HVACTest/HVACTest.ino @@ -226,6 +226,25 @@ AHA_TEST(HVACTest, default_params) { assertEqual(1, mock->getFlushedMessagesNb()); // config } +AHA_TEST(HVACTest, extended_unique_id) { + prepareTest + + device.enableExtendedUniqueIds(); + HAHVAC hvac(testUniqueId); + assertEntityConfig( + mock, + hvac, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueHVAC\"," + "\"curr_temp_t\":\"testData/testDevice/uniqueHVAC/curr_temp_t\"," + "\"dev\":{\"ids\":\"testDevice\"}" + "}" + ) + ) + assertEqual(1, mock->getFlushedMessagesNb()); // config +} + AHA_TEST(HVACTest, config_with_action) { prepareTest diff --git a/tests/LightTest/LightTest.ino b/tests/LightTest/LightTest.ino index a93b62e..d942641 100644 --- a/tests/LightTest/LightTest.ino +++ b/tests/LightTest/LightTest.ino @@ -165,6 +165,26 @@ AHA_TEST(LightTest, default_params) { assertEqual(2, mock->getFlushedMessagesNb()); // config + default state } +AHA_TEST(LightTest, extended_unique_id) { + prepareTest + + device.enableExtendedUniqueIds(); + HALight light(testUniqueId); + assertEntityConfig( + mock, + light, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueLight\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueLight/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueLight/cmd_t\"" + "}" + ) + ) + assertEqual(2, mock->getFlushedMessagesNb()); // config + default state +} + AHA_TEST(LightTest, default_params_with_brightness) { prepareTest diff --git a/tests/LockTest/LockTest.ino b/tests/LockTest/LockTest.ino index 98176ad..b5d750a 100644 --- a/tests/LockTest/LockTest.ino +++ b/tests/LockTest/LockTest.ino @@ -71,6 +71,26 @@ AHA_TEST(LockTest, default_params) { assertEqual(1, mock->getFlushedMessagesNb()); // only config should be pushed } +AHA_TEST(LockTest, extended_unique_id) { + prepareTest + + device.enableExtendedUniqueIds(); + HALock lock(testUniqueId); + assertEntityConfig( + mock, + lock, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueLock\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueLock/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueLock/cmd_t\"" + "}" + ) + ) + assertEqual(1, mock->getFlushedMessagesNb()); // only config should be pushed +} + AHA_TEST(LockTest, command_subscription) { prepareTest @@ -220,7 +240,7 @@ AHA_TEST(LockTest, publish_state_locked) { HALock lock(testUniqueId); assertTrue(lock.setState(HALock::StateLocked)); - assertSingleMqttMessage(AHATOFSTR(StateTopic), "LOCKED", true) + assertSingleMqttMessage(AHATOFSTR(StateTopic), "LOCKED", true) } AHA_TEST(LockTest, publish_state_unlocked) { diff --git a/tests/NumberTest/NumberTest.ino b/tests/NumberTest/NumberTest.ino index 24fec39..60a0aca 100644 --- a/tests/NumberTest/NumberTest.ino +++ b/tests/NumberTest/NumberTest.ino @@ -71,6 +71,26 @@ AHA_TEST(NumberTest, default_params) { assertEqual(2, mock->getFlushedMessagesNb()); // config + default state } +AHA_TEST(NumberTest, extended_unique_id) { + prepareTest + + device.enableExtendedUniqueIds(); + HANumber number(testUniqueId); + assertEntityConfig( + mock, + number, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueNumber\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueNumber/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueNumber/cmd_t\"" + "}" + ) + ) + assertEqual(2, mock->getFlushedMessagesNb()); // config + default state +} + AHA_TEST(NumberTest, command_subscription) { prepareTest diff --git a/tests/SceneTest/SceneTest.ino b/tests/SceneTest/SceneTest.ino index 8b084bd..c2a6a63 100644 --- a/tests/SceneTest/SceneTest.ino +++ b/tests/SceneTest/SceneTest.ino @@ -65,6 +65,24 @@ AHA_TEST(SceneTest, default_params) { ) } +AHA_TEST(SceneTest, extended_unique_id) { + prepareTest + + device.enableExtendedUniqueIds(); + HAScene scene(testUniqueId); + assertEntityConfig( + mock, + scene, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueScene\"," + "\"pl_on\":\"ON\"," + "\"cmd_t\":\"testData/testDevice/uniqueScene/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(SceneTest, command_subscription) { prepareTest diff --git a/tests/SelectTest/SelectTest.ino b/tests/SelectTest/SelectTest.ino index ff09cdc..5d44a22 100644 --- a/tests/SelectTest/SelectTest.ino +++ b/tests/SelectTest/SelectTest.ino @@ -87,6 +87,30 @@ AHA_TEST(SelectTest, invalid_options_empty) { assertTrue(select.getOptions() == nullptr); } +AHA_TEST(SelectTest, extended_unique_id) { + prepareTest + + device.enableExtendedUniqueIds(); + HASelect select(testUniqueId); + select.setOptions("Option A"); + + assertEqual(1, select.getOptions()->getItemsNb()); + assertEntityConfig( + mock, + select, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueSelect\"," + "\"options\":[\"Option A\"]," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueSelect/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueSelect/cmd_t\"" + "}" + ) + ) + assertEqual(1, mock->getFlushedMessagesNb()); // only config should be pushed +} + AHA_TEST(SelectTest, single_option) { prepareTest diff --git a/tests/SensorTest/SensorTest.ino b/tests/SensorTest/SensorTest.ino index 686cbd8..b08d5fe 100644 --- a/tests/SensorTest/SensorTest.ino +++ b/tests/SensorTest/SensorTest.ino @@ -36,6 +36,24 @@ AHA_TEST(SensorTest, default_params) { ) } +AHA_TEST(SensorTest, extended_unique_id) { + initMqttTest(testDeviceId) + + device.enableExtendedUniqueIds(); + HASensor sensor(testUniqueId); + assertEntityConfig( + mock, + sensor, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueSensor\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueSensor/stat_t\"" + "}" + ) + ) +} + AHA_TEST(SensorTest, availability) { initMqttTest(testDeviceId) diff --git a/tests/SwitchTest/SwitchTest.ino b/tests/SwitchTest/SwitchTest.ino index 96a8c76..061cd40 100644 --- a/tests/SwitchTest/SwitchTest.ino +++ b/tests/SwitchTest/SwitchTest.ino @@ -71,6 +71,26 @@ AHA_TEST(SwitchTest, default_params) { assertEqual(2, mock->getFlushedMessagesNb()); } +AHA_TEST(SwitchTest, extended_unique_id) { + prepareTest + + device.enableExtendedUniqueIds(); + HASwitch testSwitch(testUniqueId); + assertEntityConfig( + mock, + testSwitch, + ( + "{" + "\"uniq_id\":\"testDevice_uniqueSwitch\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueSwitch/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueSwitch/cmd_t\"" + "}" + ) + ) + assertEqual(2, mock->getFlushedMessagesNb()); +} + AHA_TEST(SwitchTest, command_subscription) { prepareTest @@ -241,7 +261,7 @@ AHA_TEST(SwitchTest, publish_state_on) { HASwitch testSwitch(testUniqueId); assertTrue(testSwitch.setState(true)); - assertSingleMqttMessage(AHATOFSTR(StateTopic), "ON", true) + assertSingleMqttMessage(AHATOFSTR(StateTopic), "ON", true) } AHA_TEST(SwitchTest, publish_state_off) {