From 7d5d70d5258044f03b9b8917ec54b03d72e881be Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Thu, 16 Apr 2026 19:49:50 +1000 Subject: [PATCH] Modernize MQTT discovery compatibility and runtime refreshes. Align ArduinoHA with current Home Assistant MQTT discovery expectations by adding default entity IDs, device discovery support, safer runtime discovery refresh behavior, expanded entity category coverage, and the tests/docs needed to support the migration away from legacy object_id payloads. --- .gitignore | 3 +- CHANGELOG.md | 19 ++ README.md | 19 +- src/ArduinoHADefines.h | 3 + src/HAMqtt.cpp | 150 ++++++++++++++ src/HAMqtt.h | 22 ++ src/device-types/HABaseDeviceType.cpp | 88 +++++++- src/device-types/HABaseDeviceType.h | 81 +++++++- src/device-types/HABinarySensor.cpp | 41 +++- src/device-types/HABinarySensor.h | 15 +- src/device-types/HAButton.cpp | 41 +++- src/device-types/HAButton.h | 15 +- src/device-types/HACamera.cpp | 36 +++- src/device-types/HACamera.h | 3 + src/device-types/HACover.cpp | 55 ++++- src/device-types/HACover.h | 3 + src/device-types/HADeviceTracker.cpp | 36 +++- src/device-types/HADeviceTracker.h | 3 + src/device-types/HADeviceTrigger.cpp | 39 +++- src/device-types/HADeviceTrigger.h | 3 + src/device-types/HAFan.cpp | 70 ++++++- src/device-types/HAFan.h | 3 + src/device-types/HAHVAC.cpp | 192 +++++++++++++++++- src/device-types/HAHVAC.h | 3 + src/device-types/HALight.cpp | 88 +++++++- src/device-types/HALight.h | 3 + src/device-types/HALock.cpp | 49 ++++- src/device-types/HALock.h | 3 + src/device-types/HANumber.cpp | 89 +++++++- src/device-types/HANumber.h | 15 +- src/device-types/HAScene.cpp | 45 +++- src/device-types/HAScene.h | 3 + src/device-types/HASelect.cpp | 54 ++++- src/device-types/HASelect.h | 3 + src/device-types/HASensor.cpp | 57 +++++- src/device-types/HASensor.h | 15 +- src/device-types/HASwitch.cpp | 50 ++++- src/device-types/HASwitch.h | 3 + src/device-types/HATagScanner.cpp | 20 +- src/device-types/HATagScanner.h | 3 + src/device-types/HAText.cpp | 71 ++++++- src/device-types/HAText.h | 3 + src/utils/HADictionary.cpp | 6 + src/utils/HADictionary.h | 6 + .../BaseDeviceTypeTest/BaseDeviceTypeTest.ino | 39 ++++ tests/CameraTest/CameraTest.ino | 67 ++++++ tests/CoverTest/CoverTest.ino | 70 +++++++ tests/DeviceTrackerTest/DeviceTrackerTest.ino | 67 ++++++ tests/DeviceTriggerTest/DeviceTriggerTest.ino | 29 +++ tests/FanTest/FanTest.ino | 73 +++++++ tests/HVACTest/HVACTest.ino | 67 ++++++ tests/LockTest/LockTest.ino | 70 +++++++ tests/NumberTest/NumberTest.ino | 104 ++++++++++ tests/SceneTest/SceneTest.ino | 68 +++++++ tests/SensorTest/SensorTest.ino | 39 ++++ tests/SwitchTest/SwitchTest.ino | 73 +++++++ tests/TagScannerTest/TagScannerTest.ino | 26 +++ 57 files changed, 2212 insertions(+), 109 deletions(-) diff --git a/.gitignore b/.gitignore index 1a030ea..f197d55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ docsrc/xml docsrc/build -.DS_Store \ No newline at end of file +.DS_Store +tmp/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dc6498..022e321 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## Unreleased + +**New features:** +* Added support for the `default_entity_id` (`def_ent_id`) discovery property across device types. New code should prefer `setDefaultEntityId()` over `setObjectId()`. +* Added opt-in MQTT device discovery payload support through `HAMqtt::enableDeviceDiscovery()`, while keeping single-component discovery as the default behavior. +* Added `HABaseDeviceType::removeFromDiscovery()` and `HABaseDeviceType::republishDiscovery()` helpers for explicit discovery lifecycle management. +* Added support for the `entity_category` property across the exposed MQTT device types. + +**Fixes:** +* Fixed discovery refresh behavior for runtime config updates so device discovery mode clears stale retained per-entity configs before republishing device payloads. +* Fixed `HANumber::updateMinMaxStep()` so discovery updates follow the active discovery mode instead of always writing a retained per-entity config. +* Ignore empty `unit_of_measurement` values in discovery payloads. +* Allow `HANumber` configurations where `min` is equal to `max`. + +**Migration notes:** +* Home Assistant deprecated MQTT `object_id` in favor of `default_entity_id`, and newer Home Assistant versions may warn on or remove `object_id` handling in discovery payloads. +* Existing code using `setObjectId()` remains supported as a legacy fallback, but new projects should migrate to `setDefaultEntityId()`. +* If you enable device discovery mode, avoid publishing per-entity discovery topics manually. Use `republishDiscovery()` when a runtime config change needs to refresh discovery state. + ## 2.1.0 **New features:** diff --git a/README.md b/README.md index 1790341..0fb1792 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,23 @@ but I successfully use it on ESP8266/ESP8255 boards in my projects. * Doxygen documentation for all classes * Covered by unit tests (AUnit + EpoxyDuino + AUniter) +## Discovery Notes + +ArduinoHA supports two MQTT discovery modes: + +* Single-component discovery, which remains the default behavior and publishes one retained discovery payload per entity. +* Device discovery, which can be enabled explicitly with `HAMqtt::enableDeviceDiscovery()` and publishes a single retained `homeassistant/device/.../config` payload with component mappings under `cmps`. + +For entity ID suggestions in Home Assistant, prefer `setDefaultEntityId()` over `setObjectId()`. +`setObjectId()` is still available as a legacy fallback, but newer Home Assistant versions are moving toward `default_entity_id`. + +If you need to manage discovery at runtime: + +* Use `HABaseDeviceType::republishDiscovery()` after changing discovery-relevant config at runtime. +* Use `HABaseDeviceType::removeFromDiscovery()` to clear the retained discovery payload for a single entity. + +When device discovery mode is enabled, runtime discovery refreshes automatically clear any stale retained per-entity config before republishing the device discovery payload. + ## Supported HA types | Home Assistant type | Supported | @@ -47,7 +64,7 @@ but I successfully use it on ESP8266/ESP8255 boards in my projects. | Switch | ✅ | | Update | ❌ | | Tag scanner | ✅ | -| Text | ❌ | +| Text | ✅ | | Vacuum | ❌ | | Valve | ❌ | | Water heater | ❌ | diff --git a/src/ArduinoHADefines.h b/src/ArduinoHADefines.h index a021b59..3f98e9b 100644 --- a/src/ArduinoHADefines.h +++ b/src/ArduinoHADefines.h @@ -29,6 +29,9 @@ #define ARDUINOHA_ENABLE_STDFUNCTION #endif +// Current library version used in discovery origin metadata. +#define ARDUINOHA_LIBRARY_VERSION "2.1.0" + #if defined(ARDUINOHA_DEBUG) #include diff --git a/src/HAMqtt.cpp b/src/HAMqtt.cpp index af3a35b..55f9ca6 100644 --- a/src/HAMqtt.cpp +++ b/src/HAMqtt.cpp @@ -1,5 +1,7 @@ #include "HAMqtt.h" +#include + #ifndef ARDUINOHA_TEST #include #endif @@ -7,6 +9,8 @@ #include "HADevice.h" #include "device-types/HABaseDeviceType.h" #include "mocks/PubSubClientMock.h" +#include "utils/HADictionary.h" +#include "utils/HASerializer.h" #define HAMQTT_INIT \ _device(device), \ @@ -17,6 +21,7 @@ _initialized(false), \ _discoveryPrefix(DefaultDiscoveryPrefix), \ _dataPrefix(DefaultDataPrefix), \ + _deviceDiscoveryEnabled(false), \ _username(nullptr), \ _password(nullptr), \ _lastConnectionAttemptAt(0), \ @@ -31,6 +36,7 @@ static const char* DefaultDiscoveryPrefix = "homeassistant"; static const char* DefaultDataPrefix = "aha"; +static const char* DeviceDiscoveryOriginName = "ArduinoHA"; HAMqtt* HAMqtt::_instance = nullptr; @@ -332,6 +338,10 @@ void HAMqtt::connectToServer() void HAMqtt::onConnectedLogic() { + if (_deviceDiscoveryEnabled) { + publishDeviceDiscovery(); + } + if (_connectedCallback) { _connectedCallback(); } @@ -343,6 +353,146 @@ void HAMqtt::onConnectedLogic() } } +bool HAMqtt::publishDeviceDiscovery() +{ + if (!_device.getUniqueId()) { + return false; + } + + const HASerializer* deviceSerializer = _device.getSerializer(); + if (!deviceSerializer) { + return false; + } + + HABaseDeviceType* componentTypes[_devicesTypesNb]; + HASerializer* componentSerializers[_devicesTypesNb]; + uint8_t componentSerializerCount = 0; + uint16_t componentsPayloadLength = 2; // {} + + for (uint8_t i = 0; i < _devicesTypesNb; i++) { + HABaseDeviceType* deviceType = _devicesTypes[i]; + if (!deviceType || !deviceType->supportsDeviceDiscovery()) { + continue; + } + + HASerializer* serializer = deviceType->buildDeviceDiscoverySerializer(); + if (!serializer) { + continue; + } + + if (componentSerializerCount > 0) { + componentsPayloadLength += strlen_P(HASerializerJsonPropertiesSeparator); + } + + componentsPayloadLength += + strlen_P(HASerializerJsonPropertyPrefix) + + strlen(deviceType->uniqueId()) + + strlen_P(HASerializerJsonPropertySuffix) + + serializer->calculateSize(); + + componentTypes[componentSerializerCount] = deviceType; + componentSerializers[componentSerializerCount++] = serializer; + } + + if (componentSerializerCount == 0) { + return false; + } + + char originPayload[64]; + originPayload[0] = 0; + snprintf( + originPayload, + sizeof(originPayload), + "{\"name\":\"%s\",\"sw\":\"%s\"}", + DeviceDiscoveryOriginName, + ARDUINOHA_LIBRARY_VERSION + ); + const uint16_t originPayloadLength = strlen(originPayload); + + const uint16_t topicLength = + strlen(_discoveryPrefix) + 1 + + strlen_P(HAComponentDevice) + 1 + + strlen(_device.getUniqueId()) + 1 + + strlen_P(HAConfigTopic) + 1; + if (topicLength == 0) { + for (uint8_t i = 0; i < componentSerializerCount; i++) { + delete componentSerializers[i]; + } + + return false; + } + + const uint16_t payloadLength = + strlen_P(HASerializerJsonDataPrefix) + + strlen_P(HASerializerJsonPropertyPrefix) + + strlen_P(HADeviceProperty) + + strlen_P(HASerializerJsonPropertySuffix) + + deviceSerializer->calculateSize() + + strlen_P(HASerializerJsonPropertiesSeparator) + + strlen_P(HASerializerJsonPropertyPrefix) + + strlen_P(HAOriginProperty) + + strlen_P(HASerializerJsonPropertySuffix) + + originPayloadLength + + strlen_P(HASerializerJsonPropertiesSeparator) + + strlen_P(HASerializerJsonPropertyPrefix) + + strlen_P(HAComponentsProperty) + + strlen_P(HASerializerJsonPropertySuffix) + + componentsPayloadLength + + strlen_P(HASerializerJsonDataSuffix); + + char topic[topicLength]; + strcpy(topic, _discoveryPrefix); + strcat_P(topic, HASerializerSlash); + strcat_P(topic, HAComponentDevice); + strcat_P(topic, HASerializerSlash); + strcat(topic, _device.getUniqueId()); + strcat_P(topic, HASerializerSlash); + strcat_P(topic, HAConfigTopic); + + if (!_mqtt->beginPublish(topic, payloadLength, true)) { + for (uint8_t i = 0; i < componentSerializerCount; i++) { + delete componentSerializers[i]; + } + + return false; + } + + writePayload(AHATOFSTR(HASerializerJsonDataPrefix)); + + writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix)); + writePayload(AHATOFSTR(HADeviceProperty)); + writePayload(AHATOFSTR(HASerializerJsonPropertySuffix)); + deviceSerializer->flush(); + + writePayload(AHATOFSTR(HASerializerJsonPropertiesSeparator)); + writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix)); + writePayload(AHATOFSTR(HAOriginProperty)); + writePayload(AHATOFSTR(HASerializerJsonPropertySuffix)); + writePayload(originPayload, originPayloadLength); + + writePayload(AHATOFSTR(HASerializerJsonPropertiesSeparator)); + writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix)); + writePayload(AHATOFSTR(HAComponentsProperty)); + writePayload(AHATOFSTR(HASerializerJsonPropertySuffix)); + writePayload(AHATOFSTR(HASerializerJsonDataPrefix)); + + for (uint8_t i = 0; i < componentSerializerCount; i++) { + if (i > 0) { + writePayload(AHATOFSTR(HASerializerJsonPropertiesSeparator)); + } + + writePayload(AHATOFSTR(HASerializerJsonPropertyPrefix)); + writePayload(componentTypes[i]->uniqueId(), strlen(componentTypes[i]->uniqueId())); + writePayload(AHATOFSTR(HASerializerJsonPropertySuffix)); + componentSerializers[i]->flush(); + delete componentSerializers[i]; + } + + writePayload(AHATOFSTR(HASerializerJsonDataSuffix)); + writePayload(AHATOFSTR(HASerializerJsonDataSuffix)); + return endPublish(); +} + void HAMqtt::setState(ConnectionState state) { ConnectionState previousState = _currentState; diff --git a/src/HAMqtt.h b/src/HAMqtt.h index 915f8ea..f1dfd21 100644 --- a/src/HAMqtt.h +++ b/src/HAMqtt.h @@ -117,6 +117,25 @@ public: inline const char* getDataPrefix() const { return _dataPrefix; } + /** + * Enables MQTT device discovery payloads. + * Single-component discovery remains the default when this is not enabled. + */ + inline void enableDeviceDiscovery() + { _deviceDiscoveryEnabled = true; } + + /** + * Returns true when MQTT device discovery payloads are enabled. + */ + inline bool isDeviceDiscoveryEnabled() const + { return _deviceDiscoveryEnabled; } + + /** + * Republishes the current MQTT device discovery payload when device + * discovery is enabled. + */ + bool publishDeviceDiscovery(); + /** * Returns instance of the device assigned to the HAMqtt class. * It's the same object (pointer) that was passed to the HAMqtt constructor. @@ -448,6 +467,9 @@ private: /// The data prefix that's used for publishing data messages. const char* _dataPrefix; + /// Enables MQTT device discovery mode when set to true. + bool _deviceDiscoveryEnabled; + /// The username used for the authentication. It's set in the HAMqtt::begin method. const char* _username; diff --git a/src/device-types/HABaseDeviceType.cpp b/src/device-types/HABaseDeviceType.cpp index d5ce93c..e030db6 100644 --- a/src/device-types/HABaseDeviceType.cpp +++ b/src/device-types/HABaseDeviceType.cpp @@ -12,6 +12,8 @@ HABaseDeviceType::HABaseDeviceType( _uniqueId(uniqueId), _name(nullptr), _objectId(nullptr), + _defaultEntityId(nullptr), + _entityCategory(nullptr), _serializer(nullptr), _availability(AvailabilityDefault) { @@ -31,6 +33,46 @@ void HABaseDeviceType::setAvailability(bool online) publishAvailability(); } +bool HABaseDeviceType::removeFromDiscovery() +{ + const uint16_t topicLength = HASerializer::calculateConfigTopicLength( + componentName(), + uniqueId() + ); + if (topicLength == 0) { + return false; + } + + char topic[topicLength]; + if (!HASerializer::generateConfigTopic(topic, componentName(), uniqueId())) { + return false; + } + + destroySerializer(); + if (!mqtt()->beginPublish(topic, 0, true)) { + return false; + } + + return mqtt()->endPublish(); +} + +bool HABaseDeviceType::republishDiscovery() +{ + HAMqtt* mqttInstance = mqtt(); + if (!mqttInstance) { + return false; + } + + if (shouldPublishSingleComponentConfig()) { + return publishConfig(); + } + + // Clear any stale per-entity retained config so device discovery remains + // the single source of truth for supported entities. + removeFromDiscovery(); + return mqttInstance->publishDeviceDiscovery(); +} + HAMqtt* HABaseDeviceType::mqtt() { return HAMqtt::instance(); @@ -80,12 +122,12 @@ void HABaseDeviceType::destroySerializer() } } -void HABaseDeviceType::publishConfig() +bool HABaseDeviceType::publishConfig() { buildSerializer(); if (_serializer == nullptr) { - return; + return false; } const uint16_t topicLength = HASerializer::calculateConfigTopicLength( @@ -94,6 +136,7 @@ void HABaseDeviceType::publishConfig() ); const uint16_t dataLength = _serializer->calculateSize(); + bool published = false; if (topicLength > 0 && dataLength > 0) { char topic[topicLength]; HASerializer::generateConfigTopic( @@ -104,11 +147,12 @@ void HABaseDeviceType::publishConfig() if (mqtt()->beginPublish(topic, dataLength, true)) { _serializer->flush(); - mqtt()->endPublish(); + published = mqtt()->endPublish(); } } destroySerializer(); + return published; } void HABaseDeviceType::publishAvailability() @@ -207,5 +251,43 @@ bool HABaseDeviceType::publishOnDataTopic( return mqtt()->endPublish(); } + return false; +} + +bool HABaseDeviceType::shouldPublishSingleComponentConfig() const +{ + return !mqtt() || !mqtt()->isDeviceDiscoveryEnabled() || !supportsDeviceDiscovery(); +} + +void HABaseDeviceType::setEntityIdProperty(HASerializer* serializer) const +{ + if (!serializer) { + return; + } + + const char* defaultEntityId = nonEmptyString(_defaultEntityId); + if (defaultEntityId) { + serializer->set(AHATOFSTR(HADefaultEntityIdProperty), defaultEntityId); + return; + } + + const char* objectId = nonEmptyString(_objectId); + if (objectId) { + serializer->set(AHATOFSTR(HAObjectIdProperty), objectId); + } +} + +const char* HABaseDeviceType::nonEmptyString(const char* value) +{ + return (value && value[0] != '\0') ? value : nullptr; +} + +HASerializer* HABaseDeviceType::buildDeviceDiscoverySerializer() +{ + return nullptr; +} + +bool HABaseDeviceType::supportsDeviceDiscovery() const +{ return false; } \ No newline at end of file diff --git a/src/device-types/HABaseDeviceType.h b/src/device-types/HABaseDeviceType.h index 8514bce..c934624 100644 --- a/src/device-types/HABaseDeviceType.h +++ b/src/device-types/HABaseDeviceType.h @@ -81,9 +81,26 @@ public: { return _name; } /** - * Sets object ID that will be used by HA to generate entity ID. + * Sets the default entity ID that Home Assistant should use when creating + * the entity for the first time. * Keep the ID short to save the resources. * + * @param entityId The default entity ID. + */ + inline void setDefaultEntityId(const char* entityId) + { _defaultEntityId = entityId; } + + /** + * Returns the default entity ID that was assigned via setDefaultEntityId. + * It can be nullptr if there is no ID assigned. + */ + inline const char* getDefaultEntityId() const + { return _defaultEntityId; } + + /** + * Legacy alias for the MQTT `object_id` discovery property. + * Prefer setDefaultEntityId() for new code. + * * @param objectId The object ID. */ inline void setObjectId(const char* objectId) @@ -96,6 +113,20 @@ public: inline const char* getObjectId() const { return _objectId; } + /** + * Sets the entity category for this entity. + * + * @param entityCategory The category name. + */ + inline void setEntityCategory(const char* entityCategory) + { _entityCategory = entityCategory; } + + /** + * Returns the entity category for this entity. + */ + inline const char* getEntityCategory() const + { return _entityCategory; } + /** * Sets availability of the device type. * Setting the initial availability enables availability reporting for this device type. @@ -106,6 +137,17 @@ public: */ virtual void setAvailability(bool online); + /** + * Removes this entity from MQTT discovery by publishing an empty retained + * payload on its config topic. + */ + bool removeFromDiscovery(); + + /** + * Republishes MQTT discovery config for this entity. + */ + bool republishDiscovery(); + #ifdef ARDUINOHA_TEST inline HASerializer* getSerializer() const { return _serializer; } @@ -131,6 +173,12 @@ protected: const __FlashStringHelper* topic ); + /** + * Returns true if this entity should publish the single-component + * discovery config on connect. + */ + bool shouldPublishSingleComponentConfig() const; + /** * This method should build serializer that will be used for publishing the configuration. * The serializer is built each time the MQTT connection is acquired. @@ -167,7 +215,7 @@ protected: /** * Publishes configuration of this device type on the HA discovery topic. */ - void publishConfig(); + bool publishConfig(); /** * Publishes current availability of the device type. @@ -218,6 +266,29 @@ protected: bool isProgmemData = false ); + /** + * Adds the preferred entity ID property to the serializer. + * `default_entity_id` takes precedence and the legacy `object_id` is only + * emitted when no default entity ID was configured. + */ + void setEntityIdProperty(HASerializer* serializer) const; + + /** + * Returns a string pointer if it is non-empty, otherwise nullptr. + */ + static const char* nonEmptyString(const char* value); + + /** + * Builds a serializer used as a device discovery component payload. + * Unsupported entities return nullptr. + */ + virtual HASerializer* buildDeviceDiscoverySerializer(); + + /** + * Returns true when the entity can be included in MQTT device discovery. + */ + virtual bool supportsDeviceDiscovery() const; + /// The component name that was assigned via the constructor. const __FlashStringHelper* const _componentName; @@ -230,6 +301,12 @@ protected: /// The object ID that was set using setObjectId method. It can be nullptr. const char* _objectId; + /// The default entity ID used by Home Assistant on first discovery. + const char* _defaultEntityId; + + /// The entity category for the entity. It can be nullptr. + const char* _entityCategory; + /// HASerializer that belongs to this device type. It can be nullptr. HASerializer* _serializer; diff --git a/src/device-types/HABinarySensor.cpp b/src/device-types/HABinarySensor.cpp index b320cf3..f72759d 100644 --- a/src/device-types/HABinarySensor.cpp +++ b/src/device-types/HABinarySensor.cpp @@ -7,7 +7,6 @@ HABinarySensor::HABinarySensor(const char* uniqueId) : HABaseDeviceType(AHATOFSTR(HAComponentBinarySensor), uniqueId), _class(nullptr), - _entityCategory(nullptr), _icon(nullptr), _currentState(false) { @@ -45,10 +44,10 @@ void HABinarySensor::buildSerializer() _serializer = new HASerializer(this, 10); // 10 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); - _serializer->set(AHATOFSTR(HAStateEntityCategory), _entityCategory); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); if (_expireAfter.isSet()) { @@ -64,13 +63,47 @@ void HABinarySensor::buildSerializer() _serializer->topic(AHATOFSTR(HAStateTopic)); } +HASerializer* HABinarySensor::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 10); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentBinarySensor), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_expireAfter.isSet()) { + serializer->set( + AHATOFSTR(HAExpireAfterProperty), + &_expireAfter, + HASerializer::NumberPropertyType + ); + } + + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HAStateTopic)); + return serializer; +} + void HABinarySensor::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); publishState(_currentState); } diff --git a/src/device-types/HABinarySensor.h b/src/device-types/HABinarySensor.h index 7adddf7..170c5da 100644 --- a/src/device-types/HABinarySensor.h +++ b/src/device-types/HABinarySensor.h @@ -63,15 +63,6 @@ public: inline void setDeviceClass(const char* deviceClass) { _class = deviceClass; } - /** - * Sets the entity category for the sensor. - * See: https://www.home-assistant.io/integrations/binary_sensor.mqtt/#entity_category - * - * @param entityCategory The category name. - */ - inline void setEntityCategory(const char* entityCategory) - { _entityCategory = entityCategory; } - /** * Sets icon of the sensor. * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). @@ -83,6 +74,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; private: @@ -97,9 +91,6 @@ private: /// The device class. It can be nullptr. const char* _class; - /// The entity category for the sensor. It can be nullptr. - const char* _entityCategory; - /// The icon of the sensor. It can be nullptr. const char* _icon; diff --git a/src/device-types/HAButton.cpp b/src/device-types/HAButton.cpp index d173fb4..d9d16a8 100644 --- a/src/device-types/HAButton.cpp +++ b/src/device-types/HAButton.cpp @@ -7,7 +7,6 @@ HAButton::HAButton(const char* uniqueId) : HABaseDeviceType(AHATOFSTR(HAComponentButton), uniqueId), _class(nullptr), - _entityCategory(nullptr), _icon(nullptr), _retain(false), _commandCallback(nullptr) @@ -23,10 +22,10 @@ void HAButton::buildSerializer() _serializer = new HASerializer(this, 10); // 10 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); - _serializer->set(AHATOFSTR(HAStateEntityCategory), _entityCategory); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); // optional property @@ -43,13 +42,47 @@ void HAButton::buildSerializer() _serializer->topic(AHATOFSTR(HACommandTopic)); } +HASerializer* HAButton::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 10); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentButton), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HACommandTopic)); + return serializer; +} + void HAButton::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); } diff --git a/src/device-types/HAButton.h b/src/device-types/HAButton.h index e3366b6..c4d094e 100644 --- a/src/device-types/HAButton.h +++ b/src/device-types/HAButton.h @@ -36,15 +36,6 @@ public: inline void setDeviceClass(const char* deviceClass) { _class = deviceClass; } - /** - * Sets the entity category for the button. - * See: https://www.home-assistant.io/integrations/button.mqtt/#entity_category - * - * @param entityCategory The category name. - */ - inline void setEntityCategory(const char* entityCategory) - { _entityCategory = entityCategory; } - /** * Sets icon of the button. * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). @@ -93,6 +84,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; virtual void onMqttMessage( const char* topic, @@ -104,9 +98,6 @@ private: /// The device class. It can be nullptr. const char* _class; - /// The entity category for the button. It can be nullptr. - const char* _entityCategory; - /// The icon of the button. It can be nullptr. const char* _icon; diff --git a/src/device-types/HACamera.cpp b/src/device-types/HACamera.cpp index 8a9650d..78bf697 100644 --- a/src/device-types/HACamera.cpp +++ b/src/device-types/HACamera.cpp @@ -27,10 +27,11 @@ void HACamera::buildSerializer() return; } - _serializer = new HASerializer(this, 8); // 8 - max properties nb + _serializer = new HASerializer(this, 9); // 9 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); _serializer->set( AHATOFSTR(HAEncodingProperty), @@ -42,13 +43,42 @@ void HACamera::buildSerializer() _serializer->topic(AHATOFSTR(HATopic)); } +HASerializer* HACamera::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 9); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentCamera), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + serializer->set( + AHATOFSTR(HAEncodingProperty), + getEncodingProperty(), + HASerializer::ProgmemPropertyValue + ); + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HATopic)); + return serializer; +} + void HACamera::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); } diff --git a/src/device-types/HACamera.h b/src/device-types/HACamera.h index c2bcc2b..7bfa695 100644 --- a/src/device-types/HACamera.h +++ b/src/device-types/HACamera.h @@ -57,6 +57,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; private: diff --git a/src/device-types/HACover.cpp b/src/device-types/HACover.cpp index e09d0fb..26112a4 100644 --- a/src/device-types/HACover.cpp +++ b/src/device-types/HACover.cpp @@ -57,11 +57,12 @@ void HACover::buildSerializer() return; } - _serializer = new HASerializer(this, 12); // 12 - max properties nb + _serializer = new HASerializer(this, 13); // 13 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); if (_retain) { @@ -90,13 +91,61 @@ void HACover::buildSerializer() } } +HASerializer* HACover::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 13); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentCover), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HAStateTopic)); + serializer->topic(AHATOFSTR(HACommandTopic)); + + if (_features & PositionFeature) { + serializer->topic(AHATOFSTR(HAPositionTopic)); + } + + return serializer; +} + void HACover::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); if (!_retain) { diff --git a/src/device-types/HACover.h b/src/device-types/HACover.h index 8fb0ba0..188b174 100644 --- a/src/device-types/HACover.h +++ b/src/device-types/HACover.h @@ -172,6 +172,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; virtual void onMqttMessage( const char* topic, diff --git a/src/device-types/HADeviceTracker.cpp b/src/device-types/HADeviceTracker.cpp index 81e6a06..ae8e26d 100644 --- a/src/device-types/HADeviceTracker.cpp +++ b/src/device-types/HADeviceTracker.cpp @@ -33,10 +33,11 @@ void HADeviceTracker::buildSerializer() return; } - _serializer = new HASerializer(this, 8); // 8 - max properties nb + _serializer = new HASerializer(this, 9); // 9 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); _serializer->set( AHATOFSTR(HASourceTypeProperty), @@ -48,13 +49,42 @@ void HADeviceTracker::buildSerializer() _serializer->topic(AHATOFSTR(HAStateTopic)); } +HASerializer* HADeviceTracker::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 9); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentDeviceTracker), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + serializer->set( + AHATOFSTR(HASourceTypeProperty), + getSourceTypeProperty(), + HASerializer::ProgmemPropertyValue + ); + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HAStateTopic)); + return serializer; +} + void HADeviceTracker::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); publishState(_currentState); } diff --git a/src/device-types/HADeviceTracker.h b/src/device-types/HADeviceTracker.h index 9f5a693..a78eb14 100644 --- a/src/device-types/HADeviceTracker.h +++ b/src/device-types/HADeviceTracker.h @@ -84,6 +84,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; private: diff --git a/src/device-types/HADeviceTrigger.cpp b/src/device-types/HADeviceTrigger.cpp index fc83114..5f30389 100644 --- a/src/device-types/HADeviceTrigger.cpp +++ b/src/device-types/HADeviceTrigger.cpp @@ -90,13 +90,50 @@ void HADeviceTrigger::buildSerializer() _serializer->topic(AHATOFSTR(HATopic)); } +HASerializer* HADeviceTrigger::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 6); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentDeviceAutomation), + HASerializer::ProgmemPropertyValue + ); + serializer->set( + AHATOFSTR(HAAutomationTypeProperty), + AHATOFSTR(HATrigger), + HASerializer::ProgmemPropertyValue + ); + serializer->set( + AHATOFSTR(HATypeProperty), + _type, + _isProgmemType + ? HASerializer::ProgmemPropertyValue + : HASerializer::ConstCharPropertyValue + ); + serializer->set( + AHATOFSTR(HASubtypeProperty), + _subtype, + _isProgmemSubtype + ? HASerializer::ProgmemPropertyValue + : HASerializer::ConstCharPropertyValue + ); + serializer->topic(AHATOFSTR(HATopic)); + return serializer; +} + void HADeviceTrigger::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } } uint16_t HADeviceTrigger::calculateIdSize() const diff --git a/src/device-types/HADeviceTrigger.h b/src/device-types/HADeviceTrigger.h index 91b4156..aa76e9c 100644 --- a/src/device-types/HADeviceTrigger.h +++ b/src/device-types/HADeviceTrigger.h @@ -127,6 +127,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; private: diff --git a/src/device-types/HAFan.cpp b/src/device-types/HAFan.cpp index 114aa74..8772bd2 100644 --- a/src/device-types/HAFan.cpp +++ b/src/device-types/HAFan.cpp @@ -59,10 +59,11 @@ void HAFan::buildSerializer() return; } - _serializer = new HASerializer(this, 14); // 14 - max properties nb + _serializer = new HASerializer(this, 15); // 15 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); if (_retain) { @@ -108,13 +109,76 @@ void HAFan::buildSerializer() _serializer->topic(AHATOFSTR(HACommandTopic)); } +HASerializer* HAFan::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 15); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentFan), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + if (_features & SpeedsFeature) { + serializer->topic(AHATOFSTR(HAPercentageStateTopic)); + serializer->topic(AHATOFSTR(HAPercentageCommandTopic)); + + if (_speedRangeMax.isSet()) { + serializer->set( + AHATOFSTR(HASpeedRangeMaxProperty), + &_speedRangeMax, + HASerializer::NumberPropertyType + ); + } + + if (_speedRangeMin.isSet()) { + serializer->set( + AHATOFSTR(HASpeedRangeMinProperty), + &_speedRangeMin, + HASerializer::NumberPropertyType + ); + } + } + + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HAStateTopic)); + serializer->topic(AHATOFSTR(HACommandTopic)); + return serializer; +} + void HAFan::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); if (!_retain) { diff --git a/src/device-types/HAFan.h b/src/device-types/HAFan.h index f62d21d..61a8a40 100644 --- a/src/device-types/HAFan.h +++ b/src/device-types/HAFan.h @@ -210,6 +210,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; virtual void onMqttMessage( const char* topic, diff --git a/src/device-types/HAHVAC.cpp b/src/device-types/HAHVAC.cpp index c242459..450c80f 100644 --- a/src/device-types/HAHVAC.cpp +++ b/src/device-types/HAHVAC.cpp @@ -191,10 +191,11 @@ void HAHVAC::buildSerializer() return; } - _serializer = new HASerializer(this, 28); // 28 - max properties nb + _serializer = new HASerializer(this, 29); // 29 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); if (_retain) { @@ -362,13 +363,198 @@ void HAHVAC::buildSerializer() _serializer->set(HASerializer::WithAvailability); } +HASerializer* HAHVAC::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 29); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentClimate), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_features & ActionFeature) { + serializer->topic(AHATOFSTR(HAActionTopic)); + } + + if (_features & AuxHeatingFeature) { + serializer->topic(AHATOFSTR(HAAuxCommandTopic)); + serializer->topic(AHATOFSTR(HAAuxStateTopic)); + } + + if (_features & PowerFeature) { + serializer->topic(AHATOFSTR(HAPowerCommandTopic)); + } + + if (_features & FanFeature) { + serializer->topic(AHATOFSTR(HAFanModeCommandTopic)); + serializer->topic(AHATOFSTR(HAFanModeStateTopic)); + + if (_fanModes != DefaultFanModes) { + _fanModesSerializer->clear(); + + if (_fanModes & AutoFanMode) { + _fanModesSerializer->add(HAFanModeAuto); + } + + if (_fanModes & LowFanMode) { + _fanModesSerializer->add(HAFanModeLow); + } + + if (_fanModes & MediumFanMode) { + _fanModesSerializer->add(HAFanModeMedium); + } + + if (_fanModes & HighFanMode) { + _fanModesSerializer->add(HAFanModeHigh); + } + + serializer->set( + AHATOFSTR(HAFanModesProperty), + _fanModesSerializer, + HASerializer::ArrayPropertyType + ); + } + } + + if (_features & SwingFeature) { + serializer->topic(AHATOFSTR(HASwingModeCommandTopic)); + serializer->topic(AHATOFSTR(HASwingModeStateTopic)); + + if (_swingModes != DefaultSwingModes) { + _swingModesSerializer->clear(); + + if (_swingModes & OnSwingMode) { + _swingModesSerializer->add(HASwingModeOn); + } + + if (_swingModes & OffSwingMode) { + _swingModesSerializer->add(HASwingModeOff); + } + + serializer->set( + AHATOFSTR(HASwingModesProperty), + _swingModesSerializer, + HASerializer::ArrayPropertyType + ); + } + } + + if (_features & ModesFeature) { + serializer->topic(AHATOFSTR(HAModeCommandTopic)); + serializer->topic(AHATOFSTR(HAModeStateTopic)); + + if (_modes != DefaultModes) { + _modesSerializer->clear(); + + if (_modes & AutoMode) { + _modesSerializer->add(HAModeAuto); + } + + if (_modes & OffMode) { + _modesSerializer->add(HAModeOff); + } + + if (_modes & CoolMode) { + _modesSerializer->add(HAModeCool); + } + + if (_modes & HeatMode) { + _modesSerializer->add(HAModeHeat); + } + + if (_modes & DryMode) { + _modesSerializer->add(HAModeDry); + } + + if (_modes & FanOnlyMode) { + _modesSerializer->add(HAModeFanOnly); + } + + serializer->set( + AHATOFSTR(HAModesProperty), + _modesSerializer, + HASerializer::ArrayPropertyType + ); + } + } + + if (_features & TargetTemperatureFeature) { + serializer->topic(AHATOFSTR(HATemperatureCommandTopic)); + serializer->topic(AHATOFSTR(HATemperatureStateTopic)); + serializer->set( + AHATOFSTR(HATemperatureCommandTemplateProperty), + getCommandWithFloatTemplate(), + HASerializer::ProgmemPropertyValue + ); + } + + if (_temperatureUnit != DefaultUnit) { + const __FlashStringHelper *unitStr = _temperatureUnit == CelsiusUnit + ? AHATOFSTR(HATemperatureUnitC) + : AHATOFSTR(HATemperatureUnitF); + + serializer->set( + AHATOFSTR(HATemperatureUnitProperty), + unitStr, + HASerializer::ProgmemPropertyValue + ); + } + + if (_minTemp.isSet()) { + serializer->set( + AHATOFSTR(HAMinTempProperty), + &_minTemp, + HASerializer::NumberPropertyType + ); + } + + if (_maxTemp.isSet()) { + serializer->set( + AHATOFSTR(HAMaxTempProperty), + &_maxTemp, + HASerializer::NumberPropertyType + ); + } + + if (_tempStep.isSet()) { + serializer->set( + AHATOFSTR(HATempStepProperty), + &_tempStep, + HASerializer::NumberPropertyType + ); + } + + serializer->topic(AHATOFSTR(HACurrentTemperatureTopic)); + serializer->set(HASerializer::WithAvailability); + return serializer; +} + void HAHVAC::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); if (!_retain) { diff --git a/src/device-types/HAHVAC.h b/src/device-types/HAHVAC.h index b124de1..3283d50 100644 --- a/src/device-types/HAHVAC.h +++ b/src/device-types/HAHVAC.h @@ -596,6 +596,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; virtual void onMqttMessage( const char* topic, diff --git a/src/device-types/HALight.cpp b/src/device-types/HALight.cpp index 9a29c8a..7de92a3 100644 --- a/src/device-types/HALight.cpp +++ b/src/device-types/HALight.cpp @@ -134,10 +134,11 @@ void HALight::buildSerializer() return; } - _serializer = new HASerializer(this, 19); // 19 - max properties nb + _serializer = new HASerializer(this, 20); // 20 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); if (_retain) { @@ -201,13 +202,94 @@ void HALight::buildSerializer() _serializer->topic(AHATOFSTR(HACommandTopic)); } +HASerializer* HALight::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 20); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentLight), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + if (_features & BrightnessFeature) { + serializer->topic(AHATOFSTR(HABrightnessStateTopic)); + serializer->topic(AHATOFSTR(HABrightnessCommandTopic)); + + if (_brightnessScale.isSet()) { + serializer->set( + AHATOFSTR(HABrightnessScaleProperty), + &_brightnessScale, + HASerializer::NumberPropertyType + ); + } + } + + if (_features & ColorTemperatureFeature) { + serializer->topic(AHATOFSTR(HAColorTemperatureStateTopic)); + serializer->topic(AHATOFSTR(HAColorTemperatureCommandTopic)); + + if (_minMireds.isSet()) { + serializer->set( + AHATOFSTR(HAMinMiredsProperty), + &_minMireds, + HASerializer::NumberPropertyType + ); + } + + if (_maxMireds.isSet()) { + serializer->set( + AHATOFSTR(HAMaxMiredsProperty), + &_maxMireds, + HASerializer::NumberPropertyType + ); + } + } + + if (_features & RGBFeature) { + serializer->topic(AHATOFSTR(HARGBCommandTopic)); + serializer->topic(AHATOFSTR(HARGBStateTopic)); + } + + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HAStateTopic)); + serializer->topic(AHATOFSTR(HACommandTopic)); + return serializer; +} + void HALight::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); if (!_retain) { diff --git a/src/device-types/HALight.h b/src/device-types/HALight.h index c97cc47..dfe05cb 100644 --- a/src/device-types/HALight.h +++ b/src/device-types/HALight.h @@ -373,6 +373,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; virtual void onMqttMessage( const char* topic, diff --git a/src/device-types/HALock.cpp b/src/device-types/HALock.cpp index 8dbe510..25f92f1 100644 --- a/src/device-types/HALock.cpp +++ b/src/device-types/HALock.cpp @@ -38,10 +38,11 @@ void HALock::buildSerializer() return; } - _serializer = new HASerializer(this, 10); // 10 - max properties nb + _serializer = new HASerializer(this, 11); // 11 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); if (_retain) { @@ -66,13 +67,55 @@ void HALock::buildSerializer() _serializer->topic(AHATOFSTR(HACommandTopic)); } +HASerializer* HALock::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 11); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentLock), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HAStateTopic)); + serializer->topic(AHATOFSTR(HACommandTopic)); + return serializer; +} + void HALock::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); if (!_retain) { diff --git a/src/device-types/HALock.h b/src/device-types/HALock.h index 4ad47cf..2fe56ca 100644 --- a/src/device-types/HALock.h +++ b/src/device-types/HALock.h @@ -127,6 +127,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; virtual void onMqttMessage( const char* topic, diff --git a/src/device-types/HANumber.cpp b/src/device-types/HANumber.cpp index 5aad230..55ed506 100644 --- a/src/device-types/HANumber.cpp +++ b/src/device-types/HANumber.cpp @@ -8,7 +8,6 @@ HANumber::HANumber(const char* uniqueId, const NumberPrecision precision) : HABaseDeviceType(AHATOFSTR(HAComponentNumber), uniqueId), _precision(precision), _class(nullptr), - _entityCategory(nullptr), _icon(nullptr), _retain(false), _optimistic(false), @@ -42,7 +41,7 @@ void HANumber::updateMinMaxStep(const float min, const float max, const float st _minValue = HANumeric(min, _precision); _maxValue = HANumeric(max, _precision); _step = HANumeric(step, _precision); - publishConfig(); + republishDiscovery(); } void HANumber::buildSerializer() @@ -53,12 +52,12 @@ void HANumber::buildSerializer() _serializer = new HASerializer(this, 17); // 17 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); - _serializer->set(AHATOFSTR(HAStateEntityCategory), _entityCategory); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); - _serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), _unitOfMeasurement); + _serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), nonEmptyString(_unitOfMeasurement)); _serializer->set( AHATOFSTR(HAModeProperty), getModeProperty(), @@ -116,13 +115,91 @@ void HANumber::buildSerializer() _serializer->topic(AHATOFSTR(HACommandTopic)); } +HASerializer* HANumber::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 17); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentNumber), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), nonEmptyString(_unitOfMeasurement)); + serializer->set( + AHATOFSTR(HAModeProperty), + getModeProperty(), + HASerializer::ProgmemPropertyValue + ); + serializer->set( + AHATOFSTR(HACommandTemplateProperty), + getCommandTemplate(), + HASerializer::ProgmemPropertyValue + ); + + if (_minValue.isSet()) { + serializer->set( + AHATOFSTR(HAMinProperty), + &_minValue, + HASerializer::NumberPropertyType + ); + } + + if (_maxValue.isSet()) { + serializer->set( + AHATOFSTR(HAMaxProperty), + &_maxValue, + HASerializer::NumberPropertyType + ); + } + + if (_step.isSet()) { + serializer->set( + AHATOFSTR(HAStepProperty), + &_step, + HASerializer::NumberPropertyType + ); + } + + if (_retain) { + serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HAStateTopic)); + serializer->topic(AHATOFSTR(HACommandTopic)); + return serializer; +} + void HANumber::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); if (!_retain) { diff --git a/src/device-types/HANumber.h b/src/device-types/HANumber.h index 2374c36..9e4ff0d 100644 --- a/src/device-types/HANumber.h +++ b/src/device-types/HANumber.h @@ -110,15 +110,6 @@ public: inline void setDeviceClass(const char* deviceClass) { _class = deviceClass; } - /** - * Sets the entity category for the number. - * See: https://www.home-assistant.io/integrations/number.mqtt/#entity_category - * - * @param entityCategory The category name. - */ - inline void setEntityCategory(const char* entityCategory) - { _entityCategory = entityCategory; } - /** * Sets icon of the number. * Any icon from MaterialDesignIcons.com (for example: `mdi:home`). @@ -230,6 +221,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; virtual void onMqttMessage( const char* topic, @@ -270,9 +264,6 @@ private: /// The device class. It can be nullptr. const char* _class; - /// The entity category for the number. It can be nullptr. - const char* _entityCategory; - /// The icon of the number. It can be nullptr. const char* _icon; diff --git a/src/device-types/HAScene.cpp b/src/device-types/HAScene.cpp index d431220..0853a6b 100644 --- a/src/device-types/HAScene.cpp +++ b/src/device-types/HAScene.cpp @@ -22,10 +22,11 @@ void HAScene::buildSerializer() return; } - _serializer = new HASerializer(this, 8); // 8 - max properties nb + _serializer = new HASerializer(this, 9); // 9 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); // optional property @@ -48,13 +49,51 @@ void HAScene::buildSerializer() _serializer->topic(AHATOFSTR(HACommandTopic)); } +HASerializer* HAScene::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 9); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentScene), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + serializer->set( + AHATOFSTR(HAPayloadOnProperty), + AHATOFSTR(HAStateOn), + HASerializer::ProgmemPropertyValue + ); + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HACommandTopic)); + return serializer; +} + void HAScene::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); subscribeTopic(uniqueId(), AHATOFSTR(HACommandTopic)); } diff --git a/src/device-types/HAScene.h b/src/device-types/HAScene.h index 6d51986..99401e1 100644 --- a/src/device-types/HAScene.h +++ b/src/device-types/HAScene.h @@ -74,6 +74,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; virtual void onMqttMessage( const char* topic, diff --git a/src/device-types/HASelect.cpp b/src/device-types/HASelect.cpp index 6800bf4..ab4feff 100644 --- a/src/device-types/HASelect.cpp +++ b/src/device-types/HASelect.cpp @@ -102,10 +102,11 @@ void HASelect::buildSerializer() return; } - _serializer = new HASerializer(this, 11); // 11 - max properties nb + _serializer = new HASerializer(this, 12); // 12 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); _serializer->set( AHATOFSTR(HAOptionsProperty), @@ -135,13 +136,60 @@ void HASelect::buildSerializer() _serializer->topic(AHATOFSTR(HACommandTopic)); } +HASerializer* HASelect::buildDeviceDiscoverySerializer() +{ + if (!uniqueId() || !_options) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 12); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentSelect), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + serializer->set( + AHATOFSTR(HAOptionsProperty), + _options, + HASerializer::ArrayPropertyType + ); + + if (_retain) { + serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HAStateTopic)); + serializer->topic(AHATOFSTR(HACommandTopic)); + return serializer; +} + void HASelect::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); if (!_retain) { diff --git a/src/device-types/HASelect.h b/src/device-types/HASelect.h index 63f885c..858b227 100644 --- a/src/device-types/HASelect.h +++ b/src/device-types/HASelect.h @@ -141,6 +141,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; virtual void onMqttMessage( const char* topic, diff --git a/src/device-types/HASensor.cpp b/src/device-types/HASensor.cpp index 7f16fc5..1df8429 100644 --- a/src/device-types/HASensor.cpp +++ b/src/device-types/HASensor.cpp @@ -9,7 +9,6 @@ HASensor::HASensor(const char* uniqueId, const uint16_t features) : _features(features), _deviceClass(nullptr), _stateClass(nullptr), - _entityCategory(nullptr), _forceUpdate(false), _icon(nullptr), _unitOfMeasurement(nullptr), @@ -49,13 +48,13 @@ void HASensor::buildSerializer() _serializer = new HASerializer(this, 14); // 14 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _deviceClass); _serializer->set(AHATOFSTR(HAStateClassProperty), _stateClass); - _serializer->set(AHATOFSTR(HAStateEntityCategory), _entityCategory); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); - _serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), _unitOfMeasurement); + _serializer->set(AHATOFSTR(HAUnitOfMeasurementProperty), nonEmptyString(_unitOfMeasurement)); if (_forceUpdate) { _serializer->set( @@ -82,13 +81,61 @@ void HASensor::buildSerializer() _serializer->topic(AHATOFSTR(HAStateTopic)); } +HASerializer* HASensor::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 14); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentSensor), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + 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 (_forceUpdate) { + serializer->set( + AHATOFSTR(HAForceUpdateProperty), + &_forceUpdate, + HASerializer::BoolPropertyType + ); + } + + if (_expireAfter.isSet()) { + serializer->set( + AHATOFSTR(HAExpireAfterProperty), + &_expireAfter, + HASerializer::NumberPropertyType + ); + } + + if (_features & JsonAttributesFeature) { + serializer->topic(AHATOFSTR(HAJsonAttributesTopic)); + } + + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HAStateTopic)); + return serializer; +} + void HASensor::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); } diff --git a/src/device-types/HASensor.h b/src/device-types/HASensor.h index 39414a6..559d796 100644 --- a/src/device-types/HASensor.h +++ b/src/device-types/HASensor.h @@ -73,15 +73,6 @@ public: inline void setStateClass(const char* stateClass) { _stateClass = stateClass; } - /** - * Sets the entity category for the sensor. - * See: https://www.home-assistant.io/integrations/sensor.mqtt/#entity_category - * - * @param entityCategory The category name. - */ - inline void setEntityCategory(const char* entityCategory) - { _entityCategory = entityCategory; } - /** * Forces HA panel to process each incoming value (MQTT message). * It's useful if you want to have meaningful value graphs in history. @@ -110,6 +101,9 @@ public: protected: virtual void buildSerializer() override final; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; private: @@ -122,9 +116,6 @@ private: /// The state class for the long term stats. It can be nullptr. See: https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics const char* _stateClass; - /// The entity category for the sensor. It can be nullptr. - const char* _entityCategory; - /// The force update flag for the HA panel. bool _forceUpdate; diff --git a/src/device-types/HASwitch.cpp b/src/device-types/HASwitch.cpp index 2ba796f..e7c3f0b 100644 --- a/src/device-types/HASwitch.cpp +++ b/src/device-types/HASwitch.cpp @@ -36,11 +36,12 @@ void HASwitch::buildSerializer() return; } - _serializer = new HASerializer(this, 11); // 11 - max properties nb + _serializer = new HASerializer(this, 12); // 12 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); _serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); // optional property @@ -66,13 +67,56 @@ void HASwitch::buildSerializer() _serializer->topic(AHATOFSTR(HACommandTopic)); } +HASerializer* HASwitch::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 12); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentSwitch), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HADeviceClassProperty), _class); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + + if (_retain) { + serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HAStateTopic)); + serializer->topic(AHATOFSTR(HACommandTopic)); + return serializer; +} + void HASwitch::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); if (!_retain) { diff --git a/src/device-types/HASwitch.h b/src/device-types/HASwitch.h index 0067fac..542c98d 100644 --- a/src/device-types/HASwitch.h +++ b/src/device-types/HASwitch.h @@ -134,6 +134,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; virtual void onMqttMessage( const char* topic, diff --git a/src/device-types/HATagScanner.cpp b/src/device-types/HATagScanner.cpp index efac4fe..3c25a1f 100644 --- a/src/device-types/HATagScanner.cpp +++ b/src/device-types/HATagScanner.cpp @@ -30,13 +30,31 @@ void HATagScanner::buildSerializer() _serializer->topic(AHATOFSTR(HATopic)); } +HASerializer* HATagScanner::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 2); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentTag), + HASerializer::ProgmemPropertyValue + ); + serializer->topic(AHATOFSTR(HATopic)); + return serializer; +} + void HATagScanner::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } } #endif diff --git a/src/device-types/HATagScanner.h b/src/device-types/HATagScanner.h index 6f0c576..d4844bd 100644 --- a/src/device-types/HATagScanner.h +++ b/src/device-types/HATagScanner.h @@ -31,6 +31,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; }; diff --git a/src/device-types/HAText.cpp b/src/device-types/HAText.cpp index 1c8b489..1d7d1fa 100644 --- a/src/device-types/HAText.cpp +++ b/src/device-types/HAText.cpp @@ -50,10 +50,11 @@ void HAText::buildSerializer() return; } - _serializer = new HASerializer(this, 14); // 14 - max properties nb + _serializer = new HASerializer(this, 15); // 15 - max properties nb _serializer->set(AHATOFSTR(HANameProperty), _name); - _serializer->set(AHATOFSTR(HAObjectIdProperty), _objectId); + setEntityIdProperty(_serializer); _serializer->set(HASerializer::WithUniqueId); + _serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); _serializer->set(AHATOFSTR(HAIconProperty), _icon); _serializer->set( AHATOFSTR(HAModeProperty), @@ -100,13 +101,77 @@ void HAText::buildSerializer() _serializer->topic(AHATOFSTR(HACommandTopic)); } +HASerializer* HAText::buildDeviceDiscoverySerializer() +{ + if (!uniqueId()) { + return nullptr; + } + + HASerializer* serializer = new HASerializer(this, 15); + serializer->set( + AHATOFSTR(HAPlatformProperty), + AHATOFSTR(HAComponentText), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HANameProperty), _name); + setEntityIdProperty(serializer); + serializer->set(HASerializer::WithUniqueId); + serializer->set(AHATOFSTR(HAStateEntityCategory), nonEmptyString(_entityCategory)); + serializer->set(AHATOFSTR(HAIconProperty), _icon); + serializer->set( + AHATOFSTR(HAModeProperty), + getModeProperty(), + HASerializer::ProgmemPropertyValue + ); + serializer->set(AHATOFSTR(HAPatternProperty), _pattern); + + if (_minValue.isSet()) { + serializer->set( + AHATOFSTR(HAMinProperty), + &_minValue, + HASerializer::NumberPropertyType + ); + } + + if (_maxValue.isSet()) { + serializer->set( + AHATOFSTR(HAMaxProperty), + &_maxValue, + HASerializer::NumberPropertyType + ); + } + + if (_retain) { + serializer->set( + AHATOFSTR(HARetainProperty), + &_retain, + HASerializer::BoolPropertyType + ); + } + + if (_optimistic) { + serializer->set( + AHATOFSTR(HAOptimisticProperty), + &_optimistic, + HASerializer::BoolPropertyType + ); + } + + serializer->set(HASerializer::WithAvailability); + serializer->topic(AHATOFSTR(HAStateTopic)); + serializer->topic(AHATOFSTR(HACommandTopic)); + return serializer; +} + void HAText::onMqttConnected() { if (!uniqueId()) { return; } - publishConfig(); + if (shouldPublishSingleComponentConfig()) { + publishConfig(); + } publishAvailability(); if (!_retain && _currentState) { diff --git a/src/device-types/HAText.h b/src/device-types/HAText.h index 7cfcf88..2d5cf32 100644 --- a/src/device-types/HAText.h +++ b/src/device-types/HAText.h @@ -152,6 +152,9 @@ public: protected: virtual void buildSerializer() override; + virtual HASerializer* buildDeviceDiscoverySerializer() override; + virtual bool supportsDeviceDiscovery() const override + { return true; } virtual void onMqttConnected() override; virtual void onMqttMessage( const char* topic, diff --git a/src/utils/HADictionary.cpp b/src/utils/HADictionary.cpp index 5e13814..2a9f2b0 100644 --- a/src/utils/HADictionary.cpp +++ b/src/utils/HADictionary.cpp @@ -8,6 +8,7 @@ const char HAComponentCamera[] PROGMEM = {"camera"}; const char HAComponentCover[] PROGMEM = {"cover"}; const char HAComponentDeviceTracker[] PROGMEM = {"device_tracker"}; const char HAComponentDeviceAutomation[] PROGMEM = {"device_automation"}; +const char HAComponentDevice[] PROGMEM = {"device"}; const char HAComponentLock[] PROGMEM = {"lock"}; const char HAComponentNumber[] PROGMEM = {"number"}; const char HAComponentSelect[] PROGMEM = {"select"}; @@ -40,8 +41,13 @@ const char HADeviceSoftwareVersionProperty[] PROGMEM = {"sw"}; const char HADeviceConfigurationUrlProperty[] PROGMEM = {"cu"}; const char HANameProperty[] PROGMEM = {"name"}; const char HAUniqueIdProperty[] PROGMEM = {"uniq_id"}; +const char HADefaultEntityIdProperty[] PROGMEM = {"def_ent_id"}; const char HAObjectIdProperty[] PROGMEM = {"obj_id"}; const char HADeviceProperty[] PROGMEM = {"dev"}; +const char HAOriginProperty[] PROGMEM = {"o"}; +const char HAComponentsProperty[] PROGMEM = {"cmps"}; +const char HAPlatformProperty[] PROGMEM = {"p"}; +const char HAGroupProperty[] PROGMEM = {"grp"}; const char HADeviceClassProperty[] PROGMEM = {"dev_cla"}; const char HAStateEntityCategory[] PROGMEM = {"ent_cat"}; const char HAStateClassProperty[] PROGMEM = {"stat_cla"}; diff --git a/src/utils/HADictionary.h b/src/utils/HADictionary.h index 23e1b91..f324d4b 100644 --- a/src/utils/HADictionary.h +++ b/src/utils/HADictionary.h @@ -8,6 +8,7 @@ extern const char HAComponentCamera[]; extern const char HAComponentCover[]; extern const char HAComponentDeviceTracker[]; extern const char HAComponentDeviceAutomation[]; +extern const char HAComponentDevice[]; extern const char HAComponentLock[]; extern const char HAComponentNumber[]; extern const char HAComponentSelect[]; @@ -40,8 +41,13 @@ extern const char HADeviceSoftwareVersionProperty[]; extern const char HADeviceConfigurationUrlProperty[]; extern const char HANameProperty[]; extern const char HAUniqueIdProperty[]; +extern const char HADefaultEntityIdProperty[]; extern const char HAObjectIdProperty[]; extern const char HADeviceProperty[]; +extern const char HAOriginProperty[]; +extern const char HAComponentsProperty[]; +extern const char HAPlatformProperty[]; +extern const char HAGroupProperty[]; extern const char HADeviceClassProperty[]; extern const char HAStateEntityCategory[]; extern const char HAStateClassProperty[]; diff --git a/tests/BaseDeviceTypeTest/BaseDeviceTypeTest.ino b/tests/BaseDeviceTypeTest/BaseDeviceTypeTest.ino index d334f40..c30e6a3 100644 --- a/tests/BaseDeviceTypeTest/BaseDeviceTypeTest.ino +++ b/tests/BaseDeviceTypeTest/BaseDeviceTypeTest.ino @@ -12,6 +12,7 @@ static const char* testUniqueId = "uniqueId"; const char AvailabilityTopic[] PROGMEM = {"testData/testDevice/uniqueId/avty_t"}; const char SharedAvailabilityTopic[] PROGMEM = {"testData/testDevice/avty_t"}; +const char ConfigTopic[] PROGMEM = {"homeassistant/componentName/testDevice/uniqueId/config"}; const char ComponentNameStr[] PROGMEM = {"componentName"}; class DummyDeviceType : public HABaseDeviceType @@ -21,6 +22,17 @@ public: HABaseDeviceType(componentName, uniqueId) { } protected: + virtual void buildSerializer() override { + if (_serializer || !uniqueId()) { + return; + } + + _serializer = new HASerializer(this, 3); + _serializer->set(AHATOFSTR(HANameProperty), "testName"); + setEntityIdProperty(_serializer); + _serializer->set(HASerializer::WithUniqueId); + } + virtual void onMqttConnected() override { publishAvailability(); } @@ -60,6 +72,13 @@ AHA_TEST(BaseDeviceTypeTest, object_id_setter) { assertEqual(objectId, deviceType.getObjectId()); } +AHA_TEST(BaseDeviceTypeTest, default_entity_id_setter) { + const char* entityId = "sensor.test_id"; + DummyDeviceType deviceType(AHATOFSTR(ComponentNameStr), testUniqueId); + deviceType.setDefaultEntityId(entityId); + assertEqual(entityId, deviceType.getDefaultEntityId()); +} + AHA_TEST(BaseDeviceTypeTest, default_availability) { DummyDeviceType deviceType(AHATOFSTR(ComponentNameStr), testUniqueId); assertFalse(deviceType.isAvailabilityConfigured()); @@ -106,6 +125,26 @@ AHA_TEST(BaseDeviceTypeTest, publish_shared_availability_runtime) { assertSingleMqttMessage(AHATOFSTR(SharedAvailabilityTopic), "offline", true) } +AHA_TEST(BaseDeviceTypeTest, republish_discovery) { + prepareTest + + mock->connectDummy(); + assertTrue(deviceType.republishDiscovery()); + assertSingleMqttMessage( + AHATOFSTR(ConfigTopic), + "{\"name\":\"testName\",\"uniq_id\":\"uniqueId\"}", + true + ) +} + +AHA_TEST(BaseDeviceTypeTest, remove_from_discovery) { + prepareTest + + mock->connectDummy(); + assertTrue(deviceType.removeFromDiscovery()); + assertSingleMqttMessage(AHATOFSTR(ConfigTopic), "", true) +} + void setup() { delay(1000); diff --git a/tests/CameraTest/CameraTest.ino b/tests/CameraTest/CameraTest.ino index b81017b..983c3ce 100644 --- a/tests/CameraTest/CameraTest.ino +++ b/tests/CameraTest/CameraTest.ino @@ -7,6 +7,7 @@ static const char* testDeviceId = "testDevice"; static const char* testUniqueId = "uniqueCamera"; const char ConfigTopic[] PROGMEM = {"homeassistant/camera/testDevice/uniqueCamera/config"}; +const char DeviceConfigTopic[] PROGMEM = {"homeassistant/device/testDevice/config"}; const char DataTopic[] PROGMEM = {"testData/testDevice/uniqueCamera/t"}; AHA_TEST(CameraTest, invalid_unique_id) { @@ -54,6 +55,32 @@ AHA_TEST(CameraTest, extended_unique_id) { ) } +AHA_TEST(CameraTest, device_discovery_payload) { + initMqttTest(testDeviceId) + + mqtt.enableDeviceDiscovery(); + HACamera camera(testUniqueId); + mqtt.loop(); + + assertSingleMqttMessage( + AHATOFSTR(DeviceConfigTopic), + ( + "{" + "\"dev\":{\"ids\":\"testDevice\"}," + "\"o\":{\"name\":\"ArduinoHA\",\"sw\":\"2.1.0\"}," + "\"cmps\":{" + "\"uniqueCamera\":{" + "\"p\":\"camera\"," + "\"uniq_id\":\"uniqueCamera\"," + "\"t\":\"testData/testDevice/uniqueCamera/t\"" + "}" + "}" + "}" + ), + true + ) +} + AHA_TEST(CameraTest, availability) { initMqttTest(testDeviceId) @@ -110,6 +137,46 @@ AHA_TEST(CameraTest, object_id_setter) { ) } +AHA_TEST(CameraTest, default_entity_id_setter) { + initMqttTest(testDeviceId) + + HACamera camera(testUniqueId); + camera.setDefaultEntityId("camera.test_camera"); + + assertEntityConfig( + mock, + camera, + ( + "{" + "\"def_ent_id\":\"camera.test_camera\"," + "\"uniq_id\":\"uniqueCamera\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"t\":\"testData/testDevice/uniqueCamera/t\"" + "}" + ) + ) +} + +AHA_TEST(CameraTest, entity_category_setter) { + initMqttTest(testDeviceId) + + HACamera camera(testUniqueId); + camera.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + camera, + ( + "{" + "\"uniq_id\":\"uniqueCamera\"," + "\"ent_cat\":\"diagnostic\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"t\":\"testData/testDevice/uniqueCamera/t\"" + "}" + ) + ) +} + AHA_TEST(CameraTest, icon_setter) { initMqttTest(testDeviceId) diff --git a/tests/CoverTest/CoverTest.ino b/tests/CoverTest/CoverTest.ino index b7b0348..35eb844 100644 --- a/tests/CoverTest/CoverTest.ino +++ b/tests/CoverTest/CoverTest.ino @@ -32,6 +32,7 @@ static const char* testUniqueId = "uniqueCover"; static CommandCallback lastCommandCallbackCall; const char ConfigTopic[] PROGMEM = {"homeassistant/cover/testDevice/uniqueCover/config"}; +const char DeviceConfigTopic[] PROGMEM = {"homeassistant/device/testDevice/config"}; const char StateTopic[] PROGMEM = {"testData/testDevice/uniqueCover/stat_t"}; const char PositionTopic[] PROGMEM = {"testData/testDevice/uniqueCover/pos_t"}; const char CommandTopic[] PROGMEM = {"testData/testDevice/uniqueCover/cmd_t"}; @@ -92,6 +93,33 @@ AHA_TEST(CoverTest, extended_unique_id) { assertEqual(1, mock->getFlushedMessagesNb()); // only config should be pushed } +AHA_TEST(CoverTest, device_discovery_payload) { + prepareTest + + mqtt.enableDeviceDiscovery(); + HACover cover(testUniqueId); + mqtt.loop(); + + assertSingleMqttMessage( + AHATOFSTR(DeviceConfigTopic), + ( + "{" + "\"dev\":{\"ids\":\"testDevice\"}," + "\"o\":{\"name\":\"ArduinoHA\",\"sw\":\"2.1.0\"}," + "\"cmps\":{" + "\"uniqueCover\":{" + "\"p\":\"cover\"," + "\"uniq_id\":\"uniqueCover\"," + "\"stat_t\":\"testData/testDevice/uniqueCover/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueCover/cmd_t\"" + "}" + "}" + "}" + ), + true + ) +} + AHA_TEST(CoverTest, default_params_with_position) { prepareTest @@ -217,6 +245,27 @@ AHA_TEST(CoverTest, object_id_setter) { ) } +AHA_TEST(CoverTest, default_entity_id_setter) { + prepareTest + + HACover cover(testUniqueId); + cover.setDefaultEntityId("cover.test_cover"); + + assertEntityConfig( + mock, + cover, + ( + "{" + "\"def_ent_id\":\"cover.test_cover\"," + "\"uniq_id\":\"uniqueCover\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueCover/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueCover/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(CoverTest, device_class) { prepareTest @@ -238,6 +287,27 @@ AHA_TEST(CoverTest, device_class) { ) } +AHA_TEST(CoverTest, entity_category_setter) { + prepareTest + + HACover cover(testUniqueId); + cover.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + cover, + ( + "{" + "\"uniq_id\":\"uniqueCover\"," + "\"ent_cat\":\"diagnostic\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueCover/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueCover/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(CoverTest, icon_setter) { prepareTest diff --git a/tests/DeviceTrackerTest/DeviceTrackerTest.ino b/tests/DeviceTrackerTest/DeviceTrackerTest.ino index 457f42f..b4d4ef1 100644 --- a/tests/DeviceTrackerTest/DeviceTrackerTest.ino +++ b/tests/DeviceTrackerTest/DeviceTrackerTest.ino @@ -9,6 +9,7 @@ static const char* testUniqueId = "uniqueTracker"; const char ConfigTopic[] PROGMEM = { "homeassistant/device_tracker/testDevice/uniqueTracker/config" }; +const char DeviceConfigTopic[] PROGMEM = {"homeassistant/device/testDevice/config"}; const char StateTopic[] PROGMEM = {"testData/testDevice/uniqueTracker/stat_t"}; AHA_TEST(DeviceTrackerTest, invalid_unique_id) { @@ -56,6 +57,32 @@ AHA_TEST(DeviceTrackerTest, extended_unique_id) { ) } +AHA_TEST(DeviceTrackerTest, device_discovery_payload) { + initMqttTest(testDeviceId) + + mqtt.enableDeviceDiscovery(); + HADeviceTracker tracker(testUniqueId); + mqtt.loop(); + + assertSingleMqttMessage( + AHATOFSTR(DeviceConfigTopic), + ( + "{" + "\"dev\":{\"ids\":\"testDevice\"}," + "\"o\":{\"name\":\"ArduinoHA\",\"sw\":\"2.1.0\"}," + "\"cmps\":{" + "\"uniqueTracker\":{" + "\"p\":\"device_tracker\"," + "\"uniq_id\":\"uniqueTracker\"," + "\"stat_t\":\"testData/testDevice/uniqueTracker/stat_t\"" + "}" + "}" + "}" + ), + true + ) +} + AHA_TEST(DeviceTrackerTest, source_type_gps) { initMqttTest(testDeviceId) @@ -198,6 +225,46 @@ AHA_TEST(DeviceTrackerTest, object_id_setter) { ) } +AHA_TEST(DeviceTrackerTest, default_entity_id_setter) { + initMqttTest(testDeviceId) + + HADeviceTracker tracker(testUniqueId); + tracker.setDefaultEntityId("device_tracker.test_tracker"); + + assertEntityConfig( + mock, + tracker, + ( + "{" + "\"def_ent_id\":\"device_tracker.test_tracker\"," + "\"uniq_id\":\"uniqueTracker\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueTracker/stat_t\"" + "}" + ) + ) +} + +AHA_TEST(DeviceTrackerTest, entity_category_setter) { + initMqttTest(testDeviceId) + + HADeviceTracker tracker(testUniqueId); + tracker.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + tracker, + ( + "{" + "\"uniq_id\":\"uniqueTracker\"," + "\"ent_cat\":\"diagnostic\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueTracker/stat_t\"" + "}" + ) + ) +} + AHA_TEST(DeviceTrackerTest, icon_setter) { initMqttTest(testDeviceId) diff --git a/tests/DeviceTriggerTest/DeviceTriggerTest.ino b/tests/DeviceTriggerTest/DeviceTriggerTest.ino index adba61a..3762bb9 100644 --- a/tests/DeviceTriggerTest/DeviceTriggerTest.ino +++ b/tests/DeviceTriggerTest/DeviceTriggerTest.ino @@ -10,6 +10,7 @@ static const char* triggerSubtype = "mySubtype"; const char ConfigTopic[] PROGMEM = { "homeassistant/device_automation/testDevice/myType_mySubtype/config" }; +const char DeviceConfigTopic[] PROGMEM = {"homeassistant/device/testDevice/config"}; AHA_TEST(DeviceTriggerTest, invalid_type) { initMqttTest(testDeviceId) @@ -87,6 +88,34 @@ AHA_TEST(DeviceTriggerTest, string_type_string_subtype) { ) } +AHA_TEST(DeviceTriggerTest, device_discovery_payload) { + initMqttTest(testDeviceId) + + mqtt.enableDeviceDiscovery(); + HADeviceTrigger trigger(triggerType, triggerSubtype); + mqtt.loop(); + + assertSingleMqttMessage( + AHATOFSTR(DeviceConfigTopic), + ( + "{" + "\"dev\":{\"ids\":\"testDevice\"}," + "\"o\":{\"name\":\"ArduinoHA\",\"sw\":\"2.1.0\"}," + "\"cmps\":{" + "\"myType_mySubtype\":{" + "\"p\":\"device_automation\"," + "\"atype\":\"trigger\"," + "\"type\":\"myType\"," + "\"stype\":\"mySubtype\"," + "\"t\":\"testData/testDevice/myType_mySubtype/t\"" + "}" + "}" + "}" + ), + true + ) +} + AHA_TEST(DeviceTriggerTest, progmem_type_string_subtype) { initMqttTest(testDeviceId) diff --git a/tests/FanTest/FanTest.ino b/tests/FanTest/FanTest.ino index d9c11b5..f5a8f12 100644 --- a/tests/FanTest/FanTest.ino +++ b/tests/FanTest/FanTest.ino @@ -54,6 +54,7 @@ static StateCallback lastStateCallbackCall; static SpeedCallback lastSpeedCallbackCall; const char ConfigTopic[] PROGMEM = {"homeassistant/fan/testDevice/uniqueFan/config"}; +const char DeviceConfigTopic[] PROGMEM = {"homeassistant/device/testDevice/config"}; const char StateTopic[] PROGMEM = {"testData/testDevice/uniqueFan/stat_t"}; const char SpeedPercentageTopic[] PROGMEM = {"testData/testDevice/uniqueFan/pct_stat_t"}; const char StateCommandTopic[] PROGMEM = {"testData/testDevice/uniqueFan/cmd_t"}; @@ -122,6 +123,36 @@ AHA_TEST(FanTest, extended_unique_id) { assertEqual(2, mock->getFlushedMessagesNb()); // config + default state } +AHA_TEST(FanTest, device_discovery_payload) { + prepareTest + + mqtt.enableDeviceDiscovery(); + HAFan fan(testUniqueId); + mqtt.loop(); + + assertEqual(2, mock->getFlushedMessagesNb()); + assertMqttMessage( + 0, + AHATOFSTR(DeviceConfigTopic), + ( + "{" + "\"dev\":{\"ids\":\"testDevice\"}," + "\"o\":{\"name\":\"ArduinoHA\",\"sw\":\"2.1.0\"}," + "\"cmps\":{" + "\"uniqueFan\":{" + "\"p\":\"fan\"," + "\"uniq_id\":\"uniqueFan\"," + "\"stat_t\":\"testData/testDevice/uniqueFan/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueFan/cmd_t\"" + "}" + "}" + "}" + ), + true + ) + assertMqttMessage(1, AHATOFSTR(StateTopic), "OFF", true) +} + AHA_TEST(FanTest, default_params_with_speed) { prepareTest @@ -261,6 +292,48 @@ AHA_TEST(FanTest, object_id_setter) { ) } +AHA_TEST(FanTest, default_entity_id_setter) { + prepareTest + + HAFan fan(testUniqueId); + fan.setDefaultEntityId("fan.test_fan"); + + assertEntityConfig( + mock, + fan, + ( + "{" + "\"def_ent_id\":\"fan.test_fan\"," + "\"uniq_id\":\"uniqueFan\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueFan/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueFan/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(FanTest, entity_category_setter) { + prepareTest + + HAFan fan(testUniqueId); + fan.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + fan, + ( + "{" + "\"uniq_id\":\"uniqueFan\"," + "\"ent_cat\":\"diagnostic\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueFan/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueFan/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(FanTest, icon_setter) { prepareTest diff --git a/tests/HVACTest/HVACTest.ino b/tests/HVACTest/HVACTest.ino index b6cb2e7..6f6ac65 100644 --- a/tests/HVACTest/HVACTest.ino +++ b/tests/HVACTest/HVACTest.ino @@ -142,6 +142,7 @@ static ModeCallback lastModeCallbackCall; static TargetTempCallback lastTargetTempCallbackCall; const char ConfigTopic[] PROGMEM = {"homeassistant/climate/testDevice/uniqueHVAC/config"}; +const char DeviceConfigTopic[] PROGMEM = {"homeassistant/device/testDevice/config"}; const char CurrentTemperatureTopic[] PROGMEM = {"testData/testDevice/uniqueHVAC/curr_temp_t"}; const char ActionTopic[] PROGMEM = {"testData/testDevice/uniqueHVAC/act_t"}; const char AuxStateTopic[] PROGMEM = {"testData/testDevice/uniqueHVAC/aux_stat_t"}; @@ -245,6 +246,32 @@ AHA_TEST(HVACTest, extended_unique_id) { assertEqual(1, mock->getFlushedMessagesNb()); // config } +AHA_TEST(HVACTest, device_discovery_payload) { + prepareTest + + mqtt.enableDeviceDiscovery(); + HAHVAC hvac(testUniqueId); + mqtt.loop(); + + assertSingleMqttMessage( + AHATOFSTR(DeviceConfigTopic), + ( + "{" + "\"dev\":{\"ids\":\"testDevice\"}," + "\"o\":{\"name\":\"ArduinoHA\",\"sw\":\"2.1.0\"}," + "\"cmps\":{" + "\"uniqueHVAC\":{" + "\"p\":\"climate\"," + "\"uniq_id\":\"uniqueHVAC\"," + "\"curr_temp_t\":\"testData/testDevice/uniqueHVAC/curr_temp_t\"" + "}" + "}" + "}" + ), + true + ) +} + AHA_TEST(HVACTest, config_with_action) { prepareTest @@ -568,6 +595,46 @@ AHA_TEST(HVACTest, object_id_setter) { ) } +AHA_TEST(HVACTest, default_entity_id_setter) { + prepareTest + + HAHVAC hvac(testUniqueId); + hvac.setDefaultEntityId("climate.test_hvac"); + + assertEntityConfig( + mock, + hvac, + ( + "{" + "\"def_ent_id\":\"climate.test_hvac\"," + "\"uniq_id\":\"uniqueHVAC\"," + "\"curr_temp_t\":\"testData/testDevice/uniqueHVAC/curr_temp_t\"," + "\"dev\":{\"ids\":\"testDevice\"}" + "}" + ) + ) +} + +AHA_TEST(HVACTest, entity_category_setter) { + prepareTest + + HAHVAC hvac(testUniqueId); + hvac.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + hvac, + ( + "{" + "\"uniq_id\":\"uniqueHVAC\"," + "\"ent_cat\":\"diagnostic\"," + "\"curr_temp_t\":\"testData/testDevice/uniqueHVAC/curr_temp_t\"," + "\"dev\":{\"ids\":\"testDevice\"}" + "}" + ) + ) +} + AHA_TEST(HVACTest, icon_setter) { prepareTest diff --git a/tests/LockTest/LockTest.ino b/tests/LockTest/LockTest.ino index 90fa180..d91c3b3 100644 --- a/tests/LockTest/LockTest.ino +++ b/tests/LockTest/LockTest.ino @@ -32,6 +32,7 @@ static const char* testUniqueId = "uniqueLock"; static CommandCallback lastCommandCallbackCall; const char ConfigTopic[] PROGMEM = {"homeassistant/lock/testDevice/uniqueLock/config"}; +const char DeviceConfigTopic[] PROGMEM = {"homeassistant/device/testDevice/config"}; const char CommandTopic[] PROGMEM = {"testData/testDevice/uniqueLock/cmd_t"}; const char StateTopic[] PROGMEM = {"testData/testDevice/uniqueLock/stat_t"}; @@ -91,6 +92,33 @@ AHA_TEST(LockTest, extended_unique_id) { assertEqual(1, mock->getFlushedMessagesNb()); // only config should be pushed } +AHA_TEST(LockTest, device_discovery_payload) { + prepareTest + + mqtt.enableDeviceDiscovery(); + HALock lock(testUniqueId); + mqtt.loop(); + + assertSingleMqttMessage( + AHATOFSTR(DeviceConfigTopic), + ( + "{" + "\"dev\":{\"ids\":\"testDevice\"}," + "\"o\":{\"name\":\"ArduinoHA\",\"sw\":\"2.1.0\"}," + "\"cmps\":{" + "\"uniqueLock\":{" + "\"p\":\"lock\"," + "\"uniq_id\":\"uniqueLock\"," + "\"stat_t\":\"testData/testDevice/uniqueLock/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueLock/cmd_t\"" + "}" + "}" + "}" + ), + true + ) +} + AHA_TEST(LockTest, command_subscription) { prepareTest @@ -181,6 +209,48 @@ AHA_TEST(LockTest, object_id_setter) { ) } +AHA_TEST(LockTest, default_entity_id_setter) { + prepareTest + + HALock lock(testUniqueId); + lock.setDefaultEntityId("lock.test_lock"); + + assertEntityConfig( + mock, + lock, + ( + "{" + "\"def_ent_id\":\"lock.test_lock\"," + "\"uniq_id\":\"uniqueLock\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueLock/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueLock/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(LockTest, entity_category_setter) { + prepareTest + + HALock lock(testUniqueId); + lock.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + lock, + ( + "{" + "\"uniq_id\":\"uniqueLock\"," + "\"ent_cat\":\"diagnostic\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueLock/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueLock/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(LockTest, icon_setter) { prepareTest diff --git a/tests/NumberTest/NumberTest.ino b/tests/NumberTest/NumberTest.ino index 1c9742f..0945cc5 100644 --- a/tests/NumberTest/NumberTest.ino +++ b/tests/NumberTest/NumberTest.ino @@ -32,6 +32,7 @@ static const char* testUniqueId = "uniqueNumber"; static CommandCallback lastCommandCallbackCall; const char ConfigTopic[] PROGMEM = {"homeassistant/number/testDevice/uniqueNumber/config"}; +const char DeviceConfigTopic[] PROGMEM = {"homeassistant/device/testDevice/config"}; const char CommandTopic[] PROGMEM = {"testData/testDevice/uniqueNumber/cmd_t"}; const char StateTopic[] PROGMEM = {"testData/testDevice/uniqueNumber/stat_t"}; @@ -356,6 +357,27 @@ AHA_TEST(NumberTest, object_id_setter) { ) } +AHA_TEST(NumberTest, default_entity_id_setter) { + prepareTest + + HANumber number(testUniqueId); + number.setDefaultEntityId("number.test_number"); + + assertEntityConfig( + mock, + number, + ( + "{" + "\"def_ent_id\":\"number.test_number\"," + "\"uniq_id\":\"uniqueNumber\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueNumber/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueNumber/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(NumberTest, device_class) { prepareTest @@ -524,6 +546,26 @@ AHA_TEST(NumberTest, unit_of_measurement_setter) { ) } +AHA_TEST(NumberTest, empty_unit_of_measurement_is_ignored) { + initMqttTest(testDeviceId) + + HANumber number(testUniqueId); + number.setUnitOfMeasurement(""); + + assertEntityConfig( + mock, + number, + ( + "{" + "\"uniq_id\":\"uniqueNumber\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueNumber/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueNumber/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(NumberTest, min_setter_p0) { initMqttTest(testDeviceId) @@ -808,6 +850,29 @@ AHA_TEST(NumberTest, update_min_max_step_before_connect) { ) } +AHA_TEST(NumberTest, min_can_equal_max) { + prepareTest + + HANumber number(testUniqueId); + number.setMin(5); + number.setMax(5); + + assertEntityConfig( + mock, + number, + ( + "{" + "\"uniq_id\":\"uniqueNumber\"," + "\"min\":5," + "\"max\":5," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueNumber/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueNumber/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(NumberTest, update_min_max_step_republishes_config) { prepareTest @@ -836,6 +901,45 @@ AHA_TEST(NumberTest, update_min_max_step_republishes_config) { ) } +AHA_TEST(NumberTest, update_min_max_step_republishes_device_discovery_when_enabled) { + prepareTest + + mqtt.enableDeviceDiscovery(); + HANumber number(testUniqueId); + mqtt.loop(); + assertEqual(2, mock->getFlushedMessagesNb()); + + number.updateMinMaxStep(1, 99, 5); + + assertEqual(4, mock->getFlushedMessagesNb()); + MqttMessage* clearedConfig = mock->getFlushedMessages()[2]; + assertEqual(AHATOFSTR(ConfigTopic), clearedConfig->topic); + assertEqual((size_t)1, clearedConfig->bufferSize); + assertTrue(clearedConfig->retained); + assertMqttMessage( + 3, + AHATOFSTR(DeviceConfigTopic), + ( + "{" + "\"dev\":{\"ids\":\"testDevice\"}," + "\"o\":{\"name\":\"ArduinoHA\",\"sw\":\"2.1.0\"}," + "\"cmps\":{" + "\"uniqueNumber\":{" + "\"p\":\"number\"," + "\"uniq_id\":\"uniqueNumber\"," + "\"min\":1," + "\"max\":99," + "\"step\":5," + "\"stat_t\":\"testData/testDevice/uniqueNumber/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueNumber/cmd_t\"" + "}" + "}" + "}" + ), + true + ) +} + AHA_TEST(NumberTest, command_none) { prepareTest diff --git a/tests/SceneTest/SceneTest.ino b/tests/SceneTest/SceneTest.ino index be07cdc..bcc76cf 100644 --- a/tests/SceneTest/SceneTest.ino +++ b/tests/SceneTest/SceneTest.ino @@ -29,6 +29,7 @@ static const char* testUniqueId = "uniqueScene"; static ActivateCallback lastActivateCallbackCall; const char ConfigTopic[] PROGMEM = {"homeassistant/scene/testDevice/uniqueScene/config"}; +const char DeviceConfigTopic[] PROGMEM = {"homeassistant/device/testDevice/config"}; const char CommandTopic[] PROGMEM = {"testData/testDevice/uniqueScene/cmd_t"}; const char CommandMessage[] PROGMEM = {"on"}; @@ -83,6 +84,33 @@ AHA_TEST(SceneTest, extended_unique_id) { ) } +AHA_TEST(SceneTest, device_discovery_payload) { + prepareTest + + mqtt.enableDeviceDiscovery(); + HAScene scene(testUniqueId); + mqtt.loop(); + + assertSingleMqttMessage( + AHATOFSTR(DeviceConfigTopic), + ( + "{" + "\"dev\":{\"ids\":\"testDevice\"}," + "\"o\":{\"name\":\"ArduinoHA\",\"sw\":\"2.1.0\"}," + "\"cmps\":{" + "\"uniqueScene\":{" + "\"p\":\"scene\"," + "\"uniq_id\":\"uniqueScene\"," + "\"pl_on\":\"ON\"," + "\"cmd_t\":\"testData/testDevice/uniqueScene/cmd_t\"" + "}" + "}" + "}" + ), + true + ) +} + AHA_TEST(SceneTest, command_subscription) { prepareTest @@ -149,6 +177,46 @@ AHA_TEST(SceneTest, object_id_setter) { ) } +AHA_TEST(SceneTest, default_entity_id_setter) { + prepareTest + + HAScene scene(testUniqueId); + scene.setDefaultEntityId("scene.test_scene"); + + assertEntityConfig( + mock, + scene, + ( + "{" + "\"def_ent_id\":\"scene.test_scene\"," + "\"uniq_id\":\"uniqueScene\"," + "\"pl_on\":\"ON\"," + "\"cmd_t\":\"testData/testDevice/uniqueScene/cmd_t\"" + "}" + ) + ) +} + +AHA_TEST(SceneTest, entity_category_setter) { + prepareTest + + HAScene scene(testUniqueId); + scene.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + scene, + ( + "{" + "\"uniq_id\":\"uniqueScene\"," + "\"ent_cat\":\"diagnostic\"," + "\"pl_on\":\"ON\"," + "\"cmd_t\":\"testData/testDevice/uniqueScene/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(SceneTest, icon_setter) { prepareTest diff --git a/tests/SensorTest/SensorTest.ino b/tests/SensorTest/SensorTest.ino index 910187e..0034749 100644 --- a/tests/SensorTest/SensorTest.ino +++ b/tests/SensorTest/SensorTest.ino @@ -110,6 +110,26 @@ AHA_TEST(SensorTest, object_id_setter) { ) } +AHA_TEST(SensorTest, default_entity_id_setter) { + initMqttTest(testDeviceId) + + HASensor sensor(testUniqueId); + sensor.setDefaultEntityId("sensor.test_sensor"); + + assertEntityConfig( + mock, + sensor, + ( + "{" + "\"def_ent_id\":\"sensor.test_sensor\"," + "\"uniq_id\":\"uniqueSensor\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueSensor/stat_t\"" + "}" + ) + ) +} + AHA_TEST(SensorTest, device_class_setter) { initMqttTest(testDeviceId) @@ -230,6 +250,25 @@ AHA_TEST(SensorTest, unit_of_measurement_setter) { ) } +AHA_TEST(SensorTest, empty_unit_of_measurement_is_ignored) { + initMqttTest(testDeviceId) + + HASensor sensor(testUniqueId); + sensor.setUnitOfMeasurement(""); + + assertEntityConfig( + mock, + sensor, + ( + "{" + "\"uniq_id\":\"uniqueSensor\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueSensor/stat_t\"" + "}" + ) + ) +} + AHA_TEST(SensorTest, expire_after_setter) { initMqttTest(testDeviceId) diff --git a/tests/SwitchTest/SwitchTest.ino b/tests/SwitchTest/SwitchTest.ino index 7d6b4d6..21e0970 100644 --- a/tests/SwitchTest/SwitchTest.ino +++ b/tests/SwitchTest/SwitchTest.ino @@ -32,6 +32,7 @@ static const char* testUniqueId = "uniqueSwitch"; static CommandCallback lastCommandCallbackCall; const char ConfigTopic[] PROGMEM = {"homeassistant/switch/testDevice/uniqueSwitch/config"}; +const char DeviceConfigTopic[] PROGMEM = {"homeassistant/device/testDevice/config"}; const char StateTopic[] PROGMEM = {"testData/testDevice/uniqueSwitch/stat_t"}; const char CommandTopic[] PROGMEM = {"testData/testDevice/uniqueSwitch/cmd_t"}; @@ -91,6 +92,36 @@ AHA_TEST(SwitchTest, extended_unique_id) { assertEqual(2, mock->getFlushedMessagesNb()); } +AHA_TEST(SwitchTest, device_discovery_payload) { + prepareTest + + mqtt.enableDeviceDiscovery(); + HASwitch testSwitch(testUniqueId); + mqtt.loop(); + + assertEqual(2, mock->getFlushedMessagesNb()); + assertMqttMessage( + 0, + AHATOFSTR(DeviceConfigTopic), + ( + "{" + "\"dev\":{\"ids\":\"testDevice\"}," + "\"o\":{\"name\":\"ArduinoHA\",\"sw\":\"2.1.0\"}," + "\"cmps\":{" + "\"uniqueSwitch\":{" + "\"p\":\"switch\"," + "\"uniq_id\":\"uniqueSwitch\"," + "\"stat_t\":\"testData/testDevice/uniqueSwitch/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueSwitch/cmd_t\"" + "}" + "}" + "}" + ), + true + ) + assertMqttMessage(1, AHATOFSTR(StateTopic), "OFF", true) +} + AHA_TEST(SwitchTest, command_subscription) { prepareTest @@ -181,6 +212,27 @@ AHA_TEST(SwitchTest, object_id_setter) { ) } +AHA_TEST(SwitchTest, default_entity_id_setter) { + prepareTest + + HASwitch testSwitch(testUniqueId); + testSwitch.setDefaultEntityId("switch.test_switch"); + + assertEntityConfig( + mock, + testSwitch, + ( + "{" + "\"def_ent_id\":\"switch.test_switch\"," + "\"uniq_id\":\"uniqueSwitch\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueSwitch/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueSwitch/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(SwitchTest, device_class) { prepareTest @@ -202,6 +254,27 @@ AHA_TEST(SwitchTest, device_class) { ) } +AHA_TEST(SwitchTest, entity_category_setter) { + prepareTest + + HASwitch testSwitch(testUniqueId); + testSwitch.setEntityCategory("diagnostic"); + + assertEntityConfig( + mock, + testSwitch, + ( + "{" + "\"uniq_id\":\"uniqueSwitch\"," + "\"ent_cat\":\"diagnostic\"," + "\"dev\":{\"ids\":\"testDevice\"}," + "\"stat_t\":\"testData/testDevice/uniqueSwitch/stat_t\"," + "\"cmd_t\":\"testData/testDevice/uniqueSwitch/cmd_t\"" + "}" + ) + ) +} + AHA_TEST(SwitchTest, icon_setter) { prepareTest diff --git a/tests/TagScannerTest/TagScannerTest.ino b/tests/TagScannerTest/TagScannerTest.ino index ffae126..68756b0 100644 --- a/tests/TagScannerTest/TagScannerTest.ino +++ b/tests/TagScannerTest/TagScannerTest.ino @@ -7,6 +7,7 @@ static const char* testDeviceId = "testDevice"; static const char* testUniqueId = "uniqueScanner"; const char ConfigTopic[] PROGMEM = {"homeassistant/tag/testDevice/uniqueScanner/config"}; +const char DeviceConfigTopic[] PROGMEM = {"homeassistant/device/testDevice/config"}; AHA_TEST(TagScannerTest, invalid_unique_id) { initMqttTest(testDeviceId) @@ -34,6 +35,31 @@ AHA_TEST(TagScannerTest, default_params) { ) } +AHA_TEST(TagScannerTest, device_discovery_payload) { + initMqttTest(testDeviceId) + + mqtt.enableDeviceDiscovery(); + HATagScanner scanner(testUniqueId); + mqtt.loop(); + + assertSingleMqttMessage( + AHATOFSTR(DeviceConfigTopic), + ( + "{" + "\"dev\":{\"ids\":\"testDevice\"}," + "\"o\":{\"name\":\"ArduinoHA\",\"sw\":\"2.1.0\"}," + "\"cmps\":{" + "\"uniqueScanner\":{" + "\"p\":\"tag\"," + "\"t\":\"testData/testDevice/uniqueScanner/t\"" + "}" + "}" + "}" + ), + true + ) +} + AHA_TEST(TagScannerTest, nullptr_tag_scanned) { initMqttTest(testDeviceId)