From 051d58a8b867b196475b1fa978a18894c72131ab Mon Sep 17 00:00:00 2001 From: Dawid Chyrzynski Date: Sat, 5 Dec 2020 16:16:44 +0100 Subject: [PATCH] add support for sensor --- README.md | 28 ++- src/ArduinoHA.h | 3 + src/HAUtils.cpp | 31 ++- src/HAUtils.h | 36 ++++ src/device-types/HASensor.cpp | 342 ++++++++++++++++++++++++++++++++++ src/device-types/HASensor.h | 80 ++++++++ 6 files changed, 510 insertions(+), 10 deletions(-) create mode 100644 src/device-types/HASensor.cpp create mode 100644 src/device-types/HASensor.h diff --git a/README.md b/README.md index 7ed7a74..3180488 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,26 @@ ## Examples -## Compatible boards +## Tested boards -## Compatible Arduino Shields +* Arduino Uno +* Arduino Mega +* Controllino Maxi (standard/pure/automation/power) +* Controllino Mega (standard/pure) -## Supported types +## Tested Arduino Shields -- Binary sensors -- Device triggers -- Switches -- Sensors -- Tag scanners +* Arduino Ethernet Shield (W5100) -If you need support for non-listed device's type please open a new issue in the Github repository. +## Supported HA types + +* Binary sensors +* Device triggers +* Switches +* Sensors +* Tag scanners + +## Unsupported features + +The library doesn't support all features of the MQTT integration with Home Assistant. +If you need support for a new feature please open a new issue in the repository. diff --git a/src/ArduinoHA.h b/src/ArduinoHA.h index 3388355..6bbe139 100644 --- a/src/ArduinoHA.h +++ b/src/ArduinoHA.h @@ -1,6 +1,9 @@ #include "HADevice.h" #include "HAMqtt.h" +#include "HAUtils.h" #include "device-types/HABinarySensor.h" +#include "device-types/HASensor.h" +#include "device-types/HASensor.cpp" #include "device-types/HASwitch.h" #include "device-types/HATagScanner.h" #include "device-types/HATriggers.h" diff --git a/src/HAUtils.cpp b/src/HAUtils.cpp index 5911f3f..0f5a51e 100644 --- a/src/HAUtils.cpp +++ b/src/HAUtils.cpp @@ -40,8 +40,37 @@ char* HAUtils::byteArrayToStr( const uint16_t& length ) { - char* dst = malloc((length * 2) + 1); // include null terminator + char* dst = (char*)malloc((length * 2) + 1); // include null terminator byteArrayToStr(dst, src, length); return dst; } + +uint8_t HAUtils::getValueTypeLength(const ValueType& type) +{ + switch(type) { + case ValueTypeUint8: + return sizeof(uint8_t); + + case ValueTypeUint16: + return sizeof(uint16_t); + + case ValueTypeUint32: + return sizeof(uint32_t); + + case ValueTypeInt8: + return sizeof(int8_t); + + case ValueTypeInt16: + return sizeof(int16_t); + + case ValueTypeInt32: + return sizeof(int32_t); + + case ValueTypeDouble: + return sizeof(double); + + case ValueTypeFloat: + return sizeof(float); + } +} diff --git a/src/HAUtils.h b/src/HAUtils.h index 2031995..91bb171 100644 --- a/src/HAUtils.h +++ b/src/HAUtils.h @@ -1,9 +1,23 @@ #ifndef HAUTILS_H #define HAUTILS_H +#include + class HAUtils { public: + enum ValueType { + ValueTypeUnknown = -1, + ValueTypeUint8, + ValueTypeUint16, + ValueTypeUint32, + ValueTypeInt8, + ValueTypeInt16, + ValueTypeInt32, + ValueTypeDouble, + ValueTypeFloat + }; + static bool endsWith( const char* str, const char* suffi @@ -17,6 +31,28 @@ public: const byte* src, const uint16_t& length ); + + template + static bool compareType(A a, B b) { return false; } + + template + static bool compareType(A a, A b) { return true; } + + template + static ValueType determineValueType() { + if (compareType(0, (uint8_t)0)) return ValueTypeUint8; + else if (compareType(0, (uint16_t)0)) return ValueTypeUint16; + else if (compareType(0, (uint32_t)0)) return ValueTypeUint32; + else if (compareType(0, (int8_t)0)) return ValueTypeInt8; + else if (compareType(0, (int16_t)0)) return ValueTypeInt16; + else if (compareType(0, (int32_t)0)) return ValueTypeInt32; + else if (compareType(0, (double)0)) return ValueTypeDouble; + else if (compareType(0, (float)0)) return ValueTypeFloat; + + return ValueTypeUnknown; + } + + static uint8_t getValueTypeLength(const ValueType& type); }; #endif diff --git a/src/device-types/HASensor.cpp b/src/device-types/HASensor.cpp new file mode 100644 index 0000000..099bff0 --- /dev/null +++ b/src/device-types/HASensor.cpp @@ -0,0 +1,342 @@ +#ifndef NO_HA_SENSOR + +#include "HASensor.h" +#include "../ArduinoHADefines.h" +#include "../HAMqtt.h" +#include "../HADevice.h" +#include "../HAUtils.h" + +// todo: move all variables to progmem +static const char* HAComponentName = "sensor"; +static const char* ValueTopic = "value"; + +template +HASensor::HASensor( + const char* name, + T initialValue, + HAMqtt& mqtt +) : + BaseDeviceType(mqtt), + _name(name), + _class(nullptr), + _valueType(HAUtils::determineValueType()), + _currentValue(initialValue) +{ + +} + +template +HASensor::HASensor( + const char* name, + const char* deviceClass, + T initialValue, + HAMqtt& mqtt +) : + BaseDeviceType(mqtt), + _name(name), + _class(deviceClass), + _valueType(HAUtils::determineValueType()), + _currentValue(initialValue) +{ + +} + +template +HASensor::~HASensor() +{ + +} + +template +void HASensor::onMqttConnected() +{ + if (strlen(_name) == 0 || _valueType == HAUtils::ValueTypeUnknown) { + return; + } + + publishConfig(); + publishValue(_currentValue); +} + +template +bool HASensor::setValue(T value) +{ + if (strlen(_name) == 0 || + _valueType == HAUtils::ValueTypeUnknown) { + return false; + } + + if (publishValue(value)) { + _currentValue = value; + return true; + } + + return false; +} + +template +void HASensor::publishConfig() +{ + if (_valueType == HAUtils::ValueTypeUnknown) { + return; + } + + const HADevice* device = mqtt()->getDevice(); + if (device == nullptr) { + return; + } + + const uint16_t& deviceLength = device->calculateSerializedLength(); + if (deviceLength == 0) { + return; + } + + char serializedDevice[deviceLength]; + if (device->serialize(serializedDevice) == 0) { + return; + } + + const uint16_t& topicLength = calculateTopicLength(HAComponentName, _name, ConfigTopic); + const uint16_t& dataLength = calculateSerializedLength(serializedDevice); + + if (topicLength == 0 || dataLength == 0) { + return; + } + + char topic[topicLength]; + generateTopic(topic, HAComponentName, _name, ConfigTopic); + + if (strlen(topic) == 0) { + return; + } + + if (mqtt()->beginPublish(topic, dataLength, true)) { + writeSerializedTrigger(serializedDevice); + mqtt()->endPublish(); + } +} + +template +bool HASensor::publishValue(T value) +{ + if (strlen(_name) == 0 || + _valueType == HAUtils::ValueTypeUnknown) { + return false; + } + + const uint16_t& topicSize = calculateTopicLength( + HAComponentName, + _name, + ValueTopic + ); + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + generateTopic( + topic, + HAComponentName, + _name, + ValueTopic + ); + + if (strlen(topic) == 0) { + return false; + } + + const uint16_t& valueLength = calculateValueLength(); + if (valueLength == 0) { + return false; + } + + char valueStr[valueLength]; + if (!valueToStr(valueStr, value)) { + return false; + } + + return mqtt()->publish(topic, valueStr, true); +} + +template +uint16_t HASensor::calculateSerializedLength( + const char* serializedDevice +) const +{ + if (serializedDevice == nullptr || + _valueType == HAUtils::ValueTypeUnknown) { + return 0; + } + + const uint16_t& valueTopicLength = calculateTopicLength( + HAComponentName, + _name, + ValueTopic, + false + ); + + if (valueTopicLength == 0) { + return 0; + } + + uint16_t size = + 2 + // opening and closing bracket (without null terminator) + valueTopicLength + 11 + // 11 - length of the JSON data for this field + strlen(_name) + 10; // 10 - length of the JSON data for this field + + // unique ID + size += strlen(mqtt()->getDevice()->getUniqueId()); + size += strlen(_name) + 14; // 14 - length of the JSON data for this field + + // device class + if (_class != nullptr) { + size += strlen(_class) + 13; // 13 - length of the JSON data for this field + } + + // device + size += strlen(serializedDevice) + 7; // 7 - length of the JSON data for this field + + return size; +} + +template +bool HASensor::writeSerializedTrigger(const char* serializedDevice) const +{ + if (serializedDevice == nullptr || + _valueType == HAUtils::ValueTypeUnknown) { + return false; + } + + static const char QuotationSign[] PROGMEM = {"\""}; + + // state topic + { + const uint16_t& topicSize = calculateTopicLength( + HAComponentName, + _name, + ValueTopic + ); + if (topicSize == 0) { + return false; + } + + char stateTopic[topicSize]; + generateTopic( + stateTopic, + HAComponentName, + _name, + ValueTopic + ); + + if (strlen(stateTopic) == 0) { + return false; + } + + static const char DataBefore[] PROGMEM = {"{\"stat_t\":\""}; + + mqtt()->writePayload_P(DataBefore); + mqtt()->writePayload(stateTopic, strlen(stateTopic)); + mqtt()->writePayload_P(QuotationSign); + } + + // name + { + static const char DataBefore[] PROGMEM = {",\"name\":\""}; + + mqtt()->writePayload_P(DataBefore); + mqtt()->writePayload(_name, strlen(_name)); + mqtt()->writePayload_P(QuotationSign); + } + + // unique ID + { + static const char Underscore[] PROGMEM = {"_"}; + static const char DataBefore[] PROGMEM = {",\"uniq_id\":\""}; + + const HADevice* device = mqtt()->getDevice(); + uint8_t uniqueIdLength = strlen(_name) + strlen(device->getUniqueId()) + 2; // underscore and null temrinator + char uniqueId[uniqueIdLength]; + strcpy(uniqueId, _name); + strcat_P(uniqueId, Underscore); + strcat(uniqueId, device->getUniqueId()); + + mqtt()->writePayload_P(DataBefore); + mqtt()->writePayload(uniqueId, strlen(uniqueId)); + mqtt()->writePayload_P(QuotationSign); + } + + // device + { + static const char Data[] PROGMEM = {",\"dev\":"}; + + mqtt()->writePayload_P(Data); + mqtt()->writePayload(serializedDevice, strlen(serializedDevice)); + } + + // device class + if (_class != nullptr) { + static const char DataBefore[] PROGMEM = {",\"dev_cla\":\""}; + + mqtt()->writePayload_P(DataBefore); + mqtt()->writePayload(_class, strlen(_class)); + mqtt()->writePayload_P(QuotationSign); + } + + { + static const char Data[] PROGMEM = {"}"}; + mqtt()->writePayload_P(Data); + } + + return true; +} + +template +uint16_t HASensor::calculateValueLength() const +{ + if (_valueType == HAUtils::ValueTypeUnknown) { + return 0; + } + + switch (_valueType) { + case HAUtils::ValueTypeUint8: + case HAUtils::ValueTypeUint16: + case HAUtils::ValueTypeUint32: + case HAUtils::ValueTypeInt8: + case HAUtils::ValueTypeInt16: + case HAUtils::ValueTypeInt32: + return (HAUtils::getValueTypeLength(_valueType) * 3) + 1; // 3 digits per byte + null terminator + + case HAUtils::ValueTypeDouble: + case HAUtils::ValueTypeFloat: + return (HAUtils::getValueTypeLength(_valueType) * 3) + 4; // 3 digits per byte + dot separator + 2 precision digits + null terminator + } + + return 0; +} + +template +bool HASensor::valueToStr(char* dst, T value) const +{ + if (_valueType == HAUtils::ValueTypeUnknown) { + return false; + } +Serial.println(value); + switch (_valueType) { + case HAUtils::ValueTypeUint8: + case HAUtils::ValueTypeUint16: + case HAUtils::ValueTypeUint32: + case HAUtils::ValueTypeInt8: + case HAUtils::ValueTypeInt16: + case HAUtils::ValueTypeInt32: + sprintf(dst, "%d", value); + break; + + case HAUtils::ValueTypeDouble: + dtostrf(value, 0, 2, dst); + break; + } + + return true; +} + +#endif diff --git a/src/device-types/HASensor.h b/src/device-types/HASensor.h new file mode 100644 index 0000000..0b58828 --- /dev/null +++ b/src/device-types/HASensor.h @@ -0,0 +1,80 @@ +#ifndef HASENSOR_H +#ifndef NO_HA_SENSOR +#define HASENSOR_H + +#include "BaseDeviceType.h" +#include "../HAUtils.h" + +template +class HASensor : public BaseDeviceType +{ +public: + /** + * Initializes binary sensor. + * + * @param name Name of the sensor. Recommendes characters: [a-z0-9\-_] + * @param initialValue Initial value of the sensor. + It will be published right after "config" message in order to update HA state. + */ + HASensor( + const char* name, + T initialValue, + HAMqtt& mqtt + ); + + /** + * Initializes binary sensor with the specified class. + * You can find list of available values here: https://www.home-assistant.io/integrations/binary_sensor/#device-class + * + * @param name Name of the sensor. Recommendes characters: [a-z0-9\-_] + * @param deviceClass Name of the class (lower case). + * @param initialValue Initial value of the sensor. + It will be published right after "config" message in order to update HA state. + */ + HASensor( + const char* name, + const char* deviceClass, + T initialValue, + HAMqtt& mqtt + ); + + virtual ~HASensor(); + + /** + * Publishes configuration of the sensor to the MQTT. + */ + virtual void onMqttConnected() override; + + /** + * Changes state of the sensor and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state New state of the sensor. + * @returns Returns true if MQTT message has been published successfully. + */ + bool setValue(T value); + + /** + * Returns last known state of the sensor. + * If setState method wasn't called the initial value will be returned. + */ + inline T getValue() const + { return _currentValue; } + +private: + void publishConfig(); + bool publishValue(T value); + uint16_t calculateSerializedLength(const char* serializedDevice) const; + bool writeSerializedTrigger(const char* serializedDevice) const; + uint16_t calculateValueLength() const; + bool valueToStr(char* dst, T value) const; + + const char* _name; + const char* _class; + HAUtils::ValueType _valueType; + T _currentValue; +}; + +#endif +#endif