From 15562fa74c59fc868d93923e01ba6e10fc419d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Chyrzy=C5=84ski?= Date: Tue, 23 Mar 2021 05:23:56 -0700 Subject: [PATCH] Release 1.2.0 (#21) --- CHANGELOG.md | 23 + README.md | 18 +- .../advanced-availability.ino | 64 + examples/availability/README.md | 2 + examples/availability/availability.ino | 3 +- examples/binary-sensor/binary-sensor.ino | 2 +- examples/fan/fan.ino | 58 + examples/hvac/hvac.ino | 102 ++ examples/led-switch/led-switch.ino | 5 +- examples/mqtt-events/mqtt-events.ino | 32 + .../mqtt-with-credentials.ino | 2 +- .../multi-state-button/multi-state-button.ino | 2 +- examples/nano33iot/nano33iot.ino | 2 +- examples/nodemcu/nodemcu.ino | 2 +- examples/sensor/sensor.ino | 27 +- library.properties | 2 +- src/ArduinoHA.h | 3 +- src/ArduinoHADefines.h | 9 + src/HADevice.cpp | 82 +- src/HADevice.h | 13 + src/HAMqtt.cpp | 160 ++- src/HAMqtt.h | 96 +- src/HAUtils.cpp | 46 +- src/HAUtils.h | 45 +- src/device-types/BaseDeviceType.cpp | 103 +- src/device-types/BaseDeviceType.h | 26 +- src/device-types/DeviceTypeSerializer.cpp | 275 ++++- src/device-types/DeviceTypeSerializer.h | 51 +- src/device-types/HABinarySensor.cpp | 153 +-- src/device-types/HABinarySensor.h | 21 +- src/device-types/HAFan.cpp | 418 +++++++ src/device-types/HAFan.h | 151 +++ src/device-types/HAHVAC.cpp | 1079 +++++++++++++++++ src/device-types/HAHVAC.h | 345 ++++++ src/device-types/HASensor.cpp | 328 ++--- src/device-types/HASensor.h | 80 +- src/device-types/HASwitch.cpp | 290 ++--- src/device-types/HASwitch.h | 48 +- src/device-types/HATagScanner.cpp | 95 +- src/device-types/HATagScanner.h | 13 +- src/device-types/HATriggers.cpp | 46 +- src/device-types/HATriggers.h | 20 +- 42 files changed, 3371 insertions(+), 971 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 examples/advanced-availability/advanced-availability.ino create mode 100644 examples/fan/fan.ino create mode 100644 examples/hvac/hvac.ino create mode 100644 examples/mqtt-events/mqtt-events.ino create mode 100644 src/device-types/HAFan.cpp create mode 100644 src/device-types/HAFan.h create mode 100644 src/device-types/HAHVAC.cpp create mode 100644 src/device-types/HAHVAC.h diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..44c818e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +## 1.2.0 + +**Breaking changes:** +* Refactored HASensor implementation. Please take a look at updated example in `examples/sensor/sensor.ino` + +**New features:** +* Added support for HVAC +* Added support for excluding devices types from the compilation using defines (see `src/ArduinoHADefines.h`) +* Added support for setting icon in HASwitch and HASensor +* Added support for setting retain flag in HASwitch +* Added support for text (const char*) payload in HASensor +* Added support for fans (HAFan) +* Added support for connecting to the MQTT broker using hostname +* Added `onConnected()` method in the HAMqtt +* Added `onConnectionFailed()` method in the HAMqtt +* Added support for MQTT LWT (see `examples/advanced-availability/advanced-availability.ino`) + +**Updates:** +* Optimized codebase and logic in all devices types +* Updated all examples +* Fixed compilation warnings in all classes diff --git a/README.md b/README.md index 1fbe72e..e07eed2 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,23 @@ but I successfully use it on ESP8266/ESP8255 boards in my projects. ## Features * MQTT discovery (device is added to the Home Assistant panel automatically) +* MQTT Last Will and Testament * Auto reconnect with MQTT broker ## Examples * [Binary Sensor](examples/binary-sensor/binary-sensor.ino) +* [Fan](examples/fan/fan.ino) * [LED switch](examples/led-switch/led-switch.ino) -* [MQTT with credentials](examples/mqtt-with-credentials/mqtt-with-credentials.ino) * [Multi-state button](examples/multi-state-button/multi-state-button.ino) * [Sensor (temperature, humidity, etc.)](examples/sensor/sensor.ino) +* [HVAC](examples/hvac/hvac.ino) * [NodeMCU Wi-Fi](examples/nodemcu/nodemcu.ino) * [Arduino Nano 33 IoT Wi-Fi (SAMD)](examples/nano33iot/nano33iot.ino) * [Availability feature](examples/availability) +* [Advanced availability (MQTT LWT)](examples/advanced-availability/advanced-availability.ino) +* [MQTT with credentials](examples/mqtt-with-credentials/mqtt-with-credentials.ino) +* [MQTT events](examples/mqtt-events/mqtt-events.ino) ## Tested boards @@ -39,10 +44,21 @@ but I successfully use it on ESP8266/ESP8255 boards in my projects. ## Supported HA types * Binary sensors +* Fans * Device triggers * Switches * Sensors * Tag scanner +* HVACs *(side note: HVACs requires more flash size than other HA types. It's not suitable for Arduino Nano/Uno)* + +## Roadmap + +* FAQ + Home Assistant setup instructions +* Documentation of the library +* Unit tests +* Reduce flash memory usage +* Add support for HA covers +* Add support for HA lights ## Unsupported features diff --git a/examples/advanced-availability/advanced-availability.ino b/examples/advanced-availability/advanced-availability.ino new file mode 100644 index 0000000..d052ffc --- /dev/null +++ b/examples/advanced-availability/advanced-availability.ino @@ -0,0 +1,64 @@ +#include +#include + +#define INPUT_PIN 9 +#define BROKER_ADDR IPAddress(192,168,0,17) + +byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; +unsigned long lastReadAt = millis(); +unsigned long lastAvailabilityToggleAt = millis(); +bool lastInputState = false; + +EthernetClient client; +HADevice device(mac, sizeof(mac)); +HAMqtt mqtt(client, device); + +// "input" may be anything you want to be displayed in HA panel +// "door" is device class (based on the class HA displays different icons in the panel) +// "true" is initial state of the sensor. In this example it's "true" as we use pullup resistor +HABinarySensor sensor("input", "door", true); + +void setup() { + pinMode(INPUT_PIN, INPUT_PULLUP); + lastInputState = digitalRead(INPUT_PIN); + + // you don't need to verify return status + Ethernet.begin(mac); + + lastReadAt = millis(); + lastAvailabilityToggleAt = millis(); + + // set device's details (optional) + device.setName("Arduino"); + device.setSoftwareVersion("1.0.0"); + + // This method enables availability for all device types registered on the device. + // For example, if you have 5 sensors on the same device, you can enable + // shared availability and change availability state of all sensors using + // single method call "device.setAvailability(false|true)" + device.enableSharedAvailability(); + + // Optionally, you can enable MQTT LWT feature. If device will lose connection + // to the broker, all device types related to it will be marked as offline in + // the Home Assistant Panel. + device.enableLastWill(); + + mqtt.begin(BROKER_ADDR); +} + +void loop() { + Ethernet.maintain(); + mqtt.loop(); + + if ((millis() - lastReadAt) > 30) { // read in 30ms interval + // library produces MQTT message if a new state is different than the previous one + sensor.setState(digitalRead(INPUT_PIN)); + lastInputState = sensor.getState(); + lastReadAt = millis(); + } + + if ((millis() - lastAvailabilityToggleAt) > 5000) { + device.setAvailability(!device.isOnline()); + lastAvailabilityToggleAt = millis(); + } +} diff --git a/examples/availability/README.md b/examples/availability/README.md index f7542ea..76f471d 100644 --- a/examples/availability/README.md +++ b/examples/availability/README.md @@ -5,6 +5,8 @@ It's supported only by following device types: * Binary sensor * Sensor * Switch +* HVACs +* Fans ## Initialization and usage diff --git a/examples/availability/availability.ino b/examples/availability/availability.ino index 2bcc061..eab30c1 100644 --- a/examples/availability/availability.ino +++ b/examples/availability/availability.ino @@ -16,7 +16,7 @@ HAMqtt mqtt(client, device); // "input" may be anything you want to be displayed in HA panel // "door" is device class (based on the class HA displays different icons in the panel) // "true" is initial state of the sensor. In this example it's "true" as we use pullup resistor -HABinarySensor sensor("input", "door", true, mqtt); +HABinarySensor sensor("input", "door", true); void setup() { pinMode(INPUT_PIN, INPUT_PULLUP); @@ -26,6 +26,7 @@ void setup() { Ethernet.begin(mac); // turn on "availability" feature + // this method also sets initial availability so you can use "true" or "false" sensor.setAvailability(false); lastReadAt = millis(); diff --git a/examples/binary-sensor/binary-sensor.ino b/examples/binary-sensor/binary-sensor.ino index 967b883..43c884f 100644 --- a/examples/binary-sensor/binary-sensor.ino +++ b/examples/binary-sensor/binary-sensor.ino @@ -15,7 +15,7 @@ HAMqtt mqtt(client, device); // "input" may be anything you want to be displayed in HA panel // "door" is device class (based on the class HA displays different icons in the panel) // "true" is initial state of the sensor. In this example it's "true" as we use pullup resistor -HABinarySensor sensor("input", "door", true, mqtt); +HABinarySensor sensor("input", "door", true); void setup() { pinMode(INPUT_PIN, INPUT_PULLUP); diff --git a/examples/fan/fan.ino b/examples/fan/fan.ino new file mode 100644 index 0000000..332de0f --- /dev/null +++ b/examples/fan/fan.ino @@ -0,0 +1,58 @@ +#include +#include + +#define BROKER_ADDR IPAddress(192,168,0,17) + +byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; + +EthernetClient client; +HADevice device(mac, sizeof(mac)); +HAMqtt mqtt(client, device); + +// HAFan::SpeedsFeature enables support for setting different speeds of fan. +// You can skip this argument if you don't need speed management. +HAFan fan("ventilation", HAFan::SpeedsFeature); + +void onStateChanged(bool state) { + Serial.print("State: "); + Serial.println(state); +} + +void onSpeedChanged(HAFan::Speed speed) { + Serial.print("Speed: "); + if (speed == HAFan::OffSpeed) { + Serial.print("off"); + } else if (speed == HAFan::LowSpeed) { + Serial.print("low"); + } else if (speed == HAFan::MediumSpeed) { + Serial.print("medium"); + } else if (speed == HAFan::HighSpeed) { + Serial.print("high"); + } +} + +void setup() { + // you don't need to verify return status + Ethernet.begin(mac); + + // set device's details (optional) + device.setName("Arduino"); + device.setSoftwareVersion("1.0.0"); + + // configure fan (optional) + // default speeds are: Off | Low | Medium | High + fan.setSpeeds(HAFan::OffSpeed | HAFan::LowSpeed | HAFan::HighSpeed); + fan.setName("Bathroom"); + fan.setRetain(true); + + // handle fan states + fan.onStateChanged(onStateChanged); + fan.onSpeedChanged(onSpeedChanged); + + mqtt.begin(BROKER_ADDR); +} + +void loop() { + Ethernet.maintain(); + mqtt.loop(); +} diff --git a/examples/hvac/hvac.ino b/examples/hvac/hvac.ino new file mode 100644 index 0000000..5bd2d90 --- /dev/null +++ b/examples/hvac/hvac.ino @@ -0,0 +1,102 @@ +#include +#include + +#define BROKER_ADDR IPAddress(192,168,0,17) +#define WIFI_SSID "MyNetwork" +#define WIFI_PASSWORD "MyPassword" + +WiFiClient client; +HADevice device; +HAMqtt mqtt(client, device); + +// see src/device-types/HAHVAC.h header for more details +HAHVAC hvac("my_name", HAHVAC::AuxHeatingFeature | HAHVAC::AwayModeFeature | HAHVAC::HoldFeature); + +unsigned long lastTempPublishAt = 0; +double lastTemp = 0; + +void onAuxHeatingStateChanged(bool state) { + Serial.print("Aux heating: "); + Serial.println(state); +} + +void onAwayStateChanged(bool state) { + Serial.print("Away state: "); + Serial.println(state); +} + +void onHoldStateChanged(bool state) { + Serial.print("Hold state: "); + Serial.println(state); +} + +void onTargetTemperatureChanged(double temp) { + Serial.print("Target temperature: "); + Serial.println(temp); +} + +void onModeChanged(HAHVAC::Mode mode) { + Serial.print("Mode: "); + if (mode == HAHVAC::OffMode) { + Serial.println("off"); + } else if (mode == HAHVAC::AutoMode) { + Serial.println("auto"); + } else if (mode == HAHVAC::CoolMode) { + Serial.println("cool"); + } else if (mode == HAHVAC::HeatMode) { + Serial.println("heat"); + } else if (mode == HAHVAC::DryMode) { + Serial.println("dry"); + } else if (mode == HAHVAC::FanOnlyMode) { + Serial.println("fan only"); + } +} + +void setup() { + Serial.begin(9600); + Serial.println("Starting..."); + + // Unique ID must be set! + byte mac[WL_MAC_ADDR_LENGTH]; + WiFi.macAddress(mac); + device.setUniqueId(mac, sizeof(mac)); + + // connect to wifi + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); // waiting for the connection + } + Serial.println(); + Serial.println("Connected to the network"); + + // set device's details (optional) + device.setName("NodeMCU"); + device.setSoftwareVersion("1.0.0"); + + // assign callbacks (optional) + hvac.onAuxHeatingStateChanged(onAuxHeatingStateChanged); + hvac.onAwayStateChanged(onAwayStateChanged); + hvac.onHoldStateChanged(onHoldStateChanged); + hvac.onTargetTemperatureChanged(onTargetTemperatureChanged); + hvac.onModeChanged(onModeChanged); + + // configure HVAC (optional) + hvac.setName("My HVAC"); + hvac.setMinTemp(10); + hvac.setMaxTemp(30); + hvac.setTempStep(0.5); + hvac.setRetain(true); + + mqtt.begin(BROKER_ADDR); +} + +void loop() { + mqtt.loop(); + + if ((millis() - lastTempPublishAt) > 3000) { + hvac.setCurrentTemperature(lastTemp); + lastTempPublishAt = millis(); + lastTemp += 0.5; + } +} diff --git a/examples/led-switch/led-switch.ino b/examples/led-switch/led-switch.ino index 1636532..c7dbefc 100644 --- a/examples/led-switch/led-switch.ino +++ b/examples/led-switch/led-switch.ino @@ -9,7 +9,7 @@ byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; EthernetClient client; HADevice device(mac, sizeof(mac)); HAMqtt mqtt(client, device); -HASwitch led("led", false, mqtt); // you can use custom name in place of "led" +HASwitch led("led", false); // you can use custom name in place of "led" void onSwitchStateChanged(bool state, HASwitch* s) { @@ -27,6 +27,9 @@ void setup() { device.setName("Arduino"); device.setSoftwareVersion("1.0.0"); + // set icon (optional) + led.setIcon("mdi:lightbulb"); + // handle switch state led.onStateChanged(onSwitchStateChanged); diff --git a/examples/mqtt-events/mqtt-events.ino b/examples/mqtt-events/mqtt-events.ino new file mode 100644 index 0000000..aaba0f0 --- /dev/null +++ b/examples/mqtt-events/mqtt-events.ino @@ -0,0 +1,32 @@ +#include +#include + +#define BROKER_ADDR IPAddress(192,168,0,17) + +byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; + +EthernetClient client; +HADevice device(mac, sizeof(mac)); +HAMqtt mqtt(client, device); + +void onMqttConnected() { + Serial.println("Connected to the broker!"); +} + +void onMqttConnectionFailed() { + Serial.println("Failed to connect to the broker!"); +} + +void setup() { + Serial.begin(9600); + Ethernet.begin(mac); + + mqtt.onConnected(onMqttConnected); + mqtt.onConnectionFailed(onMqttConnectionFailed); + mqtt.begin(BROKER_ADDR); +} + +void loop() { + Ethernet.maintain(); + mqtt.loop(); +} diff --git a/examples/mqtt-with-credentials/mqtt-with-credentials.ino b/examples/mqtt-with-credentials/mqtt-with-credentials.ino index 363f70f..7dfd06a 100644 --- a/examples/mqtt-with-credentials/mqtt-with-credentials.ino +++ b/examples/mqtt-with-credentials/mqtt-with-credentials.ino @@ -11,7 +11,7 @@ byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; EthernetClient client; HADevice device(mac, sizeof(mac)); HAMqtt mqtt(client, device); -HASwitch led("led", false, mqtt); // you can use custom name in place of "led" +HASwitch led("led", false); // you can use custom name in place of "led" void onSwitchStateChanged(bool state, HASwitch* s) { diff --git a/examples/multi-state-button/multi-state-button.ino b/examples/multi-state-button/multi-state-button.ino index 6f119fa..ba15fbf 100644 --- a/examples/multi-state-button/multi-state-button.ino +++ b/examples/multi-state-button/multi-state-button.ino @@ -14,7 +14,7 @@ byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; EthernetClient client; HADevice device(mac, sizeof(mac)); HAMqtt mqtt(client, device); -HATriggers triggers(mqtt); +HATriggers triggers; Button btn(BUTTON_PIN); bool holdingBtn = false; diff --git a/examples/nano33iot/nano33iot.ino b/examples/nano33iot/nano33iot.ino index 0524847..4db9e3c 100644 --- a/examples/nano33iot/nano33iot.ino +++ b/examples/nano33iot/nano33iot.ino @@ -9,7 +9,7 @@ WiFiClient client; HADevice device; HAMqtt mqtt(client, device); -HASwitch led("led", false, mqtt); // you can use custom name in place of "led" +HASwitch led("led", false); // you can use custom name in place of "led" void onSwitchStateChanged(bool state, HASwitch* s) { diff --git a/examples/nodemcu/nodemcu.ino b/examples/nodemcu/nodemcu.ino index 8732886..74cb01b 100644 --- a/examples/nodemcu/nodemcu.ino +++ b/examples/nodemcu/nodemcu.ino @@ -9,7 +9,7 @@ WiFiClient client; HADevice device; HAMqtt mqtt(client, device); -HASwitch led("led", false, mqtt); // you can use custom name in place of "led" +HASwitch led("led", false); // you can use custom name in place of "led" void onSwitchStateChanged(bool state, HASwitch* s) { diff --git a/examples/sensor/sensor.ino b/examples/sensor/sensor.ino index c8a029d..e6f13de 100644 --- a/examples/sensor/sensor.ino +++ b/examples/sensor/sensor.ino @@ -10,21 +10,7 @@ double lastValue = 0; EthernetClient client; HADevice device(mac, sizeof(mac)); HAMqtt mqtt(client, device); - -/** - * Supported data types: - * - uint8_t - * - uint16_t - * - uint32_t - * - int8_t - * - int16_t - * - int32_t - * - double - * - float - */ -// you can use custom name in place of "temp" -// "0" is initial value of the sensor -HASensor temp("temp", 0, mqtt); +HASensor temp("temp"); void setup() { // you don't need to verify return status @@ -34,8 +20,10 @@ void setup() { device.setName("Arduino"); device.setSoftwareVersion("1.0.0"); - // you can set custom units for the sensor (optional) + // configure sensor (optional) temp.setUnitOfMeasurement("°C"); + temp.setDeviceClass("temperature"); + temp.setIcon("mdi:home"); mqtt.begin(BROKER_ADDR); } @@ -48,5 +36,12 @@ void loop() { lastSentAt = millis(); lastValue = lastValue + 0.5; temp.setValue(lastValue); + + // Supported data types: + // uint32_t (uint16_t, uint8_t) + // int32_t (int16_t, int8_t) + // double + // float + // const char* } } diff --git a/library.properties b/library.properties index d9d0eac..85a6fcd 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=home-assistant-integration -version=1.1.1 +version=1.2.0 author=Dawid Chyrzynski maintainer=Dawid Chyrzynski sentence=Home Assistant MQTT integration for Arduino diff --git a/src/ArduinoHA.h b/src/ArduinoHA.h index 7cfe510..8c9bc50 100644 --- a/src/ArduinoHA.h +++ b/src/ArduinoHA.h @@ -5,8 +5,9 @@ #include "HAMqtt.h" #include "HAUtils.h" #include "device-types/HABinarySensor.h" +#include "device-types/HAFan.h" +#include "device-types/HAHVAC.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/ArduinoHADefines.h b/src/ArduinoHADefines.h index ab0b9c9..8f9611b 100644 --- a/src/ArduinoHADefines.h +++ b/src/ArduinoHADefines.h @@ -2,3 +2,12 @@ // Please note that you need to initialize serial interface manually // by calling Serial.begin([baudRate]) before initializing ArduinoHA. // #define ARDUINOHA_DEBUG + +// You can reduce Flash size of the compiled library by commenting unused components below +#define ARDUINOHA_BINARY_SENSOR +#define ARDUINOHA_FAN +#define ARDUINOHA_HVAC +#define ARDUINOHA_SENSOR +#define ARDUINOHA_SWITCH +#define ARDUINOHA_TAG_SCANNER +#define ARDUINOHA_TRIGGERS diff --git a/src/HADevice.cpp b/src/HADevice.cpp index c22d9b2..f7b57f0 100644 --- a/src/HADevice.cpp +++ b/src/HADevice.cpp @@ -1,13 +1,19 @@ #include +#include "ArduinoHADefines.h" #include "HADevice.h" #include "HAUtils.h" +#include "HAMqtt.h" +#include "device-types/DeviceTypeSerializer.h" #define HADEVICE_INIT \ _manufacturer(nullptr), \ _model(nullptr), \ _name(nullptr), \ - _softwareVersion(nullptr) + _softwareVersion(nullptr), \ + _sharedAvailability(false), \ + _availabilityTopic(nullptr), \ + _available(true) // device will be available by default HADevice::HADevice() : _uniqueId(nullptr), @@ -30,6 +36,58 @@ HADevice::HADevice(const byte* uniqueId, const uint16_t& length) : } +void HADevice::setAvailability(bool online) +{ + _available = online; + publishAvailability(); +} + +bool HADevice::enableSharedAvailability() +{ + if (_sharedAvailability) { + return false; + } + +#if defined(ARDUINOHA_DEBUG) + Serial.println(F("Enabling shared availability in the device")); +#endif + + const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( + nullptr, + nullptr, + DeviceTypeSerializer::AvailabilityTopic + ); + if (topicSize == 0) { + return false; + } + + _availabilityTopic = (char*)malloc(topicSize); + _sharedAvailability = true; + + DeviceTypeSerializer::generateTopic( + _availabilityTopic, + nullptr, + nullptr, + DeviceTypeSerializer::AvailabilityTopic + ); + return true; +} + +bool HADevice::enableLastWill() +{ + HAMqtt* mqtt = HAMqtt::instance(); + if (mqtt == nullptr || _availabilityTopic == nullptr) { + return false; + } + + mqtt->setLastWill( + _availabilityTopic, + DeviceTypeSerializer::Offline, + false + ); + return true; +} + bool HADevice::setUniqueId(const byte* uniqueId, const uint16_t& length) { if (_uniqueId != nullptr) { @@ -40,6 +98,28 @@ bool HADevice::setUniqueId(const byte* uniqueId, const uint16_t& length) return true; } +void HADevice::publishAvailability() +{ + if (!_sharedAvailability) { + return; + } + + HAMqtt* mqtt = HAMqtt::instance(); + if (mqtt == nullptr) { + return; + } + + mqtt->publish( + _availabilityTopic, + ( + _available ? + DeviceTypeSerializer::Online : + DeviceTypeSerializer::Offline + ), + true + ); +} + uint16_t HADevice::calculateSerializedLength() const { uint16_t size = diff --git a/src/HADevice.h b/src/HADevice.h index 06e738b..0c5c2b4 100644 --- a/src/HADevice.h +++ b/src/HADevice.h @@ -25,7 +25,17 @@ public: inline void setSoftwareVersion(const char* softwareVersion) { _softwareVersion = softwareVersion; } + inline bool isSharedAvailabilityEnabled() const + { return _sharedAvailability; } + + inline bool isOnline() const + { return _available; } + + void setAvailability(bool online); + bool enableSharedAvailability(); + bool enableLastWill(); bool setUniqueId(const byte* uniqueId, const uint16_t& length); + void publishAvailability(); uint16_t calculateSerializedLength() const; uint16_t serialize(char* dst) const; @@ -35,6 +45,9 @@ private: const char* _model; const char* _name; const char* _softwareVersion; + bool _sharedAvailability; + char* _availabilityTopic; + bool _available; }; #endif diff --git a/src/HAMqtt.cpp b/src/HAMqtt.cpp index f83de01..0496152 100644 --- a/src/HAMqtt.cpp +++ b/src/HAMqtt.cpp @@ -5,43 +5,36 @@ #include "ArduinoHADefines.h" #include "device-types/BaseDeviceType.h" -#define HAMQTT_INIT \ - _netClient(netClient), \ - _device(device), \ - _hasDevice(true), \ - _initialized(false), \ - _discoveryPrefix(DefaultDiscoveryPrefix), \ - _mqtt(new PubSubClient(netClient)), \ - _serverIp(new IPAddress()), \ - _serverPort(0), \ - _username(nullptr), \ - _password(nullptr), \ - _lastConnectionAttemptAt(0), \ - _devicesTypesNb(0), \ - _devicesTypes(nullptr) - static const char* DefaultDiscoveryPrefix = "homeassistant"; -static HAMqtt* instance = nullptr; +HAMqtt* HAMqtt::_instance = nullptr; void onMessageReceived(char* topic, uint8_t* payload, unsigned int length) { - if (instance == nullptr || length > UINT16_MAX) { + if (HAMqtt::instance() == nullptr || length > UINT16_MAX) { return; } - instance->processMessage(topic, payload, static_cast(length)); + HAMqtt::instance()->processMessage(topic, payload, static_cast(length)); } HAMqtt::HAMqtt(Client& netClient, HADevice& device) : - HAMQTT_INIT + _netClient(netClient), + _device(device), + _connectedCallback(nullptr), + _connectionFailedCallback(nullptr), + _initialized(false), + _discoveryPrefix(DefaultDiscoveryPrefix), + _mqtt(new PubSubClient(netClient)), + _username(nullptr), + _password(nullptr), + _lastConnectionAttemptAt(0), + _devicesTypesNb(0), + _devicesTypes(nullptr), + _lastWillTopic(nullptr), + _lastWillMessage(nullptr), + _lastWillRetain(false) { - instance = this; -} - -HAMqtt::HAMqtt(const char* clientId, Client& netClient, HADevice& device) : - HAMQTT_INIT -{ - instance = this; + _instance = this; } bool HAMqtt::begin( @@ -76,13 +69,11 @@ bool HAMqtt::begin( return false; } - *_serverIp = serverIp; - _serverPort = serverPort; _username = username; _password = password; _initialized = true; - _mqtt->setServer(*_serverIp, _serverPort); + _mqtt->setServer(serverIp, serverPort); _mqtt->setCallback(onMessageReceived); return true; @@ -94,16 +85,86 @@ bool HAMqtt::begin( const char* password ) { - return begin(serverIp, 1883, username, password); + return begin(serverIp, HAMQTT_DEFAULT_PORT, username, password); +} + +bool HAMqtt::begin( + const char* hostname, + const uint16_t& serverPort, + const char* username, + const char* password +) +{ +#if defined(ARDUINOHA_DEBUG) + Serial.println(F("Initializing ArduinoHA")); + Serial.print(F("Server address: ")); + Serial.print(hostname); + Serial.print(F(":")); + Serial.print(serverPort); + Serial.println(); +#endif + + if (_device.getUniqueId() == nullptr) { +#if defined(ARDUINOHA_DEBUG) + Serial.println(F("Failed to initialize ArduinoHA. Missing device's unique ID.")); +#endif + + return false; + } + + if (_initialized) { +#if defined(ARDUINOHA_DEBUG) + Serial.println(F("ArduinoHA is already initialized")); +#endif + + return false; + } + + _username = username; + _password = password; + _initialized = true; + + _mqtt->setServer(hostname, serverPort); + _mqtt->setCallback(onMessageReceived); + + return true; +} + +bool HAMqtt::begin( + const char* hostname, + const char* username, + const char* password +) +{ + return begin(hostname, HAMQTT_DEFAULT_PORT, username, password); +} + +bool HAMqtt::disconnect(bool sendLastWill) +{ + if (!_initialized) { + return false; + } + +#if defined(ARDUINOHA_DEBUG) + Serial.println(F("Closing connection with MQTT broker")); +#endif + + if (sendLastWill && + _lastWillTopic != nullptr && + _lastWillMessage != nullptr) { + publish(_lastWillTopic, _lastWillMessage, _lastWillRetain); + } + + _initialized = false; + _lastConnectionAttemptAt = 0; + _mqtt->disconnect(); + + return true; } void HAMqtt::loop() { - if (!_initialized) { - return; - } - - if (!_mqtt->loop()) { + if (_initialized && !_mqtt->loop()) { connectToServer(); } } @@ -222,27 +283,42 @@ void HAMqtt::connectToServer() Serial.println(); #endif - if (_username == nullptr || _password == nullptr) { - _mqtt->connect(_device.getUniqueId()); - } else { - _mqtt->connect(_device.getUniqueId(), _username, _password); - } + _mqtt->connect( + _device.getUniqueId(), + _username, + _password, + _lastWillTopic, + 0, + _lastWillRetain, + _lastWillMessage, + true + ); if (isConnected()) { #if defined(ARDUINOHA_DEBUG) Serial.println(F("Connected to the broker")); #endif - onConnected(); + onConnectedLogic(); } else { #if defined(ARDUINOHA_DEBUG) Serial.println(F("Failed to connect to the broker")); + + if (_connectionFailedCallback) { + _connectionFailedCallback(); + } #endif } } -void HAMqtt::onConnected() +void HAMqtt::onConnectedLogic() { + if (_connectedCallback) { + _connectedCallback(); + } + + _device.publishAvailability(); + for (uint8_t i = 0; i < _devicesTypesNb; i++) { _devicesTypes[i]->onMqttConnected(); } diff --git a/src/HAMqtt.h b/src/HAMqtt.h index 1450b12..588fcf2 100644 --- a/src/HAMqtt.h +++ b/src/HAMqtt.h @@ -4,6 +4,9 @@ #include #include +#define HAMQTT_CALLBACK(name) void (*name)() +#define HAMQTT_DEFAULT_PORT 1883 + class PubSubClient; class HADevice; class BaseDeviceType; @@ -13,8 +16,10 @@ class HAMqtt public: static const uint16_t ReconnectInterval = 5000; // ms + inline static HAMqtt* instance() + { return _instance; } + HAMqtt(Client& netClient, HADevice& device); - HAMqtt(const char* clientId, Client& netClient, HADevice& device); /** * Sets prefix for Home Assistant discovery. @@ -34,7 +39,23 @@ public: * Returns instance of the device assigned to the HAMqtt class. */ inline HADevice const* getDevice() const - { return (_hasDevice ? &_device : nullptr); } + { return &_device; } + + /** + * Given callback will be called each time the connection with broker is acquired. + * + * @param callback + */ + inline void onConnected(HAMQTT_CALLBACK(callback)) + { _connectedCallback = callback; } + + /** + * Given callback will be called each time the library fails to connect to the broker. + * + * @param callback + */ + inline void onConnectionFailed(HAMQTT_CALLBACK(callback)) + { _connectionFailedCallback = callback; } /** * Sets parameters of the connection to the MQTT broker. @@ -48,27 +69,43 @@ public: */ bool begin( const IPAddress& serverIp, - const uint16_t& serverPort = 1883, + const uint16_t& serverPort = HAMQTT_DEFAULT_PORT, const char* username = nullptr, const char* password = nullptr ); - - /** - * Sets parameters of the connection to the MQTT broker on default port. - * The library will try to connect to the broker in first loop cycle. - * Please note that the library automatically reconnects to the broker if connection is lost. - * - * @param serverIp IP address of the MQTT broker. - * @param serverPort Port of the MQTT broker. - * @param username Username for authentication. - * @param password Password for authentication. - */ bool begin( const IPAddress& serverIp, const char* username, const char* password ); + /** + * Connects to the MQTT broker using hostname. + * + * @param hostname Hostname of the MQTT broker. + * @param serverPort Port of the MQTT broker. + * @param username Username for authentication. + * @param password Password for authentication. + */ + bool begin( + const char* hostname, + const uint16_t& serverPort = HAMQTT_DEFAULT_PORT, + const char* username = nullptr, + const char* password = nullptr + ); + bool begin( + const char* hostname, + const char* username, + const char* password + ); + + /** + * Closes connection with the MQTT broker. + * + * @param sendLastWill Set to true if you want to publish device unavailability before closing the connection. + */ + bool disconnect(bool sendLastWill = true); + /** * ArduinoHA's ticker. */ @@ -116,6 +153,25 @@ public: */ bool subscribe(const char* topic); + /** + * Enables last will message that will be produced when device disconnects from the broker. + * If you want to change availability of the device in Home Assistant panel + * please use enableLastWill() method in the HADevice object instead. + * + * @param lastWillTopic + * @param lastWillMessage + * @param lastWillRetain + */ + inline void setLastWill( + const char* lastWillTopic, + const char* lastWillMessage, + bool lastWillRetain + ) { + _lastWillTopic = lastWillTopic; + _lastWillMessage = lastWillMessage; + _lastWillRetain = lastWillRetain; + } + /** * Processes MQTT message received from the broker (subscription). * @@ -126,6 +182,8 @@ public: void processMessage(char* topic, uint8_t* payload, uint16_t length); private: + static HAMqtt* _instance; + /** * Attempts to connect to the MQTT broker. * The method uses properties passed to the "begin" method. @@ -135,21 +193,23 @@ private: /** * This method is called each time the connection with MQTT broker is acquired. */ - void onConnected(); + void onConnectedLogic(); Client& _netClient; HADevice& _device; - bool _hasDevice; + HAMQTT_CALLBACK(_connectedCallback); + HAMQTT_CALLBACK(_connectionFailedCallback); bool _initialized; const char* _discoveryPrefix; PubSubClient* _mqtt; - IPAddress* _serverIp; - uint16_t _serverPort; const char* _username; const char* _password; uint32_t _lastConnectionAttemptAt; uint8_t _devicesTypesNb; BaseDeviceType** _devicesTypes; + const char* _lastWillTopic; + const char* _lastWillMessage; + bool _lastWillRetain; }; #endif diff --git a/src/HAUtils.cpp b/src/HAUtils.cpp index 68d274f..5120048 100644 --- a/src/HAUtils.cpp +++ b/src/HAUtils.cpp @@ -1,3 +1,7 @@ +#ifdef ARDUINO_ARCH_SAMD +#include +#endif + #include #include "HAUtils.h" @@ -46,34 +50,18 @@ char* HAUtils::byteArrayToStr( return dst; } -uint8_t HAUtils::getValueTypeLength(const ValueType& type) +void HAUtils::tempToStr( + char* dst, + const double& temp +) { - 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); - - default: - return 0; - } + memset(dst, 0, sizeof(AHA_SERIALIZED_TEMP_SIZE)); + dtostrf(temp, 0, 2, dst); +} + +double HAUtils::strToTemp( + const char* src +) +{ + return atof(src); } diff --git a/src/HAUtils.h b/src/HAUtils.h index 163ef19..39db0fa 100644 --- a/src/HAUtils.h +++ b/src/HAUtils.h @@ -3,24 +3,14 @@ #include +#define AHA_SERIALIZED_TEMP_SIZE 8 + class HAUtils { public: - enum ValueType { - ValueTypeUnknown = -1, - ValueTypeUint8, - ValueTypeUint16, - ValueTypeUint32, - ValueTypeInt8, - ValueTypeInt16, - ValueTypeInt32, - ValueTypeDouble, - ValueTypeFloat - }; - static bool endsWith( const char* str, - const char* suffi + const char* suffix ); static void byteArrayToStr( char* dst, @@ -31,28 +21,13 @@ 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); + static void tempToStr( + char* dst, + const double& temp + ); + static double strToTemp( + const char* src + ); }; #endif diff --git a/src/device-types/BaseDeviceType.cpp b/src/device-types/BaseDeviceType.cpp index 15aad6c..748b5e5 100644 --- a/src/device-types/BaseDeviceType.cpp +++ b/src/device-types/BaseDeviceType.cpp @@ -1,18 +1,17 @@ #include "BaseDeviceType.h" #include "../HAMqtt.h" #include "../HADevice.h" +#include "../HAUtils.h" BaseDeviceType::BaseDeviceType( - HAMqtt& mqtt, const char* componentName, const char* name ) : - _mqtt(mqtt), _componentName(componentName), _name(name), _availability(AvailabilityDefault) { - _mqtt.addDeviceType(this); + mqtt()->addDeviceType(this); } BaseDeviceType::~BaseDeviceType() @@ -26,17 +25,84 @@ void BaseDeviceType::setAvailability(bool online) publishAvailability(); } +HAMqtt* BaseDeviceType::mqtt() const +{ + return HAMqtt::instance(); +} + +void BaseDeviceType::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t& length +) +{ + (void)topic; + (void)payload; + (void)length; +} + +void BaseDeviceType::publishConfig() +{ + 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 = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + DeviceTypeSerializer::ConfigTopic + ); + const uint16_t& dataLength = calculateSerializedLength(serializedDevice); + + if (topicLength == 0 || dataLength == 0) { + return; + } + + char topic[topicLength]; + DeviceTypeSerializer::generateTopic( + topic, + componentName(), + name(), + DeviceTypeSerializer::ConfigTopic + ); + + if (strlen(topic) == 0) { + return; + } + + if (mqtt()->beginPublish(topic, dataLength, true)) { + writeSerializedData(serializedDevice); + mqtt()->endPublish(); + } +} + void BaseDeviceType::publishAvailability() { + const HADevice* device = HAMqtt::instance()->getDevice(); + if (device == nullptr) { + return; + } + if (_availability == AvailabilityDefault || - !_mqtt.isConnected() || + !mqtt()->isConnected() || strlen(_name) == 0 || - strlen(_componentName) == 0) { + strlen(_componentName) == 0 || + device->isSharedAvailabilityEnabled()) { return; } const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), _componentName, _name, DeviceTypeSerializer::AvailabilityTopic @@ -47,7 +113,6 @@ void BaseDeviceType::publishAvailability() char topic[topicSize]; DeviceTypeSerializer::generateTopic( - mqtt(), topic, _componentName, _name, @@ -68,3 +133,27 @@ void BaseDeviceType::publishAvailability() true ); } + +bool BaseDeviceType::isMyTopic(const char* topic, const char* expectedTopic) +{ + if (topic == nullptr || expectedTopic == nullptr) { + return false; + } + + if (strlen(name()) == 0) { + return false; + } + + static const char Slash[] PROGMEM = {"/"}; + + // name + cmd topic + two slashes + null terminator + uint8_t suffixLength = strlen(name()) + strlen(expectedTopic) + 3; + char suffix[suffixLength]; + + strcpy_P(suffix, Slash); + strcat(suffix, name()); + strcat_P(suffix, Slash); + strcat(suffix, expectedTopic); + + return HAUtils::endsWith(topic, suffix); +} diff --git a/src/device-types/BaseDeviceType.h b/src/device-types/BaseDeviceType.h index 3a72f28..40e3004 100644 --- a/src/device-types/BaseDeviceType.h +++ b/src/device-types/BaseDeviceType.h @@ -3,6 +3,7 @@ #include +#include "../ArduinoHADefines.h" #include "DeviceTypeSerializer.h" class HAMqtt; @@ -11,21 +12,11 @@ class BaseDeviceType { public: BaseDeviceType( - HAMqtt& mqtt, const char* componentName, const char* name ); virtual ~BaseDeviceType(); - inline bool isOnline() const - { return (_availability == AvailabilityOnline); } - - virtual void setAvailability(bool online); - -protected: - inline HAMqtt* mqtt() const - { return &_mqtt; } - inline const char* name() const { return _name; } @@ -35,14 +26,26 @@ protected: inline bool isAvailabilityConfigured() const { return (_availability != AvailabilityDefault); } + inline bool isOnline() const + { return (_availability == AvailabilityOnline); } + + virtual void setAvailability(bool online); + +protected: + HAMqtt* mqtt() const; + virtual void onMqttConnected() = 0; virtual void onMqttMessage( const char* topic, const uint8_t* payload, const uint16_t& length - ) { }; + ); + virtual void publishConfig(); virtual void publishAvailability(); + virtual bool isMyTopic(const char* topic, const char* expectedTopic); + virtual uint16_t calculateSerializedLength(const char* serializedDevice) const = 0; + virtual bool writeSerializedData(const char* serializedDevice) const = 0; const char* const _componentName; const char* const _name; @@ -54,7 +57,6 @@ private: AvailabilityOffline }; - HAMqtt& _mqtt; Availability _availability; friend class HAMqtt; diff --git a/src/device-types/DeviceTypeSerializer.cpp b/src/device-types/DeviceTypeSerializer.cpp index 02e921f..d6ac6d4 100644 --- a/src/device-types/DeviceTypeSerializer.cpp +++ b/src/device-types/DeviceTypeSerializer.cpp @@ -1,6 +1,7 @@ #include #include "DeviceTypeSerializer.h" +#include "BaseDeviceType.h" #include "../HAMqtt.h" #include "../HADevice.h" @@ -19,31 +20,31 @@ const char* DeviceTypeSerializer::StateOn = "ON"; const char* DeviceTypeSerializer::StateOff = "OFF"; uint16_t DeviceTypeSerializer::calculateTopicLength( - const HAMqtt* mqtt, const char* component, const char* objectId, const char* suffix, bool includeNullTerminator ) { - const char* prefix = mqtt->getDiscoveryPrefix(); + const char* prefix = HAMqtt::instance()->getDiscoveryPrefix(); if (prefix == nullptr) { return 0; } uint16_t size = - strlen(prefix) + 1 + // with slash - strlen(component) + 1 + // with slash + strlen(prefix) + 1 + // prefix with slash strlen(suffix); + if (component != nullptr) { + size += strlen(component) + 1; // component with slash + } + + if (HAMqtt::instance()->getDevice() != nullptr) { + size += strlen(HAMqtt::instance()->getDevice()->getUniqueId()) + 1; // device ID with slash + } + if (objectId != nullptr) { size += strlen(objectId) + 1; // with slash - } else { - size += 1; // slash - } - - if (mqtt->getDevice() != nullptr) { - size += strlen(mqtt->getDevice()->getUniqueId()) + 1; // with slash } if (includeNullTerminator) { @@ -54,26 +55,35 @@ uint16_t DeviceTypeSerializer::calculateTopicLength( } uint16_t DeviceTypeSerializer::generateTopic( - const HAMqtt* mqtt, char* output, const char* component, const char* objectId, const char* suffix ) { - const char* prefix = mqtt->getDiscoveryPrefix(); + const char* prefix = HAMqtt::instance()->getDiscoveryPrefix(); + if (prefix == nullptr) { + return 0; + } + strcpy(output, prefix); strcat_P(output, CharSlash); - strcat(output, component); - strcat_P(output, CharSlash); - if (mqtt->getDevice() != nullptr) { - strcat(output, mqtt->getDevice()->getUniqueId()); + if (component != nullptr) { + strcat(output, component); + strcat_P(output, CharSlash); + } + + if (HAMqtt::instance()->getDevice() != nullptr) { + strcat(output, HAMqtt::instance()->getDevice()->getUniqueId()); + strcat_P(output, CharSlash); + } + + if (objectId != nullptr) { + strcat(output, objectId); strcat_P(output, CharSlash); } - strcat(output, objectId); - strcat_P(output, CharSlash); strcat(output, suffix); return strlen(output) + 1; // size with null terminator } @@ -94,10 +104,10 @@ uint16_t DeviceTypeSerializer::calculateNameFieldSize(const char* name) } uint16_t DeviceTypeSerializer::calculateUniqueIdFieldSize( - const HADevice* device, const char* name ) { + HADevice const* device = HAMqtt::instance()->getDevice(); if (device == nullptr || name == nullptr) { return 0; } @@ -111,19 +121,22 @@ uint16_t DeviceTypeSerializer::calculateUniqueIdFieldSize( } uint16_t DeviceTypeSerializer::calculateAvailabilityFieldSize( - const HAMqtt* mqtt, - const char* componentName, - const char* name + const BaseDeviceType* const dt ) { - if (mqtt == nullptr || componentName == nullptr || name == nullptr) { + const HADevice* device = HAMqtt::instance()->getDevice(); + if (device == nullptr) { + return 0; + } + + const bool& sharedAvailability = device->isSharedAvailabilityEnabled(); + if (!sharedAvailability && !dt->isAvailabilityConfigured()) { return 0; } const uint16_t& availabilityTopicLength = calculateTopicLength( - mqtt, - componentName, - name, + (sharedAvailability ? nullptr : dt->componentName()), + (sharedAvailability ? nullptr : dt->name()), AvailabilityTopic, false ); @@ -136,6 +149,16 @@ uint16_t DeviceTypeSerializer::calculateAvailabilityFieldSize( return availabilityTopicLength + 12; // 12 - length of the JSON decorators for this field } +uint16_t DeviceTypeSerializer::calculateRetainFieldSize(bool retain) +{ + if (!retain) { + return 0; + } + + // Field format: ,"ret":true + return 11; +} + uint16_t DeviceTypeSerializer::calculateDeviceFieldSize( const char* serializedDevice ) @@ -148,85 +171,87 @@ uint16_t DeviceTypeSerializer::calculateDeviceFieldSize( return strlen(serializedDevice) + 7; // 7 - length of the JSON decorators for this field } -void DeviceTypeSerializer::mqttWriteBeginningJson(HAMqtt* mqtt) +void DeviceTypeSerializer::mqttWriteBeginningJson() { - if (mqtt == nullptr) { - return; - } - static const char Data[] PROGMEM = {"{"}; - mqtt->writePayload_P(Data); + HAMqtt::instance()->writePayload_P(Data); } -void DeviceTypeSerializer::mqttWriteEndJson(HAMqtt* mqtt) +void DeviceTypeSerializer::mqttWriteEndJson() { - if (mqtt == nullptr) { - return; - } - static const char Data[] PROGMEM = {"}"}; - mqtt->writePayload_P(Data); + HAMqtt::instance()->writePayload_P(Data); } void DeviceTypeSerializer::mqttWriteConstCharField( - HAMqtt* mqtt, const char* prefix, - const char* value + const char* value, + bool quoteSuffix ) { - if (mqtt == nullptr || prefix == nullptr || value == nullptr) { + if (prefix == nullptr || value == nullptr) { return; } - mqtt->writePayload_P(prefix); - mqtt->writePayload(value, strlen(value)); - mqtt->writePayload_P(CharQuotation); + HAMqtt::instance()->writePayload_P(prefix); + HAMqtt::instance()->writePayload(value, strlen(value)); + + if (quoteSuffix) { + HAMqtt::instance()->writePayload_P(CharQuotation); + } } -void DeviceTypeSerializer::mqttWriteNameField(HAMqtt* mqtt, const char* name) +void DeviceTypeSerializer::mqttWriteNameField(const char* name) { - if (mqtt == nullptr || name == nullptr) { + if (name == nullptr) { return; } static const char Prefix[] PROGMEM = {",\"name\":\""}; - mqttWriteConstCharField(mqtt, Prefix, name); + mqttWriteConstCharField(Prefix, name); } void DeviceTypeSerializer::mqttWriteUniqueIdField( - HAMqtt* mqtt, const char* name ) { - if (mqtt == nullptr || name == nullptr) { + if (name == nullptr) { + return; + } + + HADevice const* device = HAMqtt::instance()->getDevice(); + if (device == nullptr) { return; } static const char Prefix[] PROGMEM = {",\"uniq_id\":\""}; - uint8_t uniqueIdLength = strlen(name) + strlen(mqtt->getDevice()->getUniqueId()) + 2; // underscore and null temrinator + uint8_t uniqueIdLength = strlen(name) + strlen(device->getUniqueId()) + 2; // underscore and null temrinator char uniqueId[uniqueIdLength]; strcpy(uniqueId, name); strcat_P(uniqueId, CharUnderscore); - strcat(uniqueId, mqtt->getDevice()->getUniqueId()); + strcat(uniqueId, device->getUniqueId()); - mqttWriteConstCharField(mqtt, Prefix, uniqueId); + mqttWriteConstCharField(Prefix, uniqueId); } void DeviceTypeSerializer::mqttWriteAvailabilityField( - HAMqtt* mqtt, - const char* componentName, - const char* name + const BaseDeviceType* const dt ) { - if (mqtt == nullptr || componentName == nullptr || name == nullptr) { + const HADevice* device = HAMqtt::instance()->getDevice(); + if (device == nullptr) { + return; + } + + const bool& sharedAvailability = device->isSharedAvailabilityEnabled(); + if (!sharedAvailability && !dt->isAvailabilityConfigured()) { return; } const uint16_t& topicSize = calculateTopicLength( - mqtt, - componentName, - name, + (sharedAvailability ? nullptr : dt->componentName()), + (sharedAvailability ? nullptr : dt->name()), AvailabilityTopic ); if (topicSize == 0) { @@ -235,10 +260,9 @@ void DeviceTypeSerializer::mqttWriteAvailabilityField( char availabilityTopic[topicSize]; generateTopic( - mqtt, availabilityTopic, - componentName, - name, + (sharedAvailability ? nullptr : dt->componentName()), + (sharedAvailability ? nullptr : dt->name()), AvailabilityTopic ); @@ -247,20 +271,137 @@ void DeviceTypeSerializer::mqttWriteAvailabilityField( } static const char Prefix[] PROGMEM = {",\"avty_t\":\""}; - mqttWriteConstCharField(mqtt, Prefix, availabilityTopic); + mqttWriteConstCharField(Prefix, availabilityTopic); +} + +void DeviceTypeSerializer::mqttWriteRetainField( + bool retain +) +{ + if (!retain) { + return; + } + + static const char Prefix[] PROGMEM = {",\"ret\":"}; + mqttWriteConstCharField( + Prefix, + "true", + false + ); } void DeviceTypeSerializer::mqttWriteDeviceField( - HAMqtt* mqtt, const char* serializedDevice ) { - if (mqtt == nullptr || serializedDevice == nullptr) { + if (serializedDevice == nullptr) { return; } static const char Data[] PROGMEM = {",\"dev\":"}; - mqtt->writePayload_P(Data); - mqtt->writePayload(serializedDevice, strlen(serializedDevice)); + HAMqtt::instance()->writePayload_P(Data); + HAMqtt::instance()->writePayload(serializedDevice, strlen(serializedDevice)); +} + +bool DeviceTypeSerializer::mqttWriteTopicField( + const BaseDeviceType* const dt, + const char* jsonPrefix, + const char* topicSuffix +) +{ + if (jsonPrefix == nullptr || topicSuffix == nullptr) { + return false; + } + + const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( + dt->componentName(), + dt->name() , + topicSuffix + ); + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + DeviceTypeSerializer::generateTopic( + topic, + dt->componentName(), + dt->name(), + topicSuffix + ); + + if (strlen(topic) == 0) { + return false; + } + + DeviceTypeSerializer::mqttWriteConstCharField(jsonPrefix, topic); + return true; +} + +bool DeviceTypeSerializer::mqttPublishMessage( + const BaseDeviceType* const dt, + const char* topicSuffix, + const char* data +) +{ + if (topicSuffix == nullptr || data == nullptr) { + return false; + } + + const uint16_t& topicSize = calculateTopicLength( + dt->componentName(), + dt->name(), + topicSuffix + ); + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + generateTopic( + topic, + dt->componentName(), + dt->name(), + topicSuffix + ); + + if (strlen(topic) == 0) { + return false; + } + + return HAMqtt::instance()->publish( + topic, + data, + true + ); +} + +bool DeviceTypeSerializer::mqttSubscribeTopic( + const BaseDeviceType* const dt, + const char* topicSuffix +) +{ + const uint16_t& topicSize = calculateTopicLength( + dt->componentName(), + dt->name(), + topicSuffix + ); + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + generateTopic( + topic, + dt->componentName(), + dt->name(), + topicSuffix + ); + + if (strlen(topic) == 0) { + return false; + } + + return HAMqtt::instance()->subscribe(topic); } diff --git a/src/device-types/DeviceTypeSerializer.h b/src/device-types/DeviceTypeSerializer.h index a82ebad..f9d6633 100644 --- a/src/device-types/DeviceTypeSerializer.h +++ b/src/device-types/DeviceTypeSerializer.h @@ -3,8 +3,8 @@ #include -class HAMqtt; class HADevice; +class BaseDeviceType; class DeviceTypeSerializer { @@ -21,7 +21,7 @@ public: /** * Calculates length of the topic with given parameters. - * Topic format: [discovery prefix]/[component]/[objectId]/[suffix] + * Topic format: [discovery prefix]/[component]/[deviceId]/[objectId]/[suffix] * * @param component * @param objectId @@ -29,7 +29,6 @@ public: * @param includeNullTerminator */ static uint16_t calculateTopicLength( - const HAMqtt* mqtt, const char* component, const char* objectId, const char* suffix, @@ -39,7 +38,7 @@ public: /** * Generates topic and saves it to the given buffer. * Please note that size of the buffer must be calculated by `calculateTopicLength` method first. - * Topic format: [discovery prefix]/[component]/[objectId]/[suffix] + * Topic format: [discovery prefix]/[component]/[deviceId]/[objectId]/[suffix] * * @param output * @param component @@ -48,7 +47,6 @@ public: * @param includeNullTerminator */ static uint16_t generateTopic( - const HAMqtt* mqtt, char* output, const char* component, const char* objectId, @@ -60,39 +58,54 @@ public: const char* name ); static uint16_t calculateUniqueIdFieldSize( - const HADevice* device, const char* name ); static uint16_t calculateAvailabilityFieldSize( - const HAMqtt* mqtt, - const char* componentName, - const char* name + const BaseDeviceType* const dt + ); + static uint16_t calculateRetainFieldSize( + bool retain ); static uint16_t calculateDeviceFieldSize( const char* serializedDevice ); - static void mqttWriteBeginningJson(HAMqtt* mqtt); - static void mqttWriteEndJson(HAMqtt* mqtt); + static void mqttWriteBeginningJson(); + static void mqttWriteEndJson(); static void mqttWriteConstCharField( - HAMqtt* mqtt, const char* prefix, - const char* value + const char* value, + bool quoteSuffix = true + ); + static void mqttWriteNameField( + const char* name ); - static void mqttWriteNameField(HAMqtt* mqtt, const char* name); static void mqttWriteUniqueIdField( - HAMqtt* mqtt, const char* name ); static void mqttWriteAvailabilityField( - HAMqtt* mqtt, - const char* componentName, - const char* name + const BaseDeviceType* const dt + ); + static void mqttWriteRetainField( + bool retain ); static void mqttWriteDeviceField( - HAMqtt* mqtt, const char* serializedDevice ); + static bool mqttWriteTopicField( + const BaseDeviceType* const dt, + const char* jsonPrefix, + const char* topicSuffix + ); + static bool mqttPublishMessage( + const BaseDeviceType* const dt, + const char* topicSuffix, + const char* data + ); + static bool mqttSubscribeTopic( + const BaseDeviceType* const dt, + const char* topicSuffix + ); }; #endif diff --git a/src/device-types/HABinarySensor.cpp b/src/device-types/HABinarySensor.cpp index 997e2c9..6c3ceed 100644 --- a/src/device-types/HABinarySensor.cpp +++ b/src/device-types/HABinarySensor.cpp @@ -1,16 +1,38 @@ #include "HABinarySensor.h" +#ifdef ARDUINOHA_BINARY_SENSOR + #include "../ArduinoHADefines.h" #include "../HAMqtt.h" #include "../HADevice.h" -#include "../HAUtils.h" + +HABinarySensor::HABinarySensor( + const char* name, + bool initialState +) : + BaseDeviceType("binary_sensor", name), + _class(nullptr), + _currentState(initialState) +{ + +} HABinarySensor::HABinarySensor( const char* name, bool initialState, HAMqtt& mqtt ) : - BaseDeviceType(mqtt, "binary_sensor", name), - _class(nullptr), + HABinarySensor(name, initialState) +{ + (void)mqtt; +} + +HABinarySensor::HABinarySensor( + const char* name, + const char* deviceClass, + bool initialState +) : + BaseDeviceType("binary_sensor", name), + _class(deviceClass), _currentState(initialState) { @@ -22,11 +44,9 @@ HABinarySensor::HABinarySensor( bool initialState, HAMqtt& mqtt ) : - BaseDeviceType(mqtt, "binary_sensor", name), - _class(deviceClass), - _currentState(initialState) + HABinarySensor(name, deviceClass, initialState) { - + (void)mqtt; } void HABinarySensor::onMqttConnected() @@ -46,10 +66,6 @@ bool HABinarySensor::setState(bool state) return true; } - if (strlen(name()) == 0) { - return false; - } - if (publishState(state)) { _currentState = state; return true; @@ -58,54 +74,6 @@ bool HABinarySensor::setState(bool state) return false; } -void HABinarySensor::publishConfig() -{ - 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 = DeviceTypeSerializer::calculateTopicLength( - mqtt(), - componentName(), - name(), - DeviceTypeSerializer::ConfigTopic - ); - const uint16_t& dataLength = calculateSerializedLength(serializedDevice); - - if (topicLength == 0 || dataLength == 0) { - return; - } - - char topic[topicLength]; - DeviceTypeSerializer::generateTopic( - mqtt(), - topic, - componentName(), - name(), - DeviceTypeSerializer::ConfigTopic - ); - - if (strlen(topic) == 0) { - return; - } - - if (mqtt()->beginPublish(topic, dataLength, true)) { - writeSerializedData(serializedDevice); - mqtt()->endPublish(); - } -} - bool HABinarySensor::publishState(bool state) { if (strlen(name()) == 0) { @@ -113,7 +81,6 @@ bool HABinarySensor::publishState(bool state) } const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), componentName(), name(), DeviceTypeSerializer::StateTopic @@ -124,7 +91,6 @@ bool HABinarySensor::publishState(bool state) char topic[topicSize]; DeviceTypeSerializer::generateTopic( - mqtt(), topic, componentName(), name(), @@ -162,21 +128,13 @@ uint16_t HABinarySensor::calculateSerializedLength( uint16_t size = 0; size += DeviceTypeSerializer::calculateBaseJsonDataSize(); size += DeviceTypeSerializer::calculateNameFieldSize(name()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(device, name()); + size += DeviceTypeSerializer::calculateUniqueIdFieldSize(name()); size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); - - if (isAvailabilityConfigured()) { - size += DeviceTypeSerializer::calculateAvailabilityFieldSize( - mqtt(), - componentName(), - name() - ); - } + size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); // state topic { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - mqtt(), componentName(), name(), DeviceTypeSerializer::StateTopic, @@ -206,56 +164,31 @@ bool HABinarySensor::writeSerializedData(const char* serializedDevice) const return false; } - DeviceTypeSerializer::mqttWriteBeginningJson(mqtt()); + DeviceTypeSerializer::mqttWriteBeginningJson(); // state topic { - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), - componentName(), - name(), - DeviceTypeSerializer::StateTopic - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - mqtt(), - topic, - componentName(), - name(), - DeviceTypeSerializer::StateTopic - ); - - if (strlen(topic) == 0) { - return false; - } - static const char Prefix[] PROGMEM = {"\"stat_t\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(mqtt(), Prefix, topic); + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + DeviceTypeSerializer::StateTopic + ); } // device class if (_class != nullptr) { static const char Prefix[] PROGMEM = {",\"dev_cla\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(mqtt(), Prefix, _class); + DeviceTypeSerializer::mqttWriteConstCharField(Prefix, _class); } - DeviceTypeSerializer::mqttWriteNameField(mqtt(), name()); - DeviceTypeSerializer::mqttWriteUniqueIdField(mqtt(), name()); - - if (isAvailabilityConfigured()) { - DeviceTypeSerializer::mqttWriteAvailabilityField( - mqtt(), - componentName(), - name() - ); - } - - DeviceTypeSerializer::mqttWriteDeviceField(mqtt(), serializedDevice); - DeviceTypeSerializer::mqttWriteEndJson(mqtt()); + DeviceTypeSerializer::mqttWriteNameField(name()); + DeviceTypeSerializer::mqttWriteUniqueIdField(name()); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); return true; } + +#endif diff --git a/src/device-types/HABinarySensor.h b/src/device-types/HABinarySensor.h index 1973450..b85c0c3 100644 --- a/src/device-types/HABinarySensor.h +++ b/src/device-types/HABinarySensor.h @@ -3,21 +3,27 @@ #include "BaseDeviceType.h" +#ifdef ARDUINOHA_BINARY_SENSOR + class HABinarySensor : public BaseDeviceType { public: /** * Initializes binary sensor. * - * @param name Name of the sensor. Recommendes characters: [a-z0-9\-_] + * @param name Name of the sensor. Recommended characters: [a-z0-9\-_] * @param initialState Initial state of the sensor. It will be published right after "config" message in order to update HA state. */ + HABinarySensor( + const char* name, + bool initialState + ); HABinarySensor( const char* name, bool initialState, HAMqtt& mqtt - ); + ); // legacy constructor /** * Initializes binary sensor with the specified class. @@ -28,6 +34,11 @@ public: * @param initialState Initial state of the sensor. It will be published right after "config" message in order to update HA state. */ + HABinarySensor( + const char* name, + const char* deviceClass, + bool initialState + ); HABinarySensor( const char* name, const char* deviceClass, @@ -58,13 +69,13 @@ public: { return _currentState; } private: - void publishConfig(); bool publishState(bool state); - uint16_t calculateSerializedLength(const char* serializedDevice) const; - bool writeSerializedData(const char* serializedDevice) const; + uint16_t calculateSerializedLength(const char* serializedDevice) const override; + bool writeSerializedData(const char* serializedDevice) const override; const char* _class; bool _currentState; }; #endif +#endif diff --git a/src/device-types/HAFan.cpp b/src/device-types/HAFan.cpp new file mode 100644 index 0000000..3d4f868 --- /dev/null +++ b/src/device-types/HAFan.cpp @@ -0,0 +1,418 @@ +#include "HAFan.h" +#ifdef ARDUINOHA_FAN + +#include "../HAMqtt.h" +#include "../HADevice.h" + +const char* HAFan::SpeedCommandTopic = {"sct"}; +const char* HAFan::SpeedStateTopic = {"sst"}; + +static const char OffStr[] PROGMEM = {"off"}; +static const char LowStr[] PROGMEM = {"low"}; +static const char MediumStr[] PROGMEM = {"medium"}; +static const char HighStr[] PROGMEM = {"high"}; + +HAFan::HAFan(const char* uniqueId, uint8_t features) : + BaseDeviceType("fan", uniqueId), + _features(features), + _speeds(OffSpeed | LowSpeed | MediumSpeed | HighSpeed), + _currentState(false), + _stateCallback(nullptr), + _currentSpeed(OffSpeed), + _speedCallback(nullptr), + _label(nullptr), + _retain(false) +{ + +} + +HAFan::HAFan(const char* uniqueId, uint8_t features, HAMqtt& mqtt) : + HAFan(uniqueId, features) +{ + (void)mqtt; +} + +void HAFan::onMqttConnected() +{ + if (strlen(name()) == 0) { + return; + } + + publishConfig(); + publishAvailability(); + + DeviceTypeSerializer::mqttSubscribeTopic( + this, + DeviceTypeSerializer::CommandTopic + ); + + if (_features & SpeedsFeature) { + DeviceTypeSerializer::mqttSubscribeTopic( + this, + SpeedCommandTopic + ); + } + + if (!_retain) { + publishState(_currentState); + publishSpeed(_currentSpeed); + } +} + +void HAFan::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t& length +) +{ + (void)payload; + + if (isMyTopic(topic, DeviceTypeSerializer::CommandTopic)) { + bool state = (length == strlen(DeviceTypeSerializer::StateOn)); + setState(state, true); + } else if (isMyTopic(topic, SpeedCommandTopic)) { + char speed[length + 1]; + memset(speed, 0, sizeof(speed)); + memcpy(speed, payload, length); + + setSpeedFromStr(speed); + } +} + +bool HAFan::setState(bool state, bool force) +{ + if (!force && _currentState == state) { + return true; + } + + if (publishState(state)) { + _currentState = state; + + if (_stateCallback) { + _stateCallback(state); + } + + return true; + } + + return false; +} + +bool HAFan::setSpeed(Speed speed) +{ + if (speed == UnknownSpeed) { + return false; + } + + if (publishSpeed(speed)) { + _currentSpeed = speed; + + if (_speedCallback) { + _speedCallback(_currentSpeed); + } + + return true; + } + + return false; +} + +bool HAFan::setSpeedFromStr(const char* speed) +{ + if (strcmp_P(speed, OffStr) == 0) { + return setSpeed(OffSpeed); + } else if (strcmp_P(speed, LowStr) == 0) { + return setSpeed(LowSpeed); + } else if (strcmp_P(speed, MediumStr) == 0) { + return setSpeed(MediumSpeed); + } else if (strcmp_P(speed, HighStr) == 0) { + return setSpeed(HighSpeed); + } + + return false; +} + +bool HAFan::publishState(bool state) +{ + if (strlen(name()) == 0) { + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + DeviceTypeSerializer::StateTopic, + ( + state ? + DeviceTypeSerializer::StateOn : + DeviceTypeSerializer::StateOff + ) + ); +} + +bool HAFan::publishSpeed(Speed speed) +{ + if (strlen(name()) == 0 || speed == UnknownSpeed) { + return false; + } + + char speedStr[7]; + switch (speed) { + case OffSpeed: + strcpy_P(speedStr, OffStr); + break; + + case LowSpeed: + strcpy_P(speedStr, LowStr); + break; + + case MediumSpeed: + strcpy_P(speedStr, MediumStr); + break; + + case HighSpeed: + strcpy_P(speedStr, HighStr); + break; + + default: + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + SpeedStateTopic, + speedStr + ); +} + +uint16_t HAFan::calculateSerializedLength(const char* serializedDevice) const +{ + if (serializedDevice == nullptr) { + return 0; + } + + uint16_t size = 0; + size += DeviceTypeSerializer::calculateBaseJsonDataSize(); + size += DeviceTypeSerializer::calculateUniqueIdFieldSize(name()); + size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); + size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); + size += DeviceTypeSerializer::calculateNameFieldSize(_label); + size += DeviceTypeSerializer::calculateRetainFieldSize(_retain); + + // command topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + DeviceTypeSerializer::CommandTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: "cmd_t":"[TOPIC]" + size += topicLength + 10; // 10 - length of the JSON decorators for this field + } + + // state topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + DeviceTypeSerializer::StateTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"stat_t":"[TOPIC]" + size += topicLength + 12; // 12 - length of the JSON decorators for this field + } + + // speeds + if (_features & SpeedsFeature) { + // command topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + SpeedCommandTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"spd_cmd_t":"[TOPIC]" + size += topicLength + 15; // 15 - length of the JSON decorators for this field + } + + // state topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + SpeedStateTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"spd_stat_t":"[TOPIC]" + size += topicLength + 16; // 16 - length of the JSON decorators for this field + } + + // speeds list + // Format: ,"spds":[SPEEDS] + size += calculateSpeedsLength() + 8; // 8 - length of the JSON decorators for this field + } + + return size; // exludes null terminator +} + +uint16_t HAFan::calculateSpeedsLength() const +{ + uint16_t length = 2; // opening and closing bracket + + if (_speeds & OffSpeed) { + // escape + comma + length += 3 + strlen_P(OffStr); + } + + if (_speeds & LowSpeed) { + // escape + comma + length += 3 + strlen_P(LowStr); + } + + if (_speeds & MediumSpeed) { + // escape + comma + length += 3 + strlen_P(MediumStr); + } + + if (_speeds & HighSpeed) { + // escape + comma + length += 3 + strlen_P(HighStr); + } + + if (length > 2) { + length--; // remove trailing comma + } + + return length; // excludes null terminator +} + +bool HAFan::writeSerializedData(const char* serializedDevice) const +{ + if (serializedDevice == nullptr) { + return false; + } + + DeviceTypeSerializer::mqttWriteBeginningJson(); + + // command topic + { + static const char Prefix[] PROGMEM = {"\"cmd_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + DeviceTypeSerializer::CommandTopic + ); + } + + // state topic + { + static const char Prefix[] PROGMEM = {",\"stat_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + DeviceTypeSerializer::StateTopic + ); + } + + // speeds + if (_features & SpeedsFeature) { + // command topic + { + static const char Prefix[] PROGMEM = {",\"spd_cmd_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + SpeedCommandTopic + ); + } + + // state topic + { + static const char Prefix[] PROGMEM = {",\"spd_stat_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + SpeedStateTopic + ); + } + + // supported speeds + { + static const char CharQuotation[] PROGMEM = {"\""}; + static const char CharQuotationComma[] PROGMEM = {"\","}; + static const char Prefix[] PROGMEM = {",\"spds\":"}; + + char speedStr[calculateSpeedsLength() + 1]; // plus null terminator + memset(speedStr, 0, sizeof(speedStr)); + strcpy(speedStr, "["); + + if (_speeds != 0) { + if (_speeds & OffSpeed) { + strcat_P(speedStr, CharQuotation); + strcat_P(speedStr, OffStr); + strcat_P(speedStr, CharQuotationComma); + } + + if (_speeds & LowSpeed) { + strcat_P(speedStr, CharQuotation); + strcat_P(speedStr, LowStr); + strcat_P(speedStr, CharQuotationComma); + } + + if (_speeds & MediumSpeed) { + strcat_P(speedStr, CharQuotation); + strcat_P(speedStr, MediumStr); + strcat_P(speedStr, CharQuotationComma); + } + + if (_speeds & HighSpeed) { + strcat_P(speedStr, CharQuotation); + strcat_P(speedStr, HighStr); + strcat_P(speedStr, CharQuotationComma); + } + + speedStr[strlen(speedStr) - 1] = ']'; + } else { + strcat(speedStr, "]"); + } + + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + speedStr, + false + ); + } + } + + DeviceTypeSerializer::mqttWriteRetainField(_retain); + DeviceTypeSerializer::mqttWriteNameField(_label); + DeviceTypeSerializer::mqttWriteUniqueIdField(name()); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); + + return true; +} + +#endif diff --git a/src/device-types/HAFan.h b/src/device-types/HAFan.h new file mode 100644 index 0000000..b5f2957 --- /dev/null +++ b/src/device-types/HAFan.h @@ -0,0 +1,151 @@ +#ifndef AHA_FAN_H +#define AHA_FAN_H + +#include "BaseDeviceType.h" + +#ifdef ARDUINOHA_FAN + +#define HAFAN_STATE_CALLBACK_BOOL(name) void (*name)(bool) +#define HAFAN_STATE_CALLBACK_SPEED(name) void (*name)(Speed) + +class HAFan : public BaseDeviceType +{ +public: + static const char* SpeedCommandTopic; + static const char* SpeedStateTopic; + + enum Features { + DefaultFeatures = 0, + SpeedsFeature = 1 + }; + + enum Speed { + UnknownSpeed = 0, + OffSpeed = 1, + LowSpeed = 2, + MediumSpeed = 4, + HighSpeed = 8 + }; + + HAFan(const char* uniqueId, uint8_t features = DefaultFeatures); + HAFan(const char* uniqueId, uint8_t features, HAMqtt& mqtt); // legacy constructor + + virtual void onMqttConnected() override; + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t& length + ) override; + + /** + * Changes state of the fan 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 fan (on - true, off - false). + * @param force Forces to update state without comparing it to previous known state. + * @returns Returns true if MQTT message has been published successfully. + */ + bool setState(bool state, bool force = false); + + /** + * Alias for setState(true). + */ + inline bool turnOn() + { return setState(true); } + + /** + * Alias for setState(false). + */ + inline bool turnOff() + { return setState(false); } + + /** + * Returns last known state of the fan. + * If setState method wasn't called the initial value will be returned. + */ + inline bool getState() const + { return _currentState; } + + /** + * Registers callback that will be called each time the state of the fan changes. + * Please note that it's not possible to register multiple callbacks for the same fan. + * + * @param callback + */ + inline void onStateChanged(HAFAN_STATE_CALLBACK_BOOL(callback)) + { _stateCallback = callback; } + + /** + * Sets the list of supported fan's speeds. + * + * @param speeds + */ + inline void setSpeeds(uint8_t speeds) + { _speeds = speeds; } + + /** + * Sets speed of the fan. + * + * @param speed + */ + bool setSpeed(Speed speed); + + /** + * Sets speed of the fan based on the name of the mode. + * + * @param speed + */ + bool setSpeedFromStr(const char* speed); + + /** + * Returns current speed of the fan. + */ + inline Speed getSpeed() const + { return _currentSpeed; } + + /** + * Registers callback that will be called each time the speed of the fan changes. + * Please note that it's not possible to register multiple callbacks for the same fan. + * + * @param callback + */ + inline void onSpeedChanged(HAFAN_STATE_CALLBACK_SPEED(callback)) + { _speedCallback = callback; } + + /** + * Sets name that wil be displayed in the Home Assistant panel. + * + * @param name + */ + inline void setName(const char* name) + { _label = name; } // it needs to be called "label" as "_name" is already in use + + /** + * Sets `retain` flag for commands published by Home Assistant. + * By default it's set to false. + * + * @param retain + */ + inline void setRetain(bool retain) + { _retain = retain; } + +protected: + bool publishState(bool state); + bool publishSpeed(Speed speed); + uint16_t calculateSerializedLength(const char* serializedDevice) const override; + uint16_t calculateSpeedsLength() const; + bool writeSerializedData(const char* serializedDevice) const override; + + const uint8_t _features; + uint8_t _speeds; + bool _currentState; + HAFAN_STATE_CALLBACK_BOOL(_stateCallback); + Speed _currentSpeed; + HAFAN_STATE_CALLBACK_SPEED(_speedCallback); + const char* _label; + bool _retain; +}; + +#endif +#endif diff --git a/src/device-types/HAHVAC.cpp b/src/device-types/HAHVAC.cpp new file mode 100644 index 0000000..ea2a2c6 --- /dev/null +++ b/src/device-types/HAHVAC.cpp @@ -0,0 +1,1079 @@ +#include "HAHVAC.h" +#ifdef ARDUINOHA_HVAC + +#include "../ArduinoHADefines.h" +#include "../HAMqtt.h" +#include "../HADevice.h" +#include "../HAUtils.h" + +const char* HAHVAC::ActionTopic = "at"; +const char* HAHVAC::AuxCommandTopic = "act"; +const char* HAHVAC::AuxStateTopic = "ast"; +const char* HAHVAC::AwayCommandTopic = "amct"; +const char* HAHVAC::AwayStateTopic = "amst"; +const char* HAHVAC::HoldCommandTopic = "hct"; +const char* HAHVAC::HoldStateTopic = "hst"; +const char* HAHVAC::TargetTemperatureCommandTopic = "ttct"; +const char* HAHVAC::TargetTemperatureStateTopic = "ttst"; +const char* HAHVAC::CurrentTemperatureTopic = "ctt"; +const char* HAHVAC::ModeCommandTopic = "mct"; +const char* HAHVAC::ModeStateTopic = "mst"; + +static const char OffStr[] PROGMEM = {"off"}; +static const char AutoStr[] PROGMEM = {"auto"}; +static const char CoolStr[] PROGMEM = {"cool"}; +static const char HeatStr[] PROGMEM = {"heat"}; +static const char DryStr[] PROGMEM = {"dry"}; +static const char FanOnlyStr[] PROGMEM = {"fan_only"}; + +HAHVAC::HAHVAC(const char* uniqueId, uint8_t features) : + BaseDeviceType("climate", uniqueId), + _uniqueId(uniqueId), + _features(features), + _temperatureUnit(DefaultUnit), + _action(OffAction), + _auxHeatingCallback(nullptr), + _auxHeatingState(false), + _awayCallback(nullptr), + _awayState(false), + _holdCallback(nullptr), + _holdState(false), + _currentTemperature(__DBL_MAX__), + _minTemp(__DBL_MAX__), + _maxTemp(__DBL_MAX__), + _tempStep(1), + _targetTempCallback(nullptr), + _targetTemperature(__DBL_MAX__), + _label(nullptr), + _modes( + OffMode | + AutoMode | + CoolMode | + HeatMode | + DryMode | + FanOnlyMode + ), + _modeChangedCallback(nullptr), + _currentMode(Mode::UnknownMode), + _retain(false) +{ + +} + +HAHVAC::HAHVAC( + const char* uniqueId, + uint8_t features, + HAMqtt& mqtt +) : + HAHVAC(uniqueId, features) +{ + (void)mqtt; +} + +void HAHVAC::onMqttConnected() +{ + if (strlen(name()) == 0) { + return; + } + + publishConfig(); + publishCurrentTemperature(_currentTemperature); + publishTargetTemperature(_targetTemperature); + + if (!_retain) { + publishAction(_action); + publishAuxHeatingState(_auxHeatingState); + publishAwayState(_awayState); + publishHoldState(_holdState); + publishMode(_currentMode); + } + + subscribeTopics(); +} + +void HAHVAC::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t& length +) +{ + if ((_features & AuxHeatingFeature) && + isMyTopic(topic, AuxCommandTopic)) { + bool state = (length == strlen(DeviceTypeSerializer::StateOn)); + setAuxHeatingState(state); + } else if ((_features & AwayModeFeature) && + isMyTopic(topic, AwayCommandTopic)) { + bool state = (length == strlen(DeviceTypeSerializer::StateOn)); + setAwayState(state); + } else if ((_features & HoldFeature) && + isMyTopic(topic, HoldCommandTopic)) { + bool state = (length == strlen(DeviceTypeSerializer::StateOn)); + setHoldState(state); + } else if (isMyTopic(topic, TargetTemperatureCommandTopic)) { + char src[length + 1]; + memset(src, 0, sizeof(src)); + memcpy(src, payload, length); + + setTargetTemperature(HAUtils::strToTemp(src)); + } else if (isMyTopic(topic, ModeCommandTopic)) { + char mode[length + 1]; + memset(mode, 0, sizeof(mode)); + memcpy(mode, payload, length); + + setModeFromStr(mode); + } +} + +bool HAHVAC::setAction(Action action) +{ + if (publishAction(action)) { + _action = action; + return true; + } + + return false; +} + +bool HAHVAC::setAuxHeatingState(bool state) +{ + if (publishAuxHeatingState(state)) { + _auxHeatingState = state; + + if (_auxHeatingCallback) { + _auxHeatingCallback(_auxHeatingState); + } + + return true; + } + + return false; +} + +bool HAHVAC::setAwayState(bool state) +{ + if (publishAwayState(state)) { + _awayState = state; + + if (_awayCallback) { + _awayCallback(_awayState); + } + + return true; + } + + return false; +} + +bool HAHVAC::setHoldState(bool state) +{ + if (publishHoldState(state)) { + _holdState = state; + + if (_holdCallback) { + _holdCallback(_holdState); + } + + return true; + } + + return false; +} + +bool HAHVAC::setTargetTemperature(double temperature) +{ + if (publishTargetTemperature(temperature)) { + _targetTemperature = temperature; + + if (_targetTempCallback) { + _targetTempCallback(_targetTemperature); + } + + return true; + } + + return false; +} + +bool HAHVAC::setCurrentTemperature(double temperature) +{ + if (_currentTemperature == temperature) { + return true; + } + + if (publishCurrentTemperature(temperature)) { + _currentTemperature = temperature; + return true; + } + + return false; +} + +bool HAHVAC::setMode(Mode mode) +{ + if (mode == UnknownMode) { + return false; + } + + if (publishMode(mode)) { + _currentMode = mode; + + if (_modeChangedCallback) { + _modeChangedCallback(_currentMode); + } + + return true; + } + + return false; +} + +bool HAHVAC::setModeFromStr(const char* mode) +{ + if (strcmp_P(mode, OffStr) == 0) { + return setMode(OffMode); + } else if (strcmp_P(mode, AutoStr) == 0) { + return setMode(AutoMode); + } else if (strcmp_P(mode, CoolStr) == 0) { + return setMode(CoolMode); + } else if (strcmp_P(mode, HeatStr) == 0) { + return setMode(HeatMode); + } else if (strcmp_P(mode, DryStr) == 0) { + return setMode(DryMode); + } else if (strcmp_P(mode, FanOnlyStr) == 0) { + return setMode(FanOnlyMode); + } + + return false; +} + +bool HAHVAC::setMinTemp(double minTemp) +{ + if (minTemp == __DBL_MAX__) { + return false; + } + + _minTemp = minTemp; + return true; +} + +bool HAHVAC::setMaxTemp(double maxTemp) +{ + if (maxTemp == __DBL_MAX__) { + return false; + } + + _maxTemp = maxTemp; + return true; +} + +bool HAHVAC::setTempStep(double tempStep) +{ + if (tempStep <= 0 || tempStep >= 255) { + return false; + } + + _tempStep = tempStep; + return true; +} + +bool HAHVAC::publishAction(Action action) +{ + if (!(_features & ActionFeature) || + strlen(name()) == 0) { + return false; + } + + char actionStr[8]; + switch (action) { + case OffAction: + strcpy_P(actionStr, OffStr); + break; + + case HeatingAction: + strcpy(actionStr, "heating"); + break; + + case CoolingAction: + strcpy(actionStr, "cooling"); + break; + + case DryingAction: + strcpy(actionStr, "drying"); + break; + + case IdleAction: + strcpy(actionStr, "idle"); + break; + + case FanAction: + strcpy(actionStr, "fan"); + break; + + default: + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + ActionTopic, + actionStr + ); +} + +bool HAHVAC::publishAuxHeatingState(bool state) +{ + if (!(_features & AuxHeatingFeature) || + strlen(name()) == 0) { + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + AuxStateTopic, + ( + state ? + DeviceTypeSerializer::StateOn : + DeviceTypeSerializer::StateOff + ) + ); +} + +bool HAHVAC::publishAwayState(bool state) +{ + if (!(_features & AwayModeFeature) || + strlen(name()) == 0) { + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + AwayStateTopic, + ( + state ? + DeviceTypeSerializer::StateOn : + DeviceTypeSerializer::StateOff + ) + ); +} + +bool HAHVAC::publishHoldState(bool state) +{ + if (!(_features & HoldFeature) || + strlen(name()) == 0) { + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + HoldStateTopic, + ( + state ? + DeviceTypeSerializer::StateOn : + DeviceTypeSerializer::StateOff + ) + ); +} + +bool HAHVAC::publishCurrentTemperature(double temperature) +{ + if (strlen(name()) == 0) { + return false; + } + + if (temperature == __DBL_MAX__) { + return false; + } + + char str[AHA_SERIALIZED_TEMP_SIZE]; + HAUtils::tempToStr(str, temperature); + + return DeviceTypeSerializer::mqttPublishMessage( + this, + CurrentTemperatureTopic, + str + ); +} + +bool HAHVAC::publishTargetTemperature(double temperature) +{ + if (strlen(name()) == 0) { + return false; + } + + if (temperature == __DBL_MAX__) { + return false; + } + + char str[AHA_SERIALIZED_TEMP_SIZE]; + HAUtils::tempToStr(str, temperature); + + return DeviceTypeSerializer::mqttPublishMessage( + this, + TargetTemperatureStateTopic, + str + ); +} + +bool HAHVAC::publishMode(Mode mode) +{ + if (strlen(name()) == 0 || mode == UnknownMode) { + return false; + } + + char modeStr[9]; + switch (mode) { + case OffMode: + strcpy_P(modeStr, OffStr); + break; + + case AutoMode: + strcpy_P(modeStr, AutoStr); + break; + + case CoolMode: + strcpy_P(modeStr, CoolStr); + break; + + case HeatMode: + strcpy_P(modeStr, HeatStr); + break; + + case DryMode: + strcpy_P(modeStr, DryStr); + break; + + case FanOnlyMode: + strcpy_P(modeStr, FanOnlyStr); + break; + + default: + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + ModeStateTopic, + modeStr + ); +} + +void HAHVAC::subscribeTopics() +{ + // aux heating + if (_features & AuxHeatingFeature) { + DeviceTypeSerializer::mqttSubscribeTopic( + this, + AuxCommandTopic + ); + } + + // away mode + if (_features & AwayModeFeature) { + DeviceTypeSerializer::mqttSubscribeTopic( + this, + AwayCommandTopic + ); + } + + // hold + if (_features & HoldFeature) { + DeviceTypeSerializer::mqttSubscribeTopic( + this, + HoldCommandTopic + ); + } + + // target temperature + DeviceTypeSerializer::mqttSubscribeTopic( + this, + TargetTemperatureCommandTopic + ); + + // mode + DeviceTypeSerializer::mqttSubscribeTopic( + this, + ModeCommandTopic + ); +} + +uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const +{ + if (serializedDevice == nullptr || _uniqueId == nullptr) { + return 0; + } + + const HADevice* device = mqtt()->getDevice(); + if (device == nullptr) { + return 0; + } + + uint16_t size = 0; + size += DeviceTypeSerializer::calculateBaseJsonDataSize(); + size += DeviceTypeSerializer::calculateUniqueIdFieldSize(_uniqueId); + size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); + size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); + size += DeviceTypeSerializer::calculateNameFieldSize(_label); + size += DeviceTypeSerializer::calculateRetainFieldSize(_retain); + + // current temperature + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + CurrentTemperatureTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: "curr_temp_t":"[TOPIC]" + size += topicLength + 16; // 16 - length of the JSON decorators for this field + } + + // action topic + if (_features & ActionFeature) { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + ActionTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"act_t":"[TOPIC]" + size += topicLength + 11; // 11 - length of the JSON decorators for this field + } + + // aux heating + if (_features & AuxHeatingFeature) { + // command topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + AuxCommandTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"aux_cmd_t":"[TOPIC]" + size += topicLength + 15; // 15 - length of the JSON decorators for this field + } + + // state topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + AuxStateTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"aux_stat_t":"[TOPIC]" + size += topicLength + 16; // 16 - length of the JSON decorators for this field + } + } + + // away mode + if (_features & AwayModeFeature) { + // command topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + AwayCommandTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"away_mode_cmd_t":"[TOPIC]" + size += topicLength + 21; // 21 - length of the JSON decorators for this field + } + + // state topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + AwayStateTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"away_mode_stat_t":"[TOPIC]" + size += topicLength + 22; // 22 - length of the JSON decorators for this field + } + } + + // hold + if (_features & HoldFeature) { + // command topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + HoldCommandTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"hold_cmd_t":"[TOPIC]" + size += topicLength + 16; // 16 - length of the JSON decorators for this field + } + + // state topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + HoldStateTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"hold_stat_t":"[TOPIC]" + size += topicLength + 17; // 17 - length of the JSON decorators for this field + } + } + + // min temp + if (_minTemp != __DBL_MAX__) { + char str[AHA_SERIALIZED_TEMP_SIZE]; + HAUtils::tempToStr(str, _minTemp); + + // Field format: ,"min_temp":[TEMP] + size += strlen(str) + 12; // 12 - length of the JSON decorators for this field + } + + // max temp + if (_maxTemp != __DBL_MAX__) { + char str[AHA_SERIALIZED_TEMP_SIZE]; + HAUtils::tempToStr(str, _maxTemp); + + // Field format: ,"max_temp":[TEMP] + size += strlen(str) + 12; // 12 - length of the JSON decorators for this field + } + + // temp step + { + char str[AHA_SERIALIZED_TEMP_SIZE]; + HAUtils::tempToStr(str, _tempStep); + + // Field format: ,"temp_step":[TEMP] + size += strlen(str) + 13; // 13 - length of the JSON decorators for this field + } + + // target temperature + { + // command topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + TargetTemperatureCommandTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"temp_cmd_t":"[TOPIC]" + size += topicLength + 16; // 16 - length of the JSON decorators for this field + } + + // state topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + TargetTemperatureStateTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"temp_stat_t":"[TOPIC]" + size += topicLength + 17; // 17 - length of the JSON decorators for this field + } + } + + // temperature unit + if (_temperatureUnit != DefaultUnit) { + // Field format: ,"temp_unit":"[UNIT]" + // UNIT may C or F + size += 15 + 1; // 15 - length of the JSON decorators for this field + } + + // modes + if (_modes != 0) { + // command topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + ModeCommandTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"mode_cmd_t":"[TOPIC]" + size += topicLength + 16; // 16 - length of the JSON decorators for this field + } + + // state topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + name(), + ModeStateTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"mode_stat_t":"[TOPIC]" + size += topicLength + 17; // 17 - length of the JSON decorators for this field + } + } + + // Supported modes + // Field format: ,"modes":MODES + size += 9 + calculateModesLength(); // 9 - length of the JSON decorators for this field + + return size; // excludes null terminator +} + +uint16_t HAHVAC::calculateModesLength() const +{ + uint16_t length = 2; // opening and closing bracket + + if (_modes & OffMode) { + // escape + comma + length += 3 + strlen_P(OffStr); + } + + if (_modes & AutoMode) { + // escape + comma + length += 3 + strlen_P(AutoStr); + } + + if (_modes & CoolMode) { + // escape + comma + length += 3 + strlen_P(CoolStr); + } + + if (_modes & HeatMode) { + // escape + comma + length += 3 + strlen_P(HeatStr); + } + + if (_modes & DryMode) { + // escape + comma + length += 3 + strlen_P(DryStr); + } + + if (_modes & FanOnlyMode) { + // escape + comma + length += 3 + strlen_P(FanOnlyStr); + } + + if (length > 2) { + length--; // remove trailing comma + } + + return length; // excludes null terminator +} + +bool HAHVAC::writeSerializedData(const char* serializedDevice) const +{ + if (serializedDevice == nullptr || _uniqueId == nullptr) { + return false; + } + + DeviceTypeSerializer::mqttWriteBeginningJson(); + + // current temperature topic + { + static const char Prefix[] PROGMEM = {"\"curr_temp_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + CurrentTemperatureTopic + ); + } + + // action topic + if (_features & ActionFeature) { + static const char Prefix[] PROGMEM = {",\"act_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + ActionTopic + ); + } + + // aux heating + if (_features & AuxHeatingFeature) { + // command topic + { + static const char Prefix[] PROGMEM = {",\"aux_cmd_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + AuxCommandTopic + ); + } + + // state topic + { + static const char Prefix[] PROGMEM = {",\"aux_stat_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + AuxStateTopic + ); + } + } + + // away mode + if (_features & AwayModeFeature) { + // command topic + { + static const char Prefix[] PROGMEM = {",\"away_mode_cmd_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + AwayCommandTopic + ); + } + + // state topic + { + static const char Prefix[] PROGMEM = {",\"away_mode_stat_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + AwayStateTopic + ); + } + } + + // hold + if (_features & HoldFeature) { + // command topic + { + static const char Prefix[] PROGMEM = {",\"hold_cmd_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + HoldCommandTopic + ); + } + + // state topic + { + static const char Prefix[] PROGMEM = {",\"hold_stat_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + HoldStateTopic + ); + } + } + + // min temp + if (_minTemp != __DBL_MAX__) { + char str[AHA_SERIALIZED_TEMP_SIZE]; + HAUtils::tempToStr(str, _minTemp); + + static const char Prefix[] PROGMEM = {",\"min_temp\":"}; + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + str, + false + ); + } + + // max temp + if (_maxTemp != __DBL_MAX__) { + char str[AHA_SERIALIZED_TEMP_SIZE]; + HAUtils::tempToStr(str, _maxTemp); + + static const char Prefix[] PROGMEM = {",\"max_temp\":"}; + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + str, + false + ); + } + + // temp step + { + char str[AHA_SERIALIZED_TEMP_SIZE]; + HAUtils::tempToStr(str, _tempStep); + + static const char Prefix[] PROGMEM = {",\"temp_step\":"}; + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + str, + false + ); + } + + // target temperature + { + // command topic + { + static const char Prefix[] PROGMEM = {",\"temp_cmd_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + TargetTemperatureCommandTopic + ); + } + + // state topic + { + static const char Prefix[] PROGMEM = {",\"temp_stat_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + TargetTemperatureStateTopic + ); + } + } + + // temperature unit + if (_temperatureUnit != DefaultUnit) { + static const char Prefix[] PROGMEM = {",\"temp_unit\":\""}; + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + (_temperatureUnit == CelsiusUnit ? "C" : "F") + ); + } + + // modes + if (_modes != 0) { + // command topic + { + static const char Prefix[] PROGMEM = {",\"mode_cmd_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + ModeCommandTopic + ); + } + + // state topic + { + static const char Prefix[] PROGMEM = {",\"mode_stat_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + ModeStateTopic + ); + } + } + + // supported modes + { + static const char CharQuotation[] PROGMEM = {"\""}; + static const char CharQuotationComma[] PROGMEM = {"\","}; + static const char Prefix[] PROGMEM = {",\"modes\":"}; + + char modesStr[calculateModesLength() + 1]; // plus null terminator + memset(modesStr, 0, sizeof(modesStr)); + strcpy(modesStr, "["); + + if (_modes != 0) { + if (_modes & OffMode) { + strcat_P(modesStr, CharQuotation); + strcat_P(modesStr, OffStr); + strcat_P(modesStr, CharQuotationComma); + } + + if (_modes & AutoMode) { + strcat_P(modesStr, CharQuotation); + strcat_P(modesStr, AutoStr); + strcat_P(modesStr, CharQuotationComma); + } + + if (_modes & CoolMode) { + strcat_P(modesStr, CharQuotation); + strcat_P(modesStr, CoolStr); + strcat_P(modesStr, CharQuotationComma); + } + + if (_modes & HeatMode) { + strcat_P(modesStr, CharQuotation); + strcat_P(modesStr, HeatStr); + strcat_P(modesStr, CharQuotationComma); + } + + if (_modes & DryMode) { + strcat_P(modesStr, CharQuotation); + strcat_P(modesStr, DryStr); + strcat_P(modesStr, CharQuotationComma); + } + + if (_modes & FanOnlyMode) { + strcat_P(modesStr, CharQuotation); + strcat_P(modesStr, FanOnlyStr); + strcat_P(modesStr, CharQuotationComma); + } + + modesStr[strlen(modesStr) - 1] = ']'; + } else { + strcat(modesStr, "]"); + } + + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + modesStr, + false + ); + } + + DeviceTypeSerializer::mqttWriteRetainField(_retain); + DeviceTypeSerializer::mqttWriteNameField(_label); + DeviceTypeSerializer::mqttWriteUniqueIdField(_uniqueId); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); + + return true; +} + +#endif diff --git a/src/device-types/HAHVAC.h b/src/device-types/HAHVAC.h new file mode 100644 index 0000000..a9ea351 --- /dev/null +++ b/src/device-types/HAHVAC.h @@ -0,0 +1,345 @@ +#ifndef AHA_HVAC_H +#define AHA_HVAC_H + +#include "BaseDeviceType.h" + +#ifdef ARDUINOHA_HVAC + +#define HAHVAC_STATE_CALLBACK_BOOL(name) void (*name)(bool) +#define HAHVAC_STATE_CALLBACK_DOUBLE(name) void (*name)(double) +#define HAHVAC_STATE_CALLBACK_MODE(name) void (*name)(HAHVAC::Mode) + +class HAHVAC : public BaseDeviceType +{ +public: + static const char* ActionTopic; + static const char* AuxCommandTopic; + static const char* AuxStateTopic; + static const char* AwayCommandTopic; + static const char* AwayStateTopic; + static const char* HoldCommandTopic; + static const char* HoldStateTopic; + static const char* TargetTemperatureCommandTopic; + static const char* TargetTemperatureStateTopic; + static const char* CurrentTemperatureTopic; + static const char* ModeCommandTopic; + static const char* ModeStateTopic; + + enum Features { + DefaultFeatures = 0, + ActionFeature = 1, + AuxHeatingFeature = 2, + AwayModeFeature = 4, + HoldFeature = 8 + }; + + enum Action { + UnknownAction = 0, + OffAction, + HeatingAction, + CoolingAction, + DryingAction, + IdleAction, + FanAction + }; + + enum Mode { + UnknownMode = 0, + OffMode = 1, + AutoMode = 2, + CoolMode = 4, + HeatMode = 8, + DryMode = 16, + FanOnlyMode = 32 + }; + + enum TemperatureUnit { + DefaultUnit = 1, + CelsiusUnit, + FahrenheitUnit + }; + + /** + * Initializes HVAC. + * + * @param uniqueId Unique ID of the HVAC. Recommendes characters: [a-z0-9\-_] + * @param features The list of additional features (flag). For example: `HAHVAC::AuxHeatingFeature | HAHVAC::AwayModeFeature` + */ + HAHVAC( + const char* uniqueId, + uint8_t features = 0 + ); + HAHVAC( + const char* uniqueId, + uint8_t features, + HAMqtt& mqtt + ); // legacy constructor + + virtual void onMqttConnected() override; + + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t& length + ) override; + + /** + * Changes action of the HVAC (it's displayed in the Home Assistant panel). + * + * @param action New action. + * @returns Returns true if MQTT message has been published successfully. + */ + bool setAction(Action action); + + /** + * Returns action that was previously sent to MQTT. + */ + inline Action getAction() const + { return _action; } + + /** + * Changes default temperature unit. + * Please note that this method must be called before `mqtt.begin(...)`. + * + * @param unit See TemperatureUnit enum above + */ + inline void setTemperatureUnit(TemperatureUnit unit) + { _temperatureUnit = unit; } + + /** + * Publishes aux heating state. + * Please note that HAHVAC::AuxHeatingFeature must be set in the constructor. + * + * @param state ON (true) / OFF (false) + * @returns Returns true if MQTT message has been published successfully. + */ + bool setAuxHeatingState(bool state); + + /** + * Registers callback that will be called each time the aux heating's state changes. + * Please note that it's not possible to register multiple callbacks. + * + * @param callback + */ + inline void onAuxHeatingStateChanged(HAHVAC_STATE_CALLBACK_BOOL(callback)) + { _auxHeatingCallback = callback; } + + /** + * Returns state of the aux heating. + * + * @returns ON (true) / OFF (false) + */ + inline bool getAuxHeatingState() const + { return _auxHeatingState; } + + /** + * Publishes away state. + * Please note that HAHVAC::AwayModeFeature must be set in the constructor. + * + * @param state ON (true) / OFF (false) + * @returns Returns true if MQTT message has been published successfully. + */ + bool setAwayState(bool state); + + /** + * Registers callback that will be called each time the away's state changes. + * Please note that it's not possible to register multiple callbacks. + * + * @param callback + */ + inline void onAwayStateChanged(HAHVAC_STATE_CALLBACK_BOOL(callback)) + { _awayCallback = callback; } + + /** + * Returns away's state. + * + * @returns ON (true) / OFF (false) + */ + inline bool getAwayState() const + { return _awayState; } + + /** + * Publishes hold state. + * Please note that HAHVAC::HoldFeature must be set in the constructor. + * + * @param state ON (true) / OFF (false) + * @returns Returns true if MQTT message has been published successfully. + */ + bool setHoldState(bool state); + + /** + * Registers callback that will be called each time the hold's state changes. + * Please note that it's not possible to register multiple callbacks. + * + * @param callback + */ + inline void onHoldStateChanged(HAHVAC_STATE_CALLBACK_BOOL(callback)) + { _holdCallback = callback; } + + /** + * Returns hold's state. + * + * @returns ON (true) / OFF (false) + */ + inline bool getHoldState() const + { return _holdState; } + + /** + * Publishes given target temperature. + * + * @param targetTemperature + * @returns Returns true if MQTT message has been published successfully. + */ + bool setTargetTemperature(double targetTemperature); + + /** + * Registers callback that will be called each time the target temperature changes + * Please note that it's not possible to register multiple callbacks. + * + * @param callback + */ + inline void onTargetTemperatureChanged(HAHVAC_STATE_CALLBACK_DOUBLE(callback)) + { _targetTempCallback = callback; } + + /** + * Returns target temperature. + * + * @returns Target temperature or __DBL_MAX__ if temperature is not set. + */ + inline double getTargetTemperature() const + { return _targetTemperature; } + + /** + * Publishes current temperature. + * + * @param temperature + * @returns Returns true if MQTT message has been published successfully. + */ + bool setCurrentTemperature(double temperature); + + /** + * Return temperature that was previously set by `setCurrentTemperature` method. + * + * @returns Temperature or __DBL_MAX__ if temperature is not set. + */ + inline double getCurrentTemperature() const + { return _currentTemperature; } + + /** + * Publishes working mode of the HVAC. + * + * @param mode + * @returns Returns true if MQTT message has been published successfully. + */ + bool setMode(Mode mode); + + /** + * Same as above but input is the string representation of the mode. + * + * @param mode + * @returns Returns true if MQTT message has been published successfully. + */ + bool setModeFromStr(const char* mode); + + /** + * Registers callback that will be called each time the mode changes. + * Please note that it's not possible to register multiple callbacks. + * + * @param callback + */ + inline void onModeChanged(HAHVAC_STATE_CALLBACK_MODE(callback)) + { _modeChangedCallback = callback; } + + /** + * Returns HVAC's mode. + * + * @returns It may be UnknownMode if it's not set. + */ + inline Mode getMode() const + { return _currentMode; } + + /** + * Sets name that wil be displayed in the Home Assistant panel. + * + * @param name + */ + inline void setName(const char* name) + { _label = name; } // it needs to be called "label" as "_name" is already in use + + /** + * Sets the list of supported modes. By default the list contains all available modes. + * You can merge multiple modes as following: `setModes(HAHVAC::OffMode | HAHVAC::CoolMode)` + * + * @param modes + */ + inline void setModes(uint8_t modes) + { _modes = modes; } + + /** + * Sets `retain` flag for commands published by Home Assistant. + * By default it's set to false. + * + * @param retain + */ + inline void setRetain(bool retain) + { _retain = retain; } + + /** + * Sets the minimum temperature that user will be able to select in Home Assistant panel. + * + * @param minTemp + */ + bool setMinTemp(double minTemp); + + /** + * Sets the maximum temperature that user will be able to select in Home Assistant panel. + * + * @param minTemp + */ + bool setMaxTemp(double maxTemp); + + /** + * Sets the step of the temperature's picker in the Home Assistant panel. + * + * @param tempStep + */ + bool setTempStep(double tempStep); + +private: + bool publishAction(Action action); + bool publishAuxHeatingState(bool state); + bool publishAwayState(bool state); + bool publishHoldState(bool state); + bool publishCurrentTemperature(double temperature); + bool publishTargetTemperature(double temperature); + bool publishMode(Mode mode); + void subscribeTopics(); + uint16_t calculateSerializedLength(const char* serializedDevice) const override; + uint16_t calculateModesLength() const; + bool writeSerializedData(const char* serializedDevice) const override; + + const char* _uniqueId; + const uint8_t _features; + TemperatureUnit _temperatureUnit; + Action _action; + HAHVAC_STATE_CALLBACK_BOOL(_auxHeatingCallback); + bool _auxHeatingState; + HAHVAC_STATE_CALLBACK_BOOL(_awayCallback); + bool _awayState; + HAHVAC_STATE_CALLBACK_BOOL(_holdCallback); + bool _holdState; + double _currentTemperature; + double _minTemp; + double _maxTemp; + double _tempStep; + HAHVAC_STATE_CALLBACK_DOUBLE(_targetTempCallback); + double _targetTemperature; + const char* _label; + uint8_t _modes; + HAHVAC_STATE_CALLBACK_MODE(_modeChangedCallback); + Mode _currentMode; + bool _retain; +}; + +#endif +#endif diff --git a/src/device-types/HASensor.cpp b/src/device-types/HASensor.cpp index efafcdc..f67f358 100644 --- a/src/device-types/HASensor.cpp +++ b/src/device-types/HASensor.cpp @@ -3,138 +3,90 @@ #endif #include "HASensor.h" -#include "../ArduinoHADefines.h" +#ifdef ARDUINOHA_SENSOR + #include "../HAMqtt.h" #include "../HADevice.h" -#include "../HAUtils.h" -template -HASensor::HASensor( - const char* name, - T initialValue, - HAMqtt& mqtt -) : - BaseDeviceType(mqtt, "sensor", name), +HASensor::HASensor(const char* name) : + BaseDeviceType("sensor", name), _class(nullptr), _units(nullptr), - _valueType(HAUtils::determineValueType()), - _currentValue(initialValue) + _icon(nullptr) { } -template -HASensor::HASensor( - const char* name, - const char* deviceClass, - T initialValue, - HAMqtt& mqtt -) : - BaseDeviceType(mqtt, "sensor", name), - _class(deviceClass), - _units(nullptr), - _valueType(HAUtils::determineValueType()), - _currentValue(initialValue) +HASensor::HASensor(const char* name, HAMqtt& mqtt) : + HASensor(name) { - + (void)mqtt; } -template -void HASensor::onMqttConnected() +void HASensor::onMqttConnected() { - if (strlen(name()) == 0 || - _valueType == HAUtils::ValueTypeUnknown) { + if (strlen(name()) == 0) { return; } publishConfig(); - publishValue(_currentValue); publishAvailability(); } -template -bool HASensor::setValue(T value) +bool HASensor::setValue(const char* value) { - if (strlen(name()) == 0 || - _valueType == HAUtils::ValueTypeUnknown) { + return publishValue(value); +} + +bool HASensor::setValue(uint32_t value) +{ + uint8_t digitsNb = floor(log10(value)) + 1; + char str[digitsNb + 1]; // + null terminator + memset(str, 0, sizeof(str)); + itoa(value, str, 10); + + return publishValue(str); +} + +bool HASensor::setValue(int32_t value) +{ + uint8_t digitsNb = floor(log10(value)) + 1; + char str[digitsNb + 2]; // + null terminator and minus sign + memset(str, 0, sizeof(str)); + itoa(value, str, 10); + + return publishValue(str); +} + +bool HASensor::setValue(double value, uint8_t precision) +{ + uint8_t digitsNb = floor(log10(floor(value))) + 1; + char str[digitsNb + 3 + precision]; // null terminator, dot, minus sign + dtostrf(value, 0, precision, str); + + return publishValue(str); +} + +bool HASensor::setValue(float value, uint8_t precision) +{ + uint8_t digitsNb = floor(log10(floor(value))) + 1; + char str[digitsNb + 3 + precision]; // null terminator, dot, minus sign + dtostrf(value, 0, precision, str); + + return publishValue(str); +} + +bool HASensor::publishValue(const char* value) +{ + if (strlen(name()) == 0 || value == nullptr) { return false; } - if (_currentValue == value) { - return true; - } - - 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 = DeviceTypeSerializer::calculateTopicLength( - mqtt(), - componentName(), - name(), - DeviceTypeSerializer::ConfigTopic - ); - const uint16_t& dataLength = calculateSerializedLength(serializedDevice); - - if (topicLength == 0 || dataLength == 0) { - return; - } - - char topic[topicLength]; - DeviceTypeSerializer::generateTopic( - mqtt(), - topic, - componentName(), - name(), - DeviceTypeSerializer::ConfigTopic - ); - - if (strlen(topic) == 0) { - return; - } - - if (mqtt()->beginPublish(topic, dataLength, true)) { - writeSerializedData(serializedDevice); - mqtt()->endPublish(); - } -} - -template -bool HASensor::publishValue(T value) -{ - if (strlen(name()) == 0 || - _valueType == HAUtils::ValueTypeUnknown) { + if (!mqtt()->isConnected()) { return false; } const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), componentName(), name(), DeviceTypeSerializer::StateTopic @@ -145,7 +97,6 @@ bool HASensor::publishValue(T value) char topic[topicSize]; DeviceTypeSerializer::generateTopic( - mqtt(), topic, componentName(), name(), @@ -156,26 +107,14 @@ bool HASensor::publishValue(T value) 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); + return mqtt()->publish(topic, value, true); } -template -uint16_t HASensor::calculateSerializedLength( +uint16_t HASensor::calculateSerializedLength( const char* serializedDevice ) const { - if (serializedDevice == nullptr || - _valueType == HAUtils::ValueTypeUnknown) { + if (serializedDevice == nullptr) { return 0; } @@ -187,20 +126,12 @@ uint16_t HASensor::calculateSerializedLength( uint16_t size = 0; size += DeviceTypeSerializer::calculateBaseJsonDataSize(); size += DeviceTypeSerializer::calculateNameFieldSize(name()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(device, name()); + size += DeviceTypeSerializer::calculateUniqueIdFieldSize(name()); size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); - - if (isAvailabilityConfigured()) { - size += DeviceTypeSerializer::calculateAvailabilityFieldSize( - mqtt(), - componentName(), - name() - ); - } + size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); { const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), componentName(), name(), DeviceTypeSerializer::StateTopic, @@ -227,142 +158,61 @@ uint16_t HASensor::calculateSerializedLength( size += strlen(_units) + 18; // 18 - length of the JSON decorators for this field } + // icon + if (_icon != nullptr) { + // Field format: ,"ic":"[ICON]" + size += strlen(_icon) + 8; // 8 - length of the JSON decorators for this field + } + return size; // exludes null terminator } -template -bool HASensor::writeSerializedData(const char* serializedDevice) const +bool HASensor::writeSerializedData(const char* serializedDevice) const { - if (serializedDevice == nullptr || - _valueType == HAUtils::ValueTypeUnknown) { + if (serializedDevice == nullptr) { return false; } - DeviceTypeSerializer::mqttWriteBeginningJson(mqtt()); + DeviceTypeSerializer::mqttWriteBeginningJson(); // state topic { - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), - componentName(), - name(), - DeviceTypeSerializer::StateTopic - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - mqtt(), - topic, - componentName(), - name(), - DeviceTypeSerializer::StateTopic - ); - - if (strlen(topic) == 0) { - return false; - } - static const char Prefix[] PROGMEM = {"\"stat_t\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(mqtt(), Prefix, topic); + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + DeviceTypeSerializer::StateTopic + ); } // device class if (_class != nullptr) { static const char Prefix[] PROGMEM = {",\"dev_cla\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(mqtt(), Prefix, _class); + DeviceTypeSerializer::mqttWriteConstCharField(Prefix, _class); } // units of measurement if (_units != nullptr) { static const char Prefix[] PROGMEM = {",\"unit_of_meas\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(mqtt(), Prefix, _units); + DeviceTypeSerializer::mqttWriteConstCharField(Prefix, _units); } - DeviceTypeSerializer::mqttWriteNameField(mqtt(), name()); - DeviceTypeSerializer::mqttWriteUniqueIdField(mqtt(), name()); - - if (isAvailabilityConfigured()) { - DeviceTypeSerializer::mqttWriteAvailabilityField( - mqtt(), - componentName(), - name() + // icon + if (_icon != nullptr) { + static const char Prefix[] PROGMEM = {",\"ic\":\""}; + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + _icon ); } - DeviceTypeSerializer::mqttWriteDeviceField(mqtt(), serializedDevice); - DeviceTypeSerializer::mqttWriteEndJson(mqtt()); + DeviceTypeSerializer::mqttWriteNameField(name()); + DeviceTypeSerializer::mqttWriteUniqueIdField(name()); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); return true; } -template -uint16_t HASensor::calculateValueLength() const -{ - if (_valueType == HAUtils::ValueTypeUnknown) { - return 0; - } - - uint16_t size = 0; - - switch (_valueType) { - case HAUtils::ValueTypeUint8: - size = 3; // from 0 to 255 - break; - - case HAUtils::ValueTypeUint16: - size = 5; // from 0 to 65535 - break; - - case HAUtils::ValueTypeUint32: - size = 10; // from 0 to 4294967295 - break; - - case HAUtils::ValueTypeInt8: - size = 4; // from -128 to 127 - break; - - case HAUtils::ValueTypeInt16: - size = 6; // from -32768 to 32767 - break; - - case HAUtils::ValueTypeInt32: - size = 11; // from -2147483648 to 2147483647 - break; - - case HAUtils::ValueTypeDouble: - case HAUtils::ValueTypeFloat: - // 3 digits per byte + dot separator + 2 precision digits - return (HAUtils::getValueTypeLength(_valueType) * 3) + 3; - } - - return (size > 0 ? (size + 1) : 0); -} - -template -bool HASensor::valueToStr(char* dst, T value) const -{ - if (_valueType == HAUtils::ValueTypeUnknown) { - return false; - } - - switch (_valueType) { - case HAUtils::ValueTypeUint8: - case HAUtils::ValueTypeUint16: - case HAUtils::ValueTypeUint32: - case HAUtils::ValueTypeInt8: - case HAUtils::ValueTypeInt16: - case HAUtils::ValueTypeInt32: - itoa(value, dst, 10); - break; - - case HAUtils::ValueTypeDouble: - case HAUtils::ValueTypeFloat: - dtostrf(value, 0, 2, dst); - break; - } - - return true; -} +#endif diff --git a/src/device-types/HASensor.h b/src/device-types/HASensor.h index 8d17800..db6840f 100644 --- a/src/device-types/HASensor.h +++ b/src/device-types/HASensor.h @@ -2,9 +2,9 @@ #define AHA_HASENSOR_H #include "BaseDeviceType.h" -#include "../HAUtils.h" -template +#ifdef ARDUINOHA_SENSOR + class HASensor : public BaseDeviceType { public: @@ -12,30 +12,14 @@ 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 + const char* name ); - - /** - * 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 - ); + ); // legacy constructor /** * Publishes configuration of the sensor to the MQTT. @@ -43,21 +27,38 @@ public: 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. + * Publishes new value of the sensor. + * Please note that connection to MQTT broker must be acquired. + * Otherwise method will return false. * - * @param state New state of the sensor. + * @param state Value to publish. * @returns Returns true if MQTT message has been published successfully. */ - bool setValue(T value); + bool setValue(const char* value); + bool setValue(uint32_t value); + bool setValue(int32_t value); + bool setValue(double value, uint8_t precision = 2); + bool setValue(float value, uint8_t precision = 2); + + inline bool setValue(uint8_t value) + { return setValue(static_cast(value)); } + + inline bool setValue(uint16_t value) + { return setValue(static_cast(value)); } + + inline bool setValue(int8_t value) + { return setValue(static_cast(value)); } + + inline bool setValue(int16_t value) + { return setValue(static_cast(value)); } /** - * Returns last known state of the sensor. - * If setState method wasn't called the initial value will be returned. + * The type/class of the sensor to set the icon in the frontend. + * + * @param className https://www.home-assistant.io/integrations/sensor/#device-class */ - inline T getValue() const - { return _currentValue; } + inline void setDeviceClass(const char* className) + { _class = className; } /** * Defines the units of measurement of the sensor, if any. @@ -67,18 +68,23 @@ public: inline void setUnitOfMeasurement(const char* units) { _units = units; } + /** + * Sets icon of the sensor, e.g. `mdi:home`. + * + * @param icon Material Design Icon name with mdi: prefix. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + private: - void publishConfig(); - bool publishValue(T value); - uint16_t calculateSerializedLength(const char* serializedDevice) const; - bool writeSerializedData(const char* serializedDevice) const; - uint16_t calculateValueLength() const; - bool valueToStr(char* dst, T value) const; + bool publishValue(const char* value); + uint16_t calculateSerializedLength(const char* serializedDevice) const override; + bool writeSerializedData(const char* serializedDevice) const override; const char* _class; const char* _units; - HAUtils::ValueType _valueType; - T _currentValue; + const char* _icon; }; #endif +#endif diff --git a/src/device-types/HASwitch.cpp b/src/device-types/HASwitch.cpp index b581814..6c58aa8 100644 --- a/src/device-types/HASwitch.cpp +++ b/src/device-types/HASwitch.cpp @@ -1,17 +1,30 @@ #include "HASwitch.h" +#ifdef ARDUINOHA_SWITCH + #include "../ArduinoHADefines.h" #include "../HAMqtt.h" #include "../HADevice.h" -#include "../HAUtils.h" -HASwitch::HASwitch(const char* name, bool initialState, HAMqtt& mqtt) : - BaseDeviceType(mqtt, "switch", name), +HASwitch::HASwitch(const char* name, bool initialState) : + BaseDeviceType("switch", name), _stateCallback(nullptr), - _currentState(initialState) + _currentState(initialState), + _icon(nullptr), + _retain(false) { } +HASwitch::HASwitch( + const char* name, + bool initialState, + HAMqtt& mqtt +) : + HASwitch(name, initialState) +{ + (void)mqtt; +} + void HASwitch::onMqttConnected() { if (strlen(name()) == 0) { @@ -19,9 +32,16 @@ void HASwitch::onMqttConnected() } publishConfig(); - publishState(_currentState); - subscribeCommandTopic(); publishAvailability(); + + if (!_retain) { + publishState(_currentState); + } + + DeviceTypeSerializer::mqttSubscribeTopic( + this, + DeviceTypeSerializer::CommandTopic + ); } void HASwitch::onMqttMessage( @@ -30,170 +50,50 @@ void HASwitch::onMqttMessage( const uint16_t& length ) { - if (strlen(name()) == 0) { - return; - } + (void)payload; - static const char Slash[] PROGMEM = {"/"}; - // name + cmd topic + two slashes + null terminator - uint8_t suffixLength = strlen(name()) + strlen(DeviceTypeSerializer::CommandTopic) + 3; - char suffix[suffixLength]; - - strcpy_P(suffix, Slash); - strcat(suffix, name()); - strcat_P(suffix, Slash); - strcat(suffix, DeviceTypeSerializer::CommandTopic); - - if (HAUtils::endsWith(topic, suffix)) { - bool onState = (length == strlen(DeviceTypeSerializer::StateOn)); - setState(onState); + if (isMyTopic(topic, DeviceTypeSerializer::CommandTopic)) { + bool state = (length == strlen(DeviceTypeSerializer::StateOn)); + setState(state, true); } } -bool HASwitch::setState(bool state) +bool HASwitch::setState(bool state, bool force) { - if (_currentState == state) { + if (!force && _currentState == state) { return true; } - if (strlen(name()) == 0) { - return false; - } - if (publishState(state)) { _currentState = state; - triggerCallback(_currentState); + + if (_stateCallback) { + _stateCallback(state, this); + } + return true; } return false; } -void HASwitch::triggerCallback(bool state) -{ - if (_stateCallback == nullptr) { - return; - } - - _stateCallback(state, this); -} - -void HASwitch::publishConfig() -{ - 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 = DeviceTypeSerializer::calculateTopicLength( - mqtt(), - componentName(), - name(), - DeviceTypeSerializer::ConfigTopic - ); - const uint16_t& dataLength = calculateSerializedLength(serializedDevice); - - if (topicLength == 0 || dataLength == 0) { - return; - } - - char topic[topicLength]; - DeviceTypeSerializer::generateTopic( - mqtt(), - topic, - componentName(), - name(), - DeviceTypeSerializer::ConfigTopic - ); - - if (strlen(topic) == 0) { - return; - } - - if (mqtt()->beginPublish(topic, dataLength, true)) { - writeSerializedData(serializedDevice); - mqtt()->endPublish(); - } -} - bool HASwitch::publishState(bool state) { if (strlen(name()) == 0) { return false; } - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), - componentName(), - name(), - DeviceTypeSerializer::StateTopic - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - mqtt(), - topic, - componentName(), - name(), - DeviceTypeSerializer::StateTopic - ); - - if (strlen(topic) == 0) { - return false; - } - - return mqtt()->publish( - topic, + return DeviceTypeSerializer::mqttPublishMessage( + this, + DeviceTypeSerializer::StateTopic, ( state ? DeviceTypeSerializer::StateOn : DeviceTypeSerializer::StateOff - ), - true + ) ); } -void HASwitch::subscribeCommandTopic() -{ - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), - componentName(), - name(), - DeviceTypeSerializer::CommandTopic - ); - if (topicSize == 0) { - return; - } - - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - mqtt(), - topic, - componentName(), - name(), - DeviceTypeSerializer::CommandTopic - ); - - if (strlen(topic) == 0) { - return; - } - - mqtt()->subscribe(topic); -} - uint16_t HASwitch::calculateSerializedLength(const char* serializedDevice) const { if (serializedDevice == nullptr) { @@ -208,21 +108,14 @@ uint16_t HASwitch::calculateSerializedLength(const char* serializedDevice) const uint16_t size = 0; size += DeviceTypeSerializer::calculateBaseJsonDataSize(); size += DeviceTypeSerializer::calculateNameFieldSize(name()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(device, name()); + size += DeviceTypeSerializer::calculateUniqueIdFieldSize(name()); size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); - - if (isAvailabilityConfigured()) { - size += DeviceTypeSerializer::calculateAvailabilityFieldSize( - mqtt(), - componentName(), - name() - ); - } + size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); + size += DeviceTypeSerializer::calculateRetainFieldSize(_retain); // cmd topic { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - mqtt(), componentName(), name(), DeviceTypeSerializer::CommandTopic, @@ -240,7 +133,6 @@ uint16_t HASwitch::calculateSerializedLength(const char* serializedDevice) const // state topic { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( - mqtt(), componentName(), name(), DeviceTypeSerializer::StateTopic, @@ -255,6 +147,12 @@ uint16_t HASwitch::calculateSerializedLength(const char* serializedDevice) const size += topicLength + 12; // 12 - length of the JSON decorators for this field } + // icon + if (_icon != nullptr) { + // Field format: ,"ic":"[ICON]" + size += strlen(_icon) + 8; // 8 - length of the JSON decorators for this field + } + return size; // exludes null terminator } @@ -264,79 +162,45 @@ bool HASwitch::writeSerializedData(const char* serializedDevice) const return false; } - DeviceTypeSerializer::mqttWriteBeginningJson(mqtt()); + DeviceTypeSerializer::mqttWriteBeginningJson(); // command topic { - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), - componentName(), - name(), - DeviceTypeSerializer::CommandTopic - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - mqtt(), - topic, - componentName(), - name(), - DeviceTypeSerializer::CommandTopic - ); - - if (strlen(topic) == 0) { - return false; - } - static const char Prefix[] PROGMEM = {"\"cmd_t\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(mqtt(), Prefix, topic); + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + DeviceTypeSerializer::CommandTopic + ); } // state topic { - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), - componentName(), - name(), - DeviceTypeSerializer::StateTopic - ); - if (topicSize == 0) { - return false; - } - - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - mqtt(), - topic, - componentName(), - name(), - DeviceTypeSerializer::StateTopic - ); - - if (strlen(topic) == 0) { - return false; - } - static const char Prefix[] PROGMEM = {",\"stat_t\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(mqtt(), Prefix, topic); - } - - DeviceTypeSerializer::mqttWriteNameField(mqtt(), name()); - DeviceTypeSerializer::mqttWriteUniqueIdField(mqtt(), name()); - - if (isAvailabilityConfigured()) { - DeviceTypeSerializer::mqttWriteAvailabilityField( - mqtt(), - componentName(), - name() + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + DeviceTypeSerializer::StateTopic ); } - DeviceTypeSerializer::mqttWriteDeviceField(mqtt(), serializedDevice); - DeviceTypeSerializer::mqttWriteEndJson(mqtt()); + // icon + if (_icon != nullptr) { + static const char Prefix[] PROGMEM = {",\"ic\":\""}; + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + _icon + ); + } + + DeviceTypeSerializer::mqttWriteRetainField(_retain); + DeviceTypeSerializer::mqttWriteNameField(name()); + DeviceTypeSerializer::mqttWriteUniqueIdField(name()); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); return true; } + +#endif diff --git a/src/device-types/HASwitch.h b/src/device-types/HASwitch.h index 9558189..5489cbe 100644 --- a/src/device-types/HASwitch.h +++ b/src/device-types/HASwitch.h @@ -3,7 +3,9 @@ #include "BaseDeviceType.h" -#define HASWITCH_CALLBACK void (*callback)(bool, HASwitch*) +#ifdef ARDUINOHA_SWITCH + +#define HASWITCH_CALLBACK(name) void (*name)(bool, HASwitch*) class HASwitch : public BaseDeviceType { @@ -15,11 +17,15 @@ public: * @param initialState Initial state of the switch. It will be published right after "config" message in order to update HA state. */ + HASwitch( + const char* name, + bool initialState + ); HASwitch( const char* name, bool initialState, HAMqtt& mqtt - ); + ); // legacy constructor /** * Publishes configuration of the sensor to the MQTT. @@ -48,9 +54,10 @@ public: * the MQTT message won't be published. * * @param state New state of the switch. + * @param force Forces to update state without comparing it to previous known state. * @returns Returns true if MQTT message has been published successfully. */ - bool setState(bool state); + bool setState(bool state, bool force = false); /** * Alias for setState(true). @@ -77,19 +84,36 @@ public: * * @param callback */ - inline void onStateChanged(HASWITCH_CALLBACK) + inline void onStateChanged(HASWITCH_CALLBACK(callback)) { _stateCallback = callback; } -private: - void triggerCallback(bool state); - void publishConfig(); - bool publishState(bool state); - void subscribeCommandTopic(); - uint16_t calculateSerializedLength(const char* serializedDevice) const; - bool writeSerializedData(const char* serializedDevice) const; + /** + * Sets icon of the switch, e.g. `mdi:home`. + * + * @param icon Material Design Icon name with mdi: prefix. + */ + inline void setIcon(const char* icon) + { _icon = icon; } - void (*_stateCallback)(bool, HASwitch*); + /** + * Sets `retain` flag for commands published by Home Assistant. + * By default it's set to false. + * + * @param retain + */ + inline void setRetain(bool retain) + { _retain = retain; } + +private: + bool publishState(bool state); + uint16_t calculateSerializedLength(const char* serializedDevice) const override; + bool writeSerializedData(const char* serializedDevice) const override; + + HASWITCH_CALLBACK(_stateCallback); bool _currentState; + const char* _icon; + bool _retain; }; #endif +#endif diff --git a/src/device-types/HATagScanner.cpp b/src/device-types/HATagScanner.cpp index adec525..26de9a5 100644 --- a/src/device-types/HATagScanner.cpp +++ b/src/device-types/HATagScanner.cpp @@ -1,15 +1,22 @@ #include "HATagScanner.h" +#ifdef ARDUINOHA_TAG_SCANNER + #include "../ArduinoHADefines.h" #include "../HAMqtt.h" #include "../HADevice.h" -#include "../HAUtils.h" -HATagScanner::HATagScanner(const char* name, HAMqtt& mqtt) : - BaseDeviceType(mqtt, "tag", name) +HATagScanner::HATagScanner(const char* name) : + BaseDeviceType("tag", name) { } +HATagScanner::HATagScanner(const char* name, HAMqtt& mqtt) : + HATagScanner(name) +{ + (void)mqtt; +} + void HATagScanner::onMqttConnected() { if (strlen(name()) == 0) { @@ -21,12 +28,11 @@ void HATagScanner::onMqttConnected() bool HATagScanner::tagScanned(const char* tag) { - if (tag == nullptr || strlen(tag) == 0) { + if (tag == nullptr || strlen(tag) == 0 || strlen(name()) == 0) { return false; } const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), componentName(), name(), DeviceTypeSerializer::EventTopic @@ -37,7 +43,6 @@ bool HATagScanner::tagScanned(const char* tag) char topic[topicSize]; DeviceTypeSerializer::generateTopic( - mqtt(), topic, componentName(), name(), @@ -51,54 +56,6 @@ bool HATagScanner::tagScanned(const char* tag) return mqtt()->publish(topic, tag); } -void HATagScanner::publishConfig() -{ - 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 = DeviceTypeSerializer::calculateTopicLength( - mqtt(), - componentName(), - name(), - DeviceTypeSerializer::ConfigTopic - ); - const uint16_t& dataLength = calculateSerializedLength(serializedDevice); - - if (topicLength == 0 || dataLength == 0) { - return; - } - - char topic[topicLength]; - DeviceTypeSerializer::generateTopic( - mqtt(), - topic, - componentName(), - name(), - DeviceTypeSerializer::ConfigTopic - ); - - if (strlen(topic) == 0) { - return; - } - - if (mqtt()->beginPublish(topic, dataLength, true)) { - writeSerializedData(serializedDevice); - mqtt()->endPublish(); - } -} - uint16_t HATagScanner::calculateSerializedLength( const char* serializedDevice ) const @@ -114,7 +71,6 @@ uint16_t HATagScanner::calculateSerializedLength( // event topic { const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), componentName(), name(), DeviceTypeSerializer::EventTopic, @@ -134,31 +90,22 @@ bool HATagScanner::writeSerializedData(const char* serializedDevice) const return false; } - DeviceTypeSerializer::mqttWriteBeginningJson(mqtt()); + DeviceTypeSerializer::mqttWriteBeginningJson(); // topic { - const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - mqtt(), - componentName(), - name(), - DeviceTypeSerializer::EventTopic - ); - char topic[topicSize]; - DeviceTypeSerializer::generateTopic( - mqtt(), - topic, - componentName(), - name(), - DeviceTypeSerializer::EventTopic - ); - static const char Prefix[] PROGMEM = {"\"t\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(mqtt(), Prefix, topic); + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + DeviceTypeSerializer::EventTopic + ); } - DeviceTypeSerializer::mqttWriteDeviceField(mqtt(), serializedDevice); - DeviceTypeSerializer::mqttWriteEndJson(mqtt()); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); return true; } + +#endif diff --git a/src/device-types/HATagScanner.h b/src/device-types/HATagScanner.h index 3e885a7..ac19302 100644 --- a/src/device-types/HATagScanner.h +++ b/src/device-types/HATagScanner.h @@ -3,6 +3,8 @@ #include "BaseDeviceType.h" +#ifdef ARDUINOHA_TAG_SCANNER + class HATagScanner : public BaseDeviceType { public: @@ -11,7 +13,8 @@ public: * * @param name Name of the scanner. Recommendes characters: [a-z0-9\-_] */ - HATagScanner(const char* name, HAMqtt& mqtt); + HATagScanner(const char* name); + HATagScanner(const char* name, HAMqtt& mqtt); // legacy constructor /** * Publishes configuration of the sensor to the MQTT. @@ -21,7 +24,7 @@ public: /** * Tag scanner doesn't support availability. Nothing to do here. */ - virtual void setAvailability(bool online) override { } + virtual void setAvailability(bool online) override { (void)online; } /** * Sends "tag scanned" event to the MQTT (Home Assistant). @@ -32,9 +35,9 @@ public: bool tagScanned(const char* tag); private: - void publishConfig(); - uint16_t calculateSerializedLength(const char* serializedDevice) const; - bool writeSerializedData(const char* serializedDevice) const; + uint16_t calculateSerializedLength(const char* serializedDevice) const override; + bool writeSerializedData(const char* serializedDevice) const override; }; #endif +#endif diff --git a/src/device-types/HATriggers.cpp b/src/device-types/HATriggers.cpp index b51b93e..1bff911 100644 --- a/src/device-types/HATriggers.cpp +++ b/src/device-types/HATriggers.cpp @@ -1,16 +1,24 @@ #include "HATriggers.h" +#ifdef ARDUINOHA_TRIGGERS + #include "../ArduinoHADefines.h" #include "../HAMqtt.h" #include "../HADevice.h" -HATriggers::HATriggers(HAMqtt& mqtt) : - BaseDeviceType(mqtt, "device_automation", nullptr), +HATriggers::HATriggers() : + BaseDeviceType("device_automation", nullptr), _triggers(nullptr), _triggersNb(0) { } +HATriggers::HATriggers(HAMqtt& mqtt) : + HATriggers() +{ + (void)mqtt; +} + HATriggers::~HATriggers() { if (_triggers != nullptr) { @@ -87,6 +95,14 @@ bool HATriggers::trigger(const char* type, const char* subtype) return false; } +#if defined(ARDUINOHA_DEBUG) + Serial.print(F("Triggering HATrigger: ")); + Serial.print(type); + Serial.print(F(" ")); + Serial.print(subtype); + Serial.println(); +#endif + return mqtt()->publish(topic, ""); } @@ -122,7 +138,6 @@ void HATriggers::publishConfig() trigger, serializedDevice ); - if (topicLength == 0 || dataLength == 0) { continue; } @@ -153,9 +168,8 @@ uint16_t HATriggers::calculateTopicLength( bool includeNullTerminator ) const { - uint8_t length = strlen(trigger->type) + strlen(trigger->subtype) + 1; // + underscore + uint8_t length = strlen(trigger->type) + strlen(trigger->subtype) + 2; // + underscore and slash return DeviceTypeSerializer::calculateTopicLength( - mqtt(), component, nullptr, suffix, @@ -179,7 +193,6 @@ uint16_t HATriggers::generateTopic( strcat(objectId, trigger->type); return DeviceTypeSerializer::generateTopic( - mqtt(), output, component, objectId, @@ -246,7 +259,7 @@ bool HATriggers::writeSerializedTrigger( return false; } - DeviceTypeSerializer::mqttWriteBeginningJson(mqtt()); + DeviceTypeSerializer::mqttWriteBeginningJson(); // automation type { @@ -273,28 +286,29 @@ bool HATriggers::writeSerializedTrigger( DeviceTypeSerializer::EventTopic ); - if (strlen(topic) == 0) { - return false; - } - static const char Prefix[] PROGMEM = {",\"t\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(mqtt(), Prefix, topic); + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + topic + ); } // type { static const char Prefix[] PROGMEM = {",\"type\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(mqtt(), Prefix, trigger->type); + DeviceTypeSerializer::mqttWriteConstCharField(Prefix, trigger->type); } // subtype { static const char Prefix[] PROGMEM = {",\"stype\":\""}; - DeviceTypeSerializer::mqttWriteConstCharField(mqtt(), Prefix, trigger->subtype); + DeviceTypeSerializer::mqttWriteConstCharField(Prefix, trigger->subtype); } - DeviceTypeSerializer::mqttWriteDeviceField(mqtt(), serializedDevice); - DeviceTypeSerializer::mqttWriteEndJson(mqtt()); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); return true; } + +#endif diff --git a/src/device-types/HATriggers.h b/src/device-types/HATriggers.h index 90ca8c1..47bd098 100644 --- a/src/device-types/HATriggers.h +++ b/src/device-types/HATriggers.h @@ -3,6 +3,8 @@ #include "BaseDeviceType.h" +#ifdef ARDUINOHA_TRIGGERS + struct HATrigger { const char* type; const char* subtype; @@ -11,7 +13,8 @@ struct HATrigger { class HATriggers : public BaseDeviceType { public: - HATriggers(HAMqtt& mqtt); + HATriggers(); + HATriggers(HAMqtt& mqtt); // legacy constructor virtual ~HATriggers(); virtual void onMqttConnected() override; @@ -19,12 +22,22 @@ public: /** * Triggers dont't support availability. Nothing to do here. */ - virtual void setAvailability(bool online) override { } + virtual void setAvailability(bool online) override { (void)online; } bool add(const char* type, const char* subtype); bool trigger(const char* type, const char* subtype); protected: + void publishConfig() override; + + uint16_t calculateSerializedLength( + const char* serializedDevice + ) const override { (void)serializedDevice; return 0; } + + bool writeSerializedData( + const char* serializedDevice + ) const override { (void)serializedDevice; return false; } + uint16_t calculateTopicLength( const char* component, const HATrigger *trigger, @@ -40,8 +53,6 @@ protected: ) const; private: - void publishConfig(); - uint16_t calculateSerializedLength( const HATrigger* trigger, const char* serializedDevice @@ -57,3 +68,4 @@ private: }; #endif +#endif