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)