From a4e50b7d048373d1254fef2e3e48f5e9a5fd51fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Chyrzy=C5=84ski?= Date: Sat, 17 Apr 2021 20:03:32 +0200 Subject: [PATCH] Release 1.3.0 (#36) * Add support for HVAC (#15) * remove HASwitch::triggerCallback method * fix typo in docs * HASwitch: simplify callback #define * work in progress: HVAC * replace local mqtt instances with singleton * device types optimization * cleanup * finished aux heating and away mode * wip: HVAC * finished HVAC implementation * HVAC implementation * add ifdefs for all HA components * wip: optimization * update readme * update examples * remove unused constructor from HAMqtt * update HAUtils * remove mqtt from constructors, fix compilation warnings * finished HVAC * add HVAC example, add changelog * update changelog, add documentation * Add support for icons in HASwitch and HASensor (#16) * add support for icon in HASwitch * add support for icon in HASensor * update changelog * add support for setting retain flag in HASwitch * Add support for text payload in HASensor (#17) * refactored HASensor * update changelog * fix sensor example * Add support for fans (HAFan) (#18) * finished basic implementation of the HAFan * add retain method to DeviceTypeSerializer * finished HAFan implementation * update HAHVAC * device types cleanup * add HAFan example * update changelog * update readme * update fan example * optimization, add support for ActionFeature in HVAC * Add support for MQTT LWT (#19) * add support for hostname in HAMqtt * add support for LWT and shared availability * minor fixes * added advanced availability example * update examples * update changelog * cleanup & bug fix * bump up version * minor improvements and fixes * update readme * add HAMqtt::onMessage method * Add support for covers (#28) * add HACovers * add cover example, update implementation * disable debug * update mqtt-events example * Add support for setting different prefix for non-discovery topics (#29) * add support for data prefix * cleanup * update comments and logs * update multi-state-button example * rename rexample from mqtt-events to mqtt-advanced * update changelog * update links in changelog * Home Assistant 2021.4.5 - MQTT upgrade (#33) * updae HAFan implementation * change lib version * update changelog * fix setSpeed method * update HAFan example * Separate uniqueId from name (#34) * rename name to uniqueId * remove legacy labels * update examples * rename isMyTopic to compareTopics * update changelog, rename name to uniqueId * Implement onBeforeStateChanged callback in HASwitch (#35) * add low latency mode to HASwitch * add onBeforeStateChanged callback to HASwitch * update readme * rename DEPRECATED macro * update advanced MQTT example --- CHANGELOG.md | 20 +- README.md | 39 ++- .../advanced-availability.ino | 4 +- examples/availability/availability.ino | 4 +- examples/binary-sensor/binary-sensor.ino | 4 +- examples/cover/cover.ino | 49 +++ examples/fan/fan.ino | 19 +- examples/led-switch/led-switch.ino | 10 +- examples/mqtt-advanced/mqtt-advanced.ino | 56 ++++ examples/mqtt-events/mqtt-events.ino | 32 -- .../mqtt-with-credentials.ino | 3 +- .../multi-state-button/multi-state-button.ino | 2 - examples/nano33iot/nano33iot.ino | 3 +- examples/nodemcu/nodemcu.ino | 10 +- examples/sensor/sensor.ino | 9 +- library.properties | 2 +- src/ArduinoHA.h | 1 + src/ArduinoHADefines.h | 10 + src/HAMqtt.cpp | 14 +- src/HAMqtt.h | 28 ++ src/device-types/BaseDeviceType.cpp | 40 +-- src/device-types/BaseDeviceType.h | 17 +- src/device-types/DeviceTypeSerializer.cpp | 64 ++-- src/device-types/DeviceTypeSerializer.h | 18 +- src/device-types/HABinarySensor.cpp | 34 +-- src/device-types/HABinarySensor.h | 12 +- src/device-types/HACover.cpp | 286 ++++++++++++++++++ src/device-types/HACover.h | 113 +++++++ src/device-types/HAFan.cpp | 245 ++++++--------- src/device-types/HAFan.h | 57 ++-- src/device-types/HAHVAC.cpp | 63 ++-- src/device-types/HAHVAC.h | 9 - src/device-types/HASensor.cpp | 26 +- src/device-types/HASensor.h | 6 +- src/device-types/HASwitch.cpp | 33 +- src/device-types/HASwitch.h | 24 +- src/device-types/HATagScanner.cpp | 18 +- src/device-types/HATagScanner.h | 6 +- src/device-types/HATriggers.cpp | 23 +- src/device-types/HATriggers.h | 6 +- 40 files changed, 977 insertions(+), 442 deletions(-) create mode 100644 examples/cover/cover.ino create mode 100644 examples/mqtt-advanced/mqtt-advanced.ino delete mode 100644 examples/mqtt-events/mqtt-events.ino create mode 100644 src/device-types/HACover.cpp create mode 100644 src/device-types/HACover.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 44c818ea..4573d102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,27 @@ # Changelog +## 1.3.0 + +**New features:** +* Added `onMessage()` method to HAMqtt class +* Added support for HA Covers +* Added support for setting different prefix for non-discovery topics (see [Advanced MQTT example](examples/mqtt-advanced/mqtt-advanced.ino)) +* Added `setName` method to HASensor +* Added `setName` method to HASwitch +* Added `onBeforeStateChanged` callback to HASwitch + +**Updates:** +* Removed legacy properties from HAFan (Home Assistant 2021.4.4). Deprecated methods will be removed after a quarter (2021.7) +* Separated `uniqueID` field from `name` in all devices types + ## 1.2.0 **Breaking changes:** -* Refactored HASensor implementation. Please take a look at updated example in `examples/sensor/sensor.ino` +* Refactored HASensor implementation. Please take a look at [updated example](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 excluding devices types from the compilation using defines (see [src/ArduinoHADefines.h](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 @@ -15,7 +29,7 @@ * 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`) +* Added support for MQTT LWT (see [Advanced Availability example](examples/advanced-availability/advanced-availability.ino)) **Updates:** * Optimized codebase and logic in all devices types diff --git a/README.md b/README.md index e07eed2b..9957e954 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,25 @@ but I successfully use it on ESP8266/ESP8255 boards in my projects. * MQTT discovery (device is added to the Home Assistant panel automatically) * MQTT Last Will and Testament +* Support for custom MQTT messages (publishing and subscribing) * Auto reconnect with MQTT broker +* Reporting availability (online/offline states) of a device + +## Supported HA types + +* Binary sensors +* Covers +* 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)* ## Examples * [Binary Sensor](examples/binary-sensor/binary-sensor.ino) +* [Cover](examples/cover/cover.ino) * [Fan](examples/fan/fan.ino) * [LED switch](examples/led-switch/led-switch.ino) * [Multi-state button](examples/multi-state-button/multi-state-button.ino) @@ -24,32 +38,30 @@ but I successfully use it on ESP8266/ESP8255 boards in my projects. * [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) +* [MQTT advanced](examples/mqtt-advanced/mqtt-advanced.ino) ## Tested boards * Arduino Uno * Arduino Mega -* Controllino Maxi (standard/pure/automation/power) -* Controllino Mega (standard/pure) * NodeMCU * ESP-01 * Generic ESP8266/ESP8255 * Arduino Nano 33 IoT (SAMD) -## Tested Arduino Shields +## Tested devices -* Arduino Ethernet Shield (WizNet W5100) +* Controllino Maxi (standard/pure/automation/power) +* Controllino Mega (standard/pure) +* Sonoff Dual R2 +* Sonoff Basic +* Sonoff Mini +* Tuya Wi-Fi switch module +* Tuya Wi-Fi curtain module -## Supported HA types +## Tested Arduino Shields -* 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)* +* Arduino Ethernet Shield (WizNet W5100) ## Roadmap @@ -57,7 +69,6 @@ but I successfully use it on ESP8266/ESP8255 boards in my projects. * 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 index d052ffcf..64df5bf8 100644 --- a/examples/advanced-availability/advanced-availability.ino +++ b/examples/advanced-availability/advanced-availability.ino @@ -13,7 +13,7 @@ EthernetClient client; HADevice device(mac, sizeof(mac)); HAMqtt mqtt(client, device); -// "input" may be anything you want to be displayed in HA panel +// "input" is unique ID of the sensor. You should define you own ID. // "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); @@ -32,6 +32,8 @@ void setup() { device.setName("Arduino"); device.setSoftwareVersion("1.0.0"); + sensor.setName("Door sensor"); // optional + // 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 diff --git a/examples/availability/availability.ino b/examples/availability/availability.ino index eab30c1d..d3325abf 100644 --- a/examples/availability/availability.ino +++ b/examples/availability/availability.ino @@ -13,7 +13,7 @@ EthernetClient client; HADevice device(mac, sizeof(mac)); HAMqtt mqtt(client, device); -// "input" may be anything you want to be displayed in HA panel +// "input" is unique ID of the sensor. You should define you own ID. // "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); @@ -36,6 +36,8 @@ void setup() { device.setName("Arduino"); device.setSoftwareVersion("1.0.0"); + sensor.setName("Door sensor"); // optional + mqtt.begin(BROKER_ADDR); } diff --git a/examples/binary-sensor/binary-sensor.ino b/examples/binary-sensor/binary-sensor.ino index 43c884fe..8b0862f4 100644 --- a/examples/binary-sensor/binary-sensor.ino +++ b/examples/binary-sensor/binary-sensor.ino @@ -12,7 +12,7 @@ EthernetClient client; HADevice device(mac, sizeof(mac)); HAMqtt mqtt(client, device); -// "input" may be anything you want to be displayed in HA panel +// "input" is unique ID of the sensor. You should define you own ID. // "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); @@ -28,6 +28,8 @@ void setup() { device.setName("Arduino"); device.setSoftwareVersion("1.0.0"); + sensor.setName("Door sensor"); // optional + mqtt.begin(BROKER_ADDR); } diff --git a/examples/cover/cover.ino b/examples/cover/cover.ino new file mode 100644 index 00000000..adfc9205 --- /dev/null +++ b/examples/cover/cover.ino @@ -0,0 +1,49 @@ +#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); +HACover cover("my-cover"); // "my-cover" is unique ID of the cover. You should define your own ID. + +void onCoverCommand(HACover::CoverCommand cmd) { + if (cmd == HACover::CommandOpen) { + Serial.println("Command: Open"); + cover.setState(HACover::StateOpening); + } else if (cmd == HACover::CommandClose) { + Serial.println("Command: Close"); + cover.setState(HACover::StateClosing); + } else if (cmd == HACover::CommandStop) { + Serial.println("Command: Stop"); + cover.setState(HACover::StateStopped); + } + + // Available states: + // HACover::StateClosed + // HACover::StateClosing + // HACover::StateOpen + // HACover::StateOpening + // HACover::StateStopped + + // You can also report position using setPosition() method +} + +void setup() { + Serial.begin(9600); + Ethernet.begin(mac); + + cover.onCommand(onCoverCommand); + cover.setName("My cover"); // optional + // cover.setRetain(true); // optionally you can set retain flag + + mqtt.begin(BROKER_ADDR); +} + +void loop() { + Ethernet.maintain(); + mqtt.loop(); +} diff --git a/examples/fan/fan.ino b/examples/fan/fan.ino index 332de0f1..dddfdcf0 100644 --- a/examples/fan/fan.ino +++ b/examples/fan/fan.ino @@ -11,6 +11,7 @@ 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. +// "ventilation" is unique ID of the fan. You should define your own ID. HAFan fan("ventilation", HAFan::SpeedsFeature); void onStateChanged(bool state) { @@ -18,20 +19,14 @@ void onStateChanged(bool state) { Serial.println(state); } -void onSpeedChanged(HAFan::Speed speed) { +void onSpeedChanged(uint16_t 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"); - } + Serial.println(speed); } void setup() { + Serial.begin(9600); + // you don't need to verify return status Ethernet.begin(mac); @@ -40,10 +35,10 @@ void setup() { 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); + fan.setSpeedRangeMin(1); + fan.setSpeedRangeMax(100); // handle fan states fan.onStateChanged(onStateChanged); diff --git a/examples/led-switch/led-switch.ino b/examples/led-switch/led-switch.ino index c7dbefc7..52b61c66 100644 --- a/examples/led-switch/led-switch.ino +++ b/examples/led-switch/led-switch.ino @@ -9,7 +9,13 @@ byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; EthernetClient client; HADevice device(mac, sizeof(mac)); HAMqtt mqtt(client, device); -HASwitch led("led", false); // you can use custom name in place of "led" +HASwitch led("led", false); // "led" is unique ID of the switch. You should define your own ID. + +void onBeforeSwitchStateChanged(bool state, HASwitch* s) +{ + // this callback will be called before publishing new state to HA + // in some cases there may be delay before onStateChanged is called due to network latency +} void onSwitchStateChanged(bool state, HASwitch* s) { @@ -29,8 +35,10 @@ void setup() { // set icon (optional) led.setIcon("mdi:lightbulb"); + led.setName("My LED"); // handle switch state + led.onBeforeStateChanged(onBeforeSwitchStateChanged); // optional led.onStateChanged(onSwitchStateChanged); mqtt.begin(BROKER_ADDR); diff --git a/examples/mqtt-advanced/mqtt-advanced.ino b/examples/mqtt-advanced/mqtt-advanced.ino new file mode 100644 index 00000000..e2c7c62f --- /dev/null +++ b/examples/mqtt-advanced/mqtt-advanced.ino @@ -0,0 +1,56 @@ +#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 onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) { + // This callback is called when message from MQTT broker is received. + // Please note that you should always verify if the message's topic is the one you expect. + // For example: if (memcmp(topic, "myCustomTopic") == 0) { ... } + + Serial.print("New message on topic: "); + Serial.println(topic); + Serial.print("Data: "); + Serial.println((const char*)payload); + + mqtt.publish("myPublishTopic", "hello"); +} + +void onMqttConnected() { + Serial.println("Connected to the broker!"); + + // You can subscribe to custom topic if you need + mqtt.subscribe("myCustomTopic"); +} + +void onMqttConnectionFailed() { + Serial.println("Failed to connect to the broker!"); +} + +void setup() { + Serial.begin(9600); + Ethernet.begin(mac); + + mqtt.onMessage(onMqttMessage); + mqtt.onConnected(onMqttConnected); + mqtt.onConnectionFailed(onMqttConnectionFailed); + + // If you use custom discovery prefix you can change it as following: + // mqtt.setDiscoveryPrefix("customPrefix"); + + // If you want to change prefix only for non-discovery prefix: + // mqtt.setDataPrefix("data"); + + mqtt.begin(BROKER_ADDR); +} + +void loop() { + Ethernet.maintain(); + mqtt.loop(); +} diff --git a/examples/mqtt-events/mqtt-events.ino b/examples/mqtt-events/mqtt-events.ino deleted file mode 100644 index aaba0f01..00000000 --- a/examples/mqtt-events/mqtt-events.ino +++ /dev/null @@ -1,32 +0,0 @@ -#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 7dfd06a1..c4d5a920 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); // you can use custom name in place of "led" +HASwitch led("led", false); // "led" is unique ID of the switch. You should define your own ID. void onSwitchStateChanged(bool state, HASwitch* s) { @@ -31,6 +31,7 @@ void setup() { // handle switch state led.onStateChanged(onSwitchStateChanged); + led.setName("My LED"); // optional mqtt.begin(BROKER_ADDR, BROKER_USERNAME, BROKER_PASSWORD); } diff --git a/examples/multi-state-button/multi-state-button.ino b/examples/multi-state-button/multi-state-button.ino index ba15fbf0..219efef9 100644 --- a/examples/multi-state-button/multi-state-button.ino +++ b/examples/multi-state-button/multi-state-button.ino @@ -22,8 +22,6 @@ void setup() { // you don't need to verify return status Ethernet.begin(mac); - Serial.println(Ethernet.localIP()); - // set device's details (optional) device.setName("Arduino"); device.setSoftwareVersion("1.0.0"); diff --git a/examples/nano33iot/nano33iot.ino b/examples/nano33iot/nano33iot.ino index 4db9e3c2..11561949 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); // you can use custom name in place of "led" +HASwitch led("led", false); // "led" is unique ID of the switch. You should define your own ID. void onSwitchStateChanged(bool state, HASwitch* s) { @@ -43,6 +43,7 @@ void setup() { // handle switch state led.onStateChanged(onSwitchStateChanged); + led.setName("My LED"); // optional mqtt.begin(BROKER_ADDR); } diff --git a/examples/nodemcu/nodemcu.ino b/examples/nodemcu/nodemcu.ino index 74cb01be..6a38d624 100644 --- a/examples/nodemcu/nodemcu.ino +++ b/examples/nodemcu/nodemcu.ino @@ -9,7 +9,13 @@ WiFiClient client; HADevice device; HAMqtt mqtt(client, device); -HASwitch led("led", false); // you can use custom name in place of "led" +HASwitch led("led", false); // "led" is unique ID of the switch. You should define your own ID. + +void onBeforeSwitchStateChanged(bool state, HASwitch* s) +{ + // this callback will be called before publishing new state to HA + // in some cases there may be delay before onStateChanged is called due to network latency +} void onSwitchStateChanged(bool state, HASwitch* s) { @@ -42,7 +48,9 @@ void setup() { device.setSoftwareVersion("1.0.0"); // handle switch state + led.onBeforeStateChanged(onBeforeSwitchStateChanged); // optional led.onStateChanged(onSwitchStateChanged); + led.setName("My LED"); // optional mqtt.begin(BROKER_ADDR); } diff --git a/examples/sensor/sensor.ino b/examples/sensor/sensor.ino index e6f13de4..95c166b8 100644 --- a/examples/sensor/sensor.ino +++ b/examples/sensor/sensor.ino @@ -10,7 +10,13 @@ double lastValue = 0; EthernetClient client; HADevice device(mac, sizeof(mac)); HAMqtt mqtt(client, device); -HASensor temp("temp"); +HASensor temp("temp"); // "temp" is unique ID of the sensor. You should define your own ID. + +void onBeforeSwitchStateChanged(bool state, HASwitch* s) +{ + // this callback will be called before publishing new state to HA + // in some cases there may be delay before onStateChanged is called due to network latency +} void setup() { // you don't need to verify return status @@ -24,6 +30,7 @@ void setup() { temp.setUnitOfMeasurement("°C"); temp.setDeviceClass("temperature"); temp.setIcon("mdi:home"); + temp.setName("Home temperature"); mqtt.begin(BROKER_ADDR); } diff --git a/library.properties b/library.properties index 85a6fcdb..d4d96675 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=home-assistant-integration -version=1.2.0 +version=1.3.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 8c9bc507..696ddec2 100644 --- a/src/ArduinoHA.h +++ b/src/ArduinoHA.h @@ -5,6 +5,7 @@ #include "HAMqtt.h" #include "HAUtils.h" #include "device-types/HABinarySensor.h" +#include "device-types/HACover.h" #include "device-types/HAFan.h" #include "device-types/HAHVAC.h" #include "device-types/HASensor.h" diff --git a/src/ArduinoHADefines.h b/src/ArduinoHADefines.h index 8f9611b9..d0cf0cb2 100644 --- a/src/ArduinoHADefines.h +++ b/src/ArduinoHADefines.h @@ -5,9 +5,19 @@ // You can reduce Flash size of the compiled library by commenting unused components below #define ARDUINOHA_BINARY_SENSOR +#define ARDUINOHA_COVER #define ARDUINOHA_FAN #define ARDUINOHA_HVAC #define ARDUINOHA_SENSOR #define ARDUINOHA_SWITCH #define ARDUINOHA_TAG_SCANNER #define ARDUINOHA_TRIGGERS + +#ifdef __GNUC__ +#define AHA_DEPRECATED(func) func __attribute__ ((deprecated)) +#elif defined(_MSC_VER) +#define AHA_DEPRECATED(func) __declspec(deprecated) func +#else +#warning "Arduino Home Assistant: You may miss deprecation warnings." +#define AHA_DEPRECATED(func) func +#endif diff --git a/src/HAMqtt.cpp b/src/HAMqtt.cpp index 04961526..166c3a25 100644 --- a/src/HAMqtt.cpp +++ b/src/HAMqtt.cpp @@ -20,10 +20,12 @@ void onMessageReceived(char* topic, uint8_t* payload, unsigned int length) HAMqtt::HAMqtt(Client& netClient, HADevice& device) : _netClient(netClient), _device(device), + _messageCallback(nullptr), _connectedCallback(nullptr), _connectionFailedCallback(nullptr), _initialized(false), _discoveryPrefix(DefaultDiscoveryPrefix), + _dataPrefix(nullptr), _mqtt(new PubSubClient(netClient)), _username(nullptr), _password(nullptr), @@ -195,9 +197,9 @@ bool HAMqtt::publish(const char* topic, const char* payload, bool retained) } #if defined(ARDUINOHA_DEBUG) - Serial.print(F("Publishing message with topic: ")); + Serial.print(F("Publishing: ")); Serial.print(topic); - Serial.print(F(", payload length: ")); + Serial.print(F(", len: ")); Serial.print(strlen(payload)); Serial.println(); #endif @@ -214,9 +216,9 @@ bool HAMqtt::beginPublish( ) { #if defined(ARDUINOHA_DEBUG) - Serial.print(F("Publishing message with topic: ")); + Serial.print(F("Publishing: ")); Serial.print(topic); - Serial.print(F(", payload length: ")); + Serial.print(F(", len: ")); Serial.print(payloadLength); Serial.println(); #endif @@ -263,6 +265,10 @@ void HAMqtt::processMessage(char* topic, uint8_t* payload, uint16_t length) Serial.println(); #endif + if (_messageCallback) { + _messageCallback(topic, payload, length); + } + for (uint8_t i = 0; i < _devicesTypesNb; i++) { _devicesTypes[i]->onMqttMessage(topic, payload, length); } diff --git a/src/HAMqtt.h b/src/HAMqtt.h index 588fcf2c..6f84a90b 100644 --- a/src/HAMqtt.h +++ b/src/HAMqtt.h @@ -5,6 +5,7 @@ #include #define HAMQTT_CALLBACK(name) void (*name)() +#define HAMQTT_MESSAGE_CALLBACK(name) void (*name)(const char* topic, const uint8_t* payload, uint16_t length) #define HAMQTT_DEFAULT_PORT 1883 class PubSubClient; @@ -25,6 +26,8 @@ class HAMqtt * Sets prefix for Home Assistant discovery. * It needs to match prefix set in the HA admin panel. * The default prefix is "homeassistant". + * + * @param prefix */ inline void setDiscoveryPrefix(const char* prefix) { _discoveryPrefix = prefix; } @@ -35,12 +38,35 @@ class HAMqtt inline const char* getDiscoveryPrefix() const { return _discoveryPrefix; } + /** + * Sets prefix that will be used for topics different than discovery. + * It may be useful if you want to pass MQTT trafic through bridge. + * + * @param prefix + */ + inline void setDataPrefix(const char* prefix) + { _dataPrefix = prefix; } + + /** + * Returns data prefix. + */ + inline const char* getDataPrefix() const + { return _dataPrefix; } + /** * Returns instance of the device assigned to the HAMqtt class. */ inline HADevice const* getDevice() const { return &_device; } + /** + * Given callback will be called for each received message from the broker. + * + * @param callback + */ + inline void onMessage(HAMQTT_MESSAGE_CALLBACK(callback)) + { _messageCallback = callback; } + /** * Given callback will be called each time the connection with broker is acquired. * @@ -197,10 +223,12 @@ class HAMqtt Client& _netClient; HADevice& _device; + HAMQTT_MESSAGE_CALLBACK(_messageCallback); HAMQTT_CALLBACK(_connectedCallback); HAMQTT_CALLBACK(_connectionFailedCallback); bool _initialized; const char* _discoveryPrefix; + const char* _dataPrefix; PubSubClient* _mqtt; const char* _username; const char* _password; diff --git a/src/device-types/BaseDeviceType.cpp b/src/device-types/BaseDeviceType.cpp index 748b5e5d..86756d26 100644 --- a/src/device-types/BaseDeviceType.cpp +++ b/src/device-types/BaseDeviceType.cpp @@ -5,11 +5,12 @@ BaseDeviceType::BaseDeviceType( const char* componentName, - const char* name + const char* uniqueId ) : _componentName(componentName), - _name(name), - _availability(AvailabilityDefault) + _uniqueId(uniqueId), + _availability(AvailabilityDefault), + _name(nullptr) { mqtt()->addDeviceType(this); } @@ -60,8 +61,10 @@ void BaseDeviceType::publishConfig() const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), - DeviceTypeSerializer::ConfigTopic + uniqueId(), + DeviceTypeSerializer::ConfigTopic, + true, + true ); const uint16_t& dataLength = calculateSerializedLength(serializedDevice); @@ -73,8 +76,9 @@ void BaseDeviceType::publishConfig() DeviceTypeSerializer::generateTopic( topic, componentName(), - name(), - DeviceTypeSerializer::ConfigTopic + uniqueId(), + DeviceTypeSerializer::ConfigTopic, + true ); if (strlen(topic) == 0) { @@ -96,15 +100,15 @@ void BaseDeviceType::publishAvailability() if (_availability == AvailabilityDefault || !mqtt()->isConnected() || - strlen(_name) == 0 || - strlen(_componentName) == 0 || + strlen(uniqueId()) == 0 || + strlen(componentName()) == 0 || device->isSharedAvailabilityEnabled()) { return; } const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( - _componentName, - _name, + componentName(), + uniqueId(), DeviceTypeSerializer::AvailabilityTopic ); if (topicSize == 0) { @@ -114,8 +118,8 @@ void BaseDeviceType::publishAvailability() char topic[topicSize]; DeviceTypeSerializer::generateTopic( topic, - _componentName, - _name, + componentName(), + uniqueId(), DeviceTypeSerializer::AvailabilityTopic ); @@ -134,24 +138,24 @@ void BaseDeviceType::publishAvailability() ); } -bool BaseDeviceType::isMyTopic(const char* topic, const char* expectedTopic) +bool BaseDeviceType::compareTopics(const char* topic, const char* expectedTopic) { if (topic == nullptr || expectedTopic == nullptr) { return false; } - if (strlen(name()) == 0) { + if (strlen(uniqueId()) == 0) { return false; } static const char Slash[] PROGMEM = {"/"}; - // name + cmd topic + two slashes + null terminator - uint8_t suffixLength = strlen(name()) + strlen(expectedTopic) + 3; + // unique ID + cmd topic + two slashes + null terminator + uint8_t suffixLength = strlen(uniqueId()) + strlen(expectedTopic) + 3; char suffix[suffixLength]; strcpy_P(suffix, Slash); - strcat(suffix, name()); + strcat(suffix, uniqueId()); strcat_P(suffix, Slash); strcat(suffix, expectedTopic); diff --git a/src/device-types/BaseDeviceType.h b/src/device-types/BaseDeviceType.h index 40e3004e..a46a8d4b 100644 --- a/src/device-types/BaseDeviceType.h +++ b/src/device-types/BaseDeviceType.h @@ -13,12 +13,12 @@ class BaseDeviceType public: BaseDeviceType( const char* componentName, - const char* name + const char* uniqueId ); virtual ~BaseDeviceType(); - inline const char* name() const - { return _name; } + inline const char* uniqueId() const + { return _uniqueId; } inline const char* componentName() const { return _componentName; } @@ -29,6 +29,12 @@ class BaseDeviceType inline bool isOnline() const { return (_availability == AvailabilityOnline); } + inline void setName(const char* name) + { _name = name; } + + inline const char* getName() const + { return _name; } + virtual void setAvailability(bool online); protected: @@ -43,12 +49,12 @@ class BaseDeviceType virtual void publishConfig(); virtual void publishAvailability(); - virtual bool isMyTopic(const char* topic, const char* expectedTopic); + virtual bool compareTopics(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; + const char* const _uniqueId; private: enum Availability { @@ -58,6 +64,7 @@ class BaseDeviceType }; Availability _availability; + const char* _name; friend class HAMqtt; }; diff --git a/src/device-types/DeviceTypeSerializer.cpp b/src/device-types/DeviceTypeSerializer.cpp index d6ac6d4b..dc3fc3a7 100644 --- a/src/device-types/DeviceTypeSerializer.cpp +++ b/src/device-types/DeviceTypeSerializer.cpp @@ -19,14 +19,27 @@ const char* DeviceTypeSerializer::Offline = "offline"; const char* DeviceTypeSerializer::StateOn = "ON"; const char* DeviceTypeSerializer::StateOff = "OFF"; +const char* DeviceTypeSerializer::getTopicPrefix(bool isDiscoveryTopic) +{ + if (!isDiscoveryTopic) { + const char* dataPrefix = HAMqtt::instance()->getDataPrefix(); + if (dataPrefix != nullptr) { + return dataPrefix; + } + } + + return HAMqtt::instance()->getDiscoveryPrefix(); +} + uint16_t DeviceTypeSerializer::calculateTopicLength( const char* component, const char* objectId, const char* suffix, - bool includeNullTerminator + bool includeNullTerminator, + bool isDiscoveryTopic ) { - const char* prefix = HAMqtt::instance()->getDiscoveryPrefix(); + const char* prefix = getTopicPrefix(isDiscoveryTopic); if (prefix == nullptr) { return 0; } @@ -58,10 +71,11 @@ uint16_t DeviceTypeSerializer::generateTopic( char* output, const char* component, const char* objectId, - const char* suffix + const char* suffix, + bool isDiscoveryTopic ) { - const char* prefix = HAMqtt::instance()->getDiscoveryPrefix(); + const char* prefix = getTopicPrefix(isDiscoveryTopic); if (prefix == nullptr) { return 0; } @@ -104,18 +118,18 @@ uint16_t DeviceTypeSerializer::calculateNameFieldSize(const char* name) } uint16_t DeviceTypeSerializer::calculateUniqueIdFieldSize( - const char* name + const char* uniqueId ) { HADevice const* device = HAMqtt::instance()->getDevice(); - if (device == nullptr || name == nullptr) { + if (device == nullptr || uniqueId == nullptr) { return 0; } - // Field format: ,"uniq_id":"[DEVICE ID]_[NAME]" + // Field format: ,"uniq_id":"[DEVICE ID]_[UNIQUE ID]" return ( strlen(device->getUniqueId()) + - strlen(name) + + strlen(uniqueId) + 14 // 14 - length of the JSON decorators for this field ); } @@ -136,7 +150,7 @@ uint16_t DeviceTypeSerializer::calculateAvailabilityFieldSize( const uint16_t& availabilityTopicLength = calculateTopicLength( (sharedAvailability ? nullptr : dt->componentName()), - (sharedAvailability ? nullptr : dt->name()), + (sharedAvailability ? nullptr : dt->uniqueId()), AvailabilityTopic, false ); @@ -212,10 +226,10 @@ void DeviceTypeSerializer::mqttWriteNameField(const char* name) } void DeviceTypeSerializer::mqttWriteUniqueIdField( - const char* name + const char* uniqueId ) { - if (name == nullptr) { + if (uniqueId == nullptr) { return; } @@ -226,13 +240,13 @@ void DeviceTypeSerializer::mqttWriteUniqueIdField( static const char Prefix[] PROGMEM = {",\"uniq_id\":\""}; - 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, device->getUniqueId()); + uint8_t uniqueIdLength = strlen(uniqueId) + strlen(device->getUniqueId()) + 2; // underscore + null temrinator + char finalUniqueId[uniqueIdLength]; + strcpy(finalUniqueId, uniqueId); + strcat_P(finalUniqueId, CharUnderscore); + strcat(finalUniqueId, device->getUniqueId()); - mqttWriteConstCharField(Prefix, uniqueId); + mqttWriteConstCharField(Prefix, finalUniqueId); } void DeviceTypeSerializer::mqttWriteAvailabilityField( @@ -251,7 +265,7 @@ void DeviceTypeSerializer::mqttWriteAvailabilityField( const uint16_t& topicSize = calculateTopicLength( (sharedAvailability ? nullptr : dt->componentName()), - (sharedAvailability ? nullptr : dt->name()), + (sharedAvailability ? nullptr : dt->uniqueId()), AvailabilityTopic ); if (topicSize == 0) { @@ -262,7 +276,7 @@ void DeviceTypeSerializer::mqttWriteAvailabilityField( generateTopic( availabilityTopic, (sharedAvailability ? nullptr : dt->componentName()), - (sharedAvailability ? nullptr : dt->name()), + (sharedAvailability ? nullptr : dt->uniqueId()), AvailabilityTopic ); @@ -316,7 +330,7 @@ bool DeviceTypeSerializer::mqttWriteTopicField( const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( dt->componentName(), - dt->name() , + dt->uniqueId() , topicSuffix ); if (topicSize == 0) { @@ -327,7 +341,7 @@ bool DeviceTypeSerializer::mqttWriteTopicField( DeviceTypeSerializer::generateTopic( topic, dt->componentName(), - dt->name(), + dt->uniqueId(), topicSuffix ); @@ -351,7 +365,7 @@ bool DeviceTypeSerializer::mqttPublishMessage( const uint16_t& topicSize = calculateTopicLength( dt->componentName(), - dt->name(), + dt->uniqueId(), topicSuffix ); if (topicSize == 0) { @@ -362,7 +376,7 @@ bool DeviceTypeSerializer::mqttPublishMessage( generateTopic( topic, dt->componentName(), - dt->name(), + dt->uniqueId(), topicSuffix ); @@ -384,7 +398,7 @@ bool DeviceTypeSerializer::mqttSubscribeTopic( { const uint16_t& topicSize = calculateTopicLength( dt->componentName(), - dt->name(), + dt->uniqueId(), topicSuffix ); if (topicSize == 0) { @@ -395,7 +409,7 @@ bool DeviceTypeSerializer::mqttSubscribeTopic( generateTopic( topic, dt->componentName(), - dt->name(), + dt->uniqueId(), topicSuffix ); diff --git a/src/device-types/DeviceTypeSerializer.h b/src/device-types/DeviceTypeSerializer.h index f9d66332..d269b458 100644 --- a/src/device-types/DeviceTypeSerializer.h +++ b/src/device-types/DeviceTypeSerializer.h @@ -19,38 +19,44 @@ class DeviceTypeSerializer static const char* StateOn; static const char* StateOff; + static const char* getTopicPrefix(bool isDiscoveryTopic); + /** * Calculates length of the topic with given parameters. - * Topic format: [discovery prefix]/[component]/[deviceId]/[objectId]/[suffix] + * Topic format: [prefix]/[component]/[deviceId]/[objectId]/[suffix] * * @param component * @param objectId * @param suffix * @param includeNullTerminator + * @param isDiscoveryTopic Determines which prefix will be used for topic. */ static uint16_t calculateTopicLength( const char* component, const char* objectId, const char* suffix, - bool includeNullTerminator = true + bool includeNullTerminator = true, + bool isDiscoveryTopic = false ); /** * 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]/[deviceId]/[objectId]/[suffix] + * Topic format: [prefix]/[component]/[deviceId]/[objectId]/[suffix] * * @param output * @param component * @param objectId * @param suffix * @param includeNullTerminator + * @param isDiscoveryTopic Determines which prefix will be used for topic. */ static uint16_t generateTopic( char* output, const char* component, const char* objectId, - const char* suffix + const char* suffix, + bool isDiscoveryTopic = false ); static uint16_t calculateBaseJsonDataSize(); @@ -58,7 +64,7 @@ class DeviceTypeSerializer const char* name ); static uint16_t calculateUniqueIdFieldSize( - const char* name + const char* uniqueId ); static uint16_t calculateAvailabilityFieldSize( const BaseDeviceType* const dt @@ -81,7 +87,7 @@ class DeviceTypeSerializer const char* name ); static void mqttWriteUniqueIdField( - const char* name + const char* uniqueId ); static void mqttWriteAvailabilityField( const BaseDeviceType* const dt diff --git a/src/device-types/HABinarySensor.cpp b/src/device-types/HABinarySensor.cpp index 6c3ceed8..ee7db85c 100644 --- a/src/device-types/HABinarySensor.cpp +++ b/src/device-types/HABinarySensor.cpp @@ -6,10 +6,10 @@ #include "../HADevice.h" HABinarySensor::HABinarySensor( - const char* name, + const char* uniqueId, bool initialState ) : - BaseDeviceType("binary_sensor", name), + BaseDeviceType("binary_sensor", uniqueId), _class(nullptr), _currentState(initialState) { @@ -17,21 +17,21 @@ HABinarySensor::HABinarySensor( } HABinarySensor::HABinarySensor( - const char* name, + const char* uniqueId, bool initialState, HAMqtt& mqtt ) : - HABinarySensor(name, initialState) + HABinarySensor(uniqueId, initialState) { (void)mqtt; } HABinarySensor::HABinarySensor( - const char* name, + const char* uniqueId, const char* deviceClass, bool initialState ) : - BaseDeviceType("binary_sensor", name), + BaseDeviceType("binary_sensor", uniqueId), _class(deviceClass), _currentState(initialState) { @@ -39,19 +39,19 @@ HABinarySensor::HABinarySensor( } HABinarySensor::HABinarySensor( - const char* name, + const char* uniqueId, const char* deviceClass, bool initialState, HAMqtt& mqtt ) : - HABinarySensor(name, deviceClass, initialState) + HABinarySensor(uniqueId, deviceClass, initialState) { (void)mqtt; } void HABinarySensor::onMqttConnected() { - if (strlen(name()) == 0) { + if (strlen(uniqueId()) == 0) { return; } @@ -76,13 +76,13 @@ bool HABinarySensor::setState(bool state) bool HABinarySensor::publishState(bool state) { - if (strlen(name()) == 0) { + if (strlen(uniqueId()) == 0) { return false; } const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), DeviceTypeSerializer::StateTopic ); if (topicSize == 0) { @@ -93,7 +93,7 @@ bool HABinarySensor::publishState(bool state) DeviceTypeSerializer::generateTopic( topic, componentName(), - name(), + uniqueId(), DeviceTypeSerializer::StateTopic ); @@ -127,8 +127,8 @@ uint16_t HABinarySensor::calculateSerializedLength( uint16_t size = 0; size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateNameFieldSize(name()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(name()); + size += DeviceTypeSerializer::calculateNameFieldSize(getName()); + size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); @@ -136,7 +136,7 @@ uint16_t HABinarySensor::calculateSerializedLength( { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), DeviceTypeSerializer::StateTopic, false ); @@ -182,8 +182,8 @@ bool HABinarySensor::writeSerializedData(const char* serializedDevice) const DeviceTypeSerializer::mqttWriteConstCharField(Prefix, _class); } - DeviceTypeSerializer::mqttWriteNameField(name()); - DeviceTypeSerializer::mqttWriteUniqueIdField(name()); + DeviceTypeSerializer::mqttWriteNameField(getName()); + DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); DeviceTypeSerializer::mqttWriteAvailabilityField(this); DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); DeviceTypeSerializer::mqttWriteEndJson(); diff --git a/src/device-types/HABinarySensor.h b/src/device-types/HABinarySensor.h index b85c0c33..c8865188 100644 --- a/src/device-types/HABinarySensor.h +++ b/src/device-types/HABinarySensor.h @@ -11,16 +11,16 @@ class HABinarySensor : public BaseDeviceType /** * Initializes binary sensor. * - * @param name Name of the sensor. Recommended characters: [a-z0-9\-_] + * @param uniqueId Unique ID 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, + const char* uniqueId, bool initialState ); HABinarySensor( - const char* name, + const char* uniqueId, bool initialState, HAMqtt& mqtt ); // legacy constructor @@ -29,18 +29,18 @@ class HABinarySensor : public BaseDeviceType * 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 uniqueId Unique ID of the sensor. Recommendes characters: [a-z0-9\-_] * @param deviceClass Name of the class (lower case). * @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* uniqueId, const char* deviceClass, bool initialState ); HABinarySensor( - const char* name, + const char* uniqueId, const char* deviceClass, bool initialState, HAMqtt& mqtt diff --git a/src/device-types/HACover.cpp b/src/device-types/HACover.cpp new file mode 100644 index 00000000..48d62704 --- /dev/null +++ b/src/device-types/HACover.cpp @@ -0,0 +1,286 @@ +#include "HACover.h" +#ifdef ARDUINOHA_COVER + +#include "../HAMqtt.h" +#include "../HADevice.h" + +static const char ClosedStateStr[] PROGMEM = {"closed"}; +static const char ClosingStateStr[] PROGMEM = {"closing"}; +static const char OpenStateStr[] PROGMEM = {"open"}; +static const char OpeningStateStr[] PROGMEM = {"opening"}; +static const char StoppedStateStr[] PROGMEM = {"stopped"}; +static const char OpenCommandStr[] PROGMEM = {"OPEN"}; +static const char CloseCommandStr[] PROGMEM = {"CLOSE"}; +static const char StopCommandStr[] PROGMEM = {"STOP"}; + +const char* HACover::PositionTopic = "ps"; + +HACover::HACover(const char* uniqueId) : + BaseDeviceType("cover", uniqueId), + _commandCallback(nullptr), + _currentState(StateUnknown), + _currentPosition(0), + _retain(false) +{ + +} + +HACover::HACover(const char* uniqueId, HAMqtt& mqtt) : + HACover(uniqueId) +{ + (void)mqtt; +} + +void HACover::onMqttConnected() +{ + if (strlen(uniqueId()) == 0) { + return; + } + + publishConfig(); + publishAvailability(); + + DeviceTypeSerializer::mqttSubscribeTopic( + this, + DeviceTypeSerializer::CommandTopic + ); + + if (!_retain) { + publishState(_currentState); + publishPosition(_currentPosition); + } +} + +void HACover::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t& length +) +{ + (void)payload; + + if (compareTopics(topic, DeviceTypeSerializer::CommandTopic)) { + char cmd[length + 1]; + memset(cmd, 0, sizeof(cmd)); + memcpy(cmd, payload, length); + handleCommand(cmd); + } +} + +bool HACover::setState(CoverState state, bool force) +{ + if (!force && _currentState == state) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +bool HACover::setPosition(int16_t position) +{ + if (_currentPosition == position) { + return true; + } + + if (publishPosition(position)) { + _currentPosition = position; + return true; + } + + return false; +} + +bool HACover::publishState(CoverState state) +{ + if (strlen(uniqueId()) == 0 || state == StateUnknown) { + return false; + } + + char stateStr[8]; + switch (state) { + case StateClosed: + strcpy_P(stateStr, ClosedStateStr); + break; + + case StateClosing: + strcpy_P(stateStr, ClosingStateStr); + break; + + case StateOpen: + strcpy_P(stateStr, OpenStateStr); + break; + + case StateOpening: + strcpy_P(stateStr, OpeningStateStr); + break; + + case StateStopped: + strcpy_P(stateStr, StoppedStateStr); + break; + + default: + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + DeviceTypeSerializer::StateTopic, + stateStr + ); +} + +bool HACover::publishPosition(int16_t position) +{ + if (strlen(uniqueId()) == 0) { + return false; + } + + uint8_t digitsNb = floor(log10(position)) + 1; + char str[digitsNb + 2]; // + null terminator + negative sign + memset(str, 0, sizeof(str)); + itoa(position, str, 10); + + return DeviceTypeSerializer::mqttPublishMessage( + this, + PositionTopic, + str + ); +} + +uint16_t HACover::calculateSerializedLength(const char* serializedDevice) const +{ + if (serializedDevice == nullptr) { + return 0; + } + + uint16_t size = 0; + size += DeviceTypeSerializer::calculateBaseJsonDataSize(); + size += DeviceTypeSerializer::calculateNameFieldSize(getName()); + size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); + size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); + size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); + size += DeviceTypeSerializer::calculateRetainFieldSize(_retain); + + // command topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + uniqueId(), + 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(), + uniqueId(), + 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 + } + + // position topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + uniqueId(), + PositionTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"pos_t":"[TOPIC]" + size += topicLength + 11; // 11 - length of the JSON decorators for this field + } + + return size; // exludes null terminator +} + +bool HACover::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 + ); + } + + // position topic + { + static const char Prefix[] PROGMEM = {",\"pos_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + PositionTopic + ); + } + + DeviceTypeSerializer::mqttWriteNameField(getName()); + DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteRetainField(_retain); + DeviceTypeSerializer::mqttWriteEndJson(); + + return true; +} + +void HACover::handleCommand(const char* cmd) +{ + if (!_commandCallback) { + return; + } + + if (strcmp_P(cmd, CloseCommandStr) == 0) { + _commandCallback(CommandClose); + } else if (strcmp_P(cmd, OpenCommandStr) == 0) { + _commandCallback(CommandOpen); + } else if (strcmp_P(cmd, StopCommandStr) == 0) { + _commandCallback(CommandStop); + } +} + +#endif diff --git a/src/device-types/HACover.h b/src/device-types/HACover.h new file mode 100644 index 00000000..056da73b --- /dev/null +++ b/src/device-types/HACover.h @@ -0,0 +1,113 @@ +#ifndef AHA_COVER_H +#define AHA_COVER_H + +#include "BaseDeviceType.h" + +#ifdef ARDUINOHA_COVER + +#define HACOVER_CALLBACK(name) void (*name)(CoverCommand cmd) + +class HACover : public BaseDeviceType +{ +public: + static const char* PositionTopic; + + enum CoverState { + StateUnknown = 0, + StateClosed, + StateClosing, + StateOpen, + StateOpening, + StateStopped + }; + + enum CoverCommand { + CommandOpen, + CommandClose, + CommandStop + }; + + HACover(const char* uniqueId); + HACover(const char* uniqueId, 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 cover 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 cover. + * @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(CoverState state, bool force = false); + + /** + * Sets current state of the cover without pushing the state to Home Assistant. + * This method may be useful if you want to change state before connection + * with MQTT broker is acquired. + * + * @param state New state of the cover. + */ + inline void setCurrentState(CoverState state) + { _currentState = state; } + + /** + * Changes position of the cover 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 position New position of the cover. + * @returns Returns true if MQTT message has been published successfully. + */ + bool setPosition(int16_t position); + + /** + * Sets current position of the cover without pushing the value to Home Assistant. + * This method may be useful if you want to change position before connection + * with MQTT broker is acquired. + * + * @param position New position of the cover. + */ + inline void setCurrentPosition(int16_t position) + { _currentPosition = position; } + + /** + * 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; } + + /** + * Registers callback that will be called each time the command from HA is received. + * Please note that it's not possible to register multiple callbacks for the same covers. + * + * @param callback + */ + inline void onCommand(HACOVER_CALLBACK(callback)) + { _commandCallback = callback; } + +protected: + bool publishState(CoverState state); + bool publishPosition(int16_t position); + uint16_t calculateSerializedLength(const char* serializedDevice) const override; + bool writeSerializedData(const char* serializedDevice) const override; + void handleCommand(const char* cmd); + + HACOVER_CALLBACK(_commandCallback); + CoverState _currentState; + int16_t _currentPosition; + bool _retain; +}; + +#endif +#endif diff --git a/src/device-types/HAFan.cpp b/src/device-types/HAFan.cpp index 3d4f8684..47995e75 100644 --- a/src/device-types/HAFan.cpp +++ b/src/device-types/HAFan.cpp @@ -4,24 +4,19 @@ #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"}; +const char* HAFan::PercentageCommandTopic = {"sct"}; +const char* HAFan::PercentageStateTopic = {"sst"}; HAFan::HAFan(const char* uniqueId, uint8_t features) : BaseDeviceType("fan", uniqueId), _features(features), - _speeds(OffSpeed | LowSpeed | MediumSpeed | HighSpeed), _currentState(false), _stateCallback(nullptr), - _currentSpeed(OffSpeed), + _currentSpeed(0), _speedCallback(nullptr), - _label(nullptr), - _retain(false) + _retain(false), + _speedRangeMin(1), + _speedRangeMax(100) { } @@ -34,7 +29,7 @@ HAFan::HAFan(const char* uniqueId, uint8_t features, HAMqtt& mqtt) : void HAFan::onMqttConnected() { - if (strlen(name()) == 0) { + if (strlen(uniqueId()) == 0) { return; } @@ -49,7 +44,7 @@ void HAFan::onMqttConnected() if (_features & SpeedsFeature) { DeviceTypeSerializer::mqttSubscribeTopic( this, - SpeedCommandTopic + PercentageCommandTopic ); } @@ -67,15 +62,17 @@ void HAFan::onMqttMessage( { (void)payload; - if (isMyTopic(topic, DeviceTypeSerializer::CommandTopic)) { + if (compareTopics(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); + } else if (compareTopics(topic, PercentageCommandTopic)) { + char speedStr[length + 1]; + memset(speedStr, 0, sizeof(speedStr)); + memcpy(speedStr, payload, length); + int32_t speed = atoi(speedStr); + if (speed >= 0) { + setSpeed(speed); + } } } @@ -98,12 +95,8 @@ bool HAFan::setState(bool state, bool force) return false; } -bool HAFan::setSpeed(Speed speed) +bool HAFan::setSpeed(uint16_t speed) { - if (speed == UnknownSpeed) { - return false; - } - if (publishSpeed(speed)) { _currentSpeed = speed; @@ -117,24 +110,9 @@ bool HAFan::setSpeed(Speed speed) 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) { + if (strlen(uniqueId()) == 0) { return false; } @@ -149,38 +127,21 @@ bool HAFan::publishState(bool state) ); } -bool HAFan::publishSpeed(Speed speed) +bool HAFan::publishSpeed(uint16_t speed) { - if (strlen(name()) == 0 || speed == UnknownSpeed) { + if (strlen(uniqueId()) == 0) { 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; - } + uint8_t digitsNb = floor(log10(speed)) + 1; + char str[digitsNb + 1]; // + null terminator + memset(str, 0, sizeof(str)); + itoa(speed, str, 10); return DeviceTypeSerializer::mqttPublishMessage( this, - SpeedStateTopic, - speedStr + PercentageStateTopic, + str ); } @@ -192,17 +153,17 @@ uint16_t HAFan::calculateSerializedLength(const char* serializedDevice) const uint16_t size = 0; size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(name()); + size += DeviceTypeSerializer::calculateNameFieldSize(getName()); + size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); 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(), + uniqueId(), DeviceTypeSerializer::CommandTopic, false ); @@ -219,7 +180,7 @@ uint16_t HAFan::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), DeviceTypeSerializer::StateTopic, false ); @@ -234,12 +195,12 @@ uint16_t HAFan::calculateSerializedLength(const char* serializedDevice) const // speeds if (_features & SpeedsFeature) { - // command topic + // percentage command topic { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), - SpeedCommandTopic, + uniqueId(), + PercentageCommandTopic, false ); @@ -247,16 +208,16 @@ uint16_t HAFan::calculateSerializedLength(const char* serializedDevice) const return 0; } - // Field format: ,"spd_cmd_t":"[TOPIC]" + // Field format: ,"pct_cmd_t":"[TOPIC]" size += topicLength + 15; // 15 - length of the JSON decorators for this field } - // state topic + // percentage state topic { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), - SpeedStateTopic, + uniqueId(), + PercentageStateTopic, false ); @@ -264,47 +225,28 @@ uint16_t HAFan::calculateSerializedLength(const char* serializedDevice) const return 0; } - // Field format: ,"spd_stat_t":"[TOPIC]" + // Field format: ,"pct_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 + // speed range min + if (_speedRangeMin != 1) { + uint8_t digitsNb = floor(log10(_speedRangeMin)) + 1; - if (_speeds & OffSpeed) { - // escape + comma - length += 3 + strlen_P(OffStr); - } - - if (_speeds & LowSpeed) { - // escape + comma - length += 3 + strlen_P(LowStr); - } + // Field format: ,"spd_rng_min":[VALUE] + size += digitsNb + 15; // 15 - length of the JSON decorators for this field + } - if (_speeds & MediumSpeed) { - // escape + comma - length += 3 + strlen_P(MediumStr); - } + // speed range max + if (_speedRangeMax != 100) { + uint8_t digitsNb = floor(log10(_speedRangeMax)) + 1; - if (_speeds & HighSpeed) { - // escape + comma - length += 3 + strlen_P(HighStr); - } - - if (length > 2) { - length--; // remove trailing comma + // Field format: ,"spd_rng_max":[VALUE] + size += digitsNb + 15; // 15 - length of the JSON decorators for this field + } } - return length; // excludes null terminator + return size; // excludes null terminator } bool HAFan::writeSerializedData(const char* serializedDevice) const @@ -337,79 +279,62 @@ bool HAFan::writeSerializedData(const char* serializedDevice) const // speeds if (_features & SpeedsFeature) { - // command topic + // percentage command topic { - static const char Prefix[] PROGMEM = {",\"spd_cmd_t\":\""}; + static const char Prefix[] PROGMEM = {",\"pct_cmd_t\":\""}; DeviceTypeSerializer::mqttWriteTopicField( this, Prefix, - SpeedCommandTopic + PercentageCommandTopic ); } - // state topic + // percentage state topic { - static const char Prefix[] PROGMEM = {",\"spd_stat_t\":\""}; + static const char Prefix[] PROGMEM = {",\"pct_stat_t\":\""}; DeviceTypeSerializer::mqttWriteTopicField( this, Prefix, - SpeedStateTopic + PercentageStateTopic ); } - // 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, "]"); - } + // speed range min + if (_speedRangeMin != 1) { + uint8_t digitsNb = floor(log10(_speedRangeMin)) + 1; + char str[digitsNb + 1]; // + null terminator + memset(str, 0, sizeof(str)); + itoa(_speedRangeMin, str, 10); + static const char Prefix[] PROGMEM = {",\"spd_rng_min\":"}; DeviceTypeSerializer::mqttWriteConstCharField( Prefix, - speedStr, + str, + false + ); + } + + // speed range max + if (_speedRangeMax != 100) { + uint8_t digitsNb = floor(log10(_speedRangeMax)) + 1; + char str[digitsNb + 1]; // + null terminator + memset(str, 0, sizeof(str)); + itoa(_speedRangeMax, str, 10); + + static const char Prefix[] PROGMEM = {",\"spd_rng_max\":"}; + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + str, false ); } } - DeviceTypeSerializer::mqttWriteRetainField(_retain); - DeviceTypeSerializer::mqttWriteNameField(_label); - DeviceTypeSerializer::mqttWriteUniqueIdField(name()); - DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteNameField(getName()); + DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteRetainField(_retain); DeviceTypeSerializer::mqttWriteEndJson(); return true; diff --git a/src/device-types/HAFan.h b/src/device-types/HAFan.h index b5f29577..76685ea4 100644 --- a/src/device-types/HAFan.h +++ b/src/device-types/HAFan.h @@ -6,19 +6,21 @@ #ifdef ARDUINOHA_FAN #define HAFAN_STATE_CALLBACK_BOOL(name) void (*name)(bool) -#define HAFAN_STATE_CALLBACK_SPEED(name) void (*name)(Speed) +#define HAFAN_STATE_CALLBACK_SPEED(name) void (*name)(uint16_t) +#define HAFAN_STATE_CALLBACK_SPEED_DEPRECATED(name) void (*name)(Speed) class HAFan : public BaseDeviceType { public: - static const char* SpeedCommandTopic; - static const char* SpeedStateTopic; + static const char* PercentageCommandTopic; + static const char* PercentageStateTopic; enum Features { DefaultFeatures = 0, SpeedsFeature = 1 }; + // @deprecated enum Speed { UnknownSpeed = 0, OffSpeed = 1, @@ -81,27 +83,20 @@ class HAFan : public BaseDeviceType * * @param speeds */ - inline void setSpeeds(uint8_t speeds) - { _speeds = speeds; } + AHA_DEPRECATED(inline void setSpeeds(uint8_t speeds)) + { (void)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); + bool setSpeed(uint16_t speed); /** * Returns current speed of the fan. */ - inline Speed getSpeed() const + inline uint16_t getSpeed() const { return _currentSpeed; } /** @@ -113,13 +108,8 @@ class HAFan : public BaseDeviceType 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 + AHA_DEPRECATED(inline void onSpeedChanged(HAFAN_STATE_CALLBACK_SPEED_DEPRECATED(callback))) + { (void)callback; } /** * Sets `retain` flag for commands published by Home Assistant. @@ -130,21 +120,36 @@ class HAFan : public BaseDeviceType inline void setRetain(bool retain) { _retain = retain; } + /** + * Sets minimum range for slider in the HA panel. + * + * @param min + */ + inline void setSpeedRangeMin(uint16_t min) + { _speedRangeMin = min; } + + /** + * Sets maximum range for slider in the HA panel. + * + * @param min + */ + inline void setSpeedRangeMax(uint16_t max) + { _speedRangeMax = max; } + protected: bool publishState(bool state); - bool publishSpeed(Speed speed); + bool publishSpeed(uint16_t 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; + uint16_t _currentSpeed; HAFAN_STATE_CALLBACK_SPEED(_speedCallback); - const char* _label; bool _retain; + uint16_t _speedRangeMin; + uint16_t _speedRangeMax; }; #endif diff --git a/src/device-types/HAHVAC.cpp b/src/device-types/HAHVAC.cpp index ea2a2c6d..aadc25d8 100644 --- a/src/device-types/HAHVAC.cpp +++ b/src/device-types/HAHVAC.cpp @@ -44,7 +44,6 @@ HAHVAC::HAHVAC(const char* uniqueId, uint8_t features) : _tempStep(1), _targetTempCallback(nullptr), _targetTemperature(__DBL_MAX__), - _label(nullptr), _modes( OffMode | AutoMode | @@ -72,7 +71,7 @@ HAHVAC::HAHVAC( void HAHVAC::onMqttConnected() { - if (strlen(name()) == 0) { + if (strlen(uniqueId()) == 0) { return; } @@ -98,24 +97,24 @@ void HAHVAC::onMqttMessage( ) { if ((_features & AuxHeatingFeature) && - isMyTopic(topic, AuxCommandTopic)) { + compareTopics(topic, AuxCommandTopic)) { bool state = (length == strlen(DeviceTypeSerializer::StateOn)); setAuxHeatingState(state); } else if ((_features & AwayModeFeature) && - isMyTopic(topic, AwayCommandTopic)) { + compareTopics(topic, AwayCommandTopic)) { bool state = (length == strlen(DeviceTypeSerializer::StateOn)); setAwayState(state); } else if ((_features & HoldFeature) && - isMyTopic(topic, HoldCommandTopic)) { + compareTopics(topic, HoldCommandTopic)) { bool state = (length == strlen(DeviceTypeSerializer::StateOn)); setHoldState(state); - } else if (isMyTopic(topic, TargetTemperatureCommandTopic)) { + } else if (compareTopics(topic, TargetTemperatureCommandTopic)) { char src[length + 1]; memset(src, 0, sizeof(src)); memcpy(src, payload, length); setTargetTemperature(HAUtils::strToTemp(src)); - } else if (isMyTopic(topic, ModeCommandTopic)) { + } else if (compareTopics(topic, ModeCommandTopic)) { char mode[length + 1]; memset(mode, 0, sizeof(mode)); memcpy(mode, payload, length); @@ -279,7 +278,7 @@ bool HAHVAC::setTempStep(double tempStep) bool HAHVAC::publishAction(Action action) { if (!(_features & ActionFeature) || - strlen(name()) == 0) { + strlen(uniqueId()) == 0) { return false; } @@ -323,7 +322,7 @@ bool HAHVAC::publishAction(Action action) bool HAHVAC::publishAuxHeatingState(bool state) { if (!(_features & AuxHeatingFeature) || - strlen(name()) == 0) { + strlen(uniqueId()) == 0) { return false; } @@ -341,7 +340,7 @@ bool HAHVAC::publishAuxHeatingState(bool state) bool HAHVAC::publishAwayState(bool state) { if (!(_features & AwayModeFeature) || - strlen(name()) == 0) { + strlen(uniqueId()) == 0) { return false; } @@ -359,7 +358,7 @@ bool HAHVAC::publishAwayState(bool state) bool HAHVAC::publishHoldState(bool state) { if (!(_features & HoldFeature) || - strlen(name()) == 0) { + strlen(uniqueId()) == 0) { return false; } @@ -376,7 +375,7 @@ bool HAHVAC::publishHoldState(bool state) bool HAHVAC::publishCurrentTemperature(double temperature) { - if (strlen(name()) == 0) { + if (strlen(uniqueId()) == 0) { return false; } @@ -396,7 +395,7 @@ bool HAHVAC::publishCurrentTemperature(double temperature) bool HAHVAC::publishTargetTemperature(double temperature) { - if (strlen(name()) == 0) { + if (strlen(uniqueId()) == 0) { return false; } @@ -416,7 +415,7 @@ bool HAHVAC::publishTargetTemperature(double temperature) bool HAHVAC::publishMode(Mode mode) { - if (strlen(name()) == 0 || mode == UnknownMode) { + if (strlen(uniqueId()) == 0 || mode == UnknownMode) { return false; } @@ -509,17 +508,17 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const uint16_t size = 0; size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(_uniqueId); + size += DeviceTypeSerializer::calculateNameFieldSize(getName()); + 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(), + uniqueId(), CurrentTemperatureTopic, false ); @@ -536,7 +535,7 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const if (_features & ActionFeature) { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), ActionTopic, false ); @@ -555,7 +554,7 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), AuxCommandTopic, false ); @@ -572,7 +571,7 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), AuxStateTopic, false ); @@ -592,7 +591,7 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), AwayCommandTopic, false ); @@ -609,7 +608,7 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), AwayStateTopic, false ); @@ -629,7 +628,7 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), HoldCommandTopic, false ); @@ -646,7 +645,7 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), HoldStateTopic, false ); @@ -693,7 +692,7 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), TargetTemperatureCommandTopic, false ); @@ -710,7 +709,7 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), TargetTemperatureStateTopic, false ); @@ -737,7 +736,7 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), ModeCommandTopic, false ); @@ -754,7 +753,7 @@ uint16_t HAHVAC::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), ModeStateTopic, false ); @@ -1066,11 +1065,11 @@ bool HAHVAC::writeSerializedData(const char* serializedDevice) const ); } - DeviceTypeSerializer::mqttWriteRetainField(_retain); - DeviceTypeSerializer::mqttWriteNameField(_label); - DeviceTypeSerializer::mqttWriteUniqueIdField(_uniqueId); - DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteNameField(getName()); + DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteRetainField(_retain); DeviceTypeSerializer::mqttWriteEndJson(); return true; diff --git a/src/device-types/HAHVAC.h b/src/device-types/HAHVAC.h index a9ea3519..44c4ecb1 100644 --- a/src/device-types/HAHVAC.h +++ b/src/device-types/HAHVAC.h @@ -258,14 +258,6 @@ class HAHVAC : public BaseDeviceType 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)` @@ -334,7 +326,6 @@ class HAHVAC : public BaseDeviceType double _tempStep; HAHVAC_STATE_CALLBACK_DOUBLE(_targetTempCallback); double _targetTemperature; - const char* _label; uint8_t _modes; HAHVAC_STATE_CALLBACK_MODE(_modeChangedCallback); Mode _currentMode; diff --git a/src/device-types/HASensor.cpp b/src/device-types/HASensor.cpp index f67f358f..cc75f3cc 100644 --- a/src/device-types/HASensor.cpp +++ b/src/device-types/HASensor.cpp @@ -8,8 +8,8 @@ #include "../HAMqtt.h" #include "../HADevice.h" -HASensor::HASensor(const char* name) : - BaseDeviceType("sensor", name), +HASensor::HASensor(const char* uniqueId) : + BaseDeviceType("sensor", uniqueId), _class(nullptr), _units(nullptr), _icon(nullptr) @@ -17,15 +17,15 @@ HASensor::HASensor(const char* name) : } -HASensor::HASensor(const char* name, HAMqtt& mqtt) : - HASensor(name) +HASensor::HASensor(const char* uniqueId, HAMqtt& mqtt) : + HASensor(uniqueId) { (void)mqtt; } void HASensor::onMqttConnected() { - if (strlen(name()) == 0) { + if (strlen(uniqueId()) == 0) { return; } @@ -78,7 +78,7 @@ bool HASensor::setValue(float value, uint8_t precision) bool HASensor::publishValue(const char* value) { - if (strlen(name()) == 0 || value == nullptr) { + if (strlen(uniqueId()) == 0 || value == nullptr) { return false; } @@ -88,7 +88,7 @@ bool HASensor::publishValue(const char* value) const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), DeviceTypeSerializer::StateTopic ); if (topicSize == 0) { @@ -99,7 +99,7 @@ bool HASensor::publishValue(const char* value) DeviceTypeSerializer::generateTopic( topic, componentName(), - name(), + uniqueId(), DeviceTypeSerializer::StateTopic ); @@ -125,15 +125,15 @@ uint16_t HASensor::calculateSerializedLength( uint16_t size = 0; size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateNameFieldSize(name()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(name()); + size += DeviceTypeSerializer::calculateNameFieldSize(getName()); + size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); { const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), DeviceTypeSerializer::StateTopic, false ); @@ -206,8 +206,8 @@ bool HASensor::writeSerializedData(const char* serializedDevice) const ); } - DeviceTypeSerializer::mqttWriteNameField(name()); - DeviceTypeSerializer::mqttWriteUniqueIdField(name()); + DeviceTypeSerializer::mqttWriteNameField(getName()); + DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); DeviceTypeSerializer::mqttWriteAvailabilityField(this); DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); DeviceTypeSerializer::mqttWriteEndJson(); diff --git a/src/device-types/HASensor.h b/src/device-types/HASensor.h index db6840f7..908dc72c 100644 --- a/src/device-types/HASensor.h +++ b/src/device-types/HASensor.h @@ -11,13 +11,13 @@ class HASensor : public BaseDeviceType /** * Initializes binary sensor. * - * @param name Name of the sensor. Recommendes characters: [a-z0-9\-_] + * @param uniqueId Unique ID of the sensor. Recommendes characters: [a-z0-9\-_] */ HASensor( - const char* name + const char* uniqueId ); HASensor( - const char* name, + const char* uniqueId, HAMqtt& mqtt ); // legacy constructor diff --git a/src/device-types/HASwitch.cpp b/src/device-types/HASwitch.cpp index 6c58aa8b..e62b91a2 100644 --- a/src/device-types/HASwitch.cpp +++ b/src/device-types/HASwitch.cpp @@ -5,9 +5,10 @@ #include "../HAMqtt.h" #include "../HADevice.h" -HASwitch::HASwitch(const char* name, bool initialState) : - BaseDeviceType("switch", name), +HASwitch::HASwitch(const char* uniqueId, bool initialState) : + BaseDeviceType("switch", uniqueId), _stateCallback(nullptr), + _beforeStateCallback(nullptr), _currentState(initialState), _icon(nullptr), _retain(false) @@ -16,18 +17,18 @@ HASwitch::HASwitch(const char* name, bool initialState) : } HASwitch::HASwitch( - const char* name, + const char* uniqueId, bool initialState, HAMqtt& mqtt ) : - HASwitch(name, initialState) + HASwitch(uniqueId, initialState) { (void)mqtt; } void HASwitch::onMqttConnected() { - if (strlen(name()) == 0) { + if (strlen(uniqueId()) == 0) { return; } @@ -52,7 +53,7 @@ void HASwitch::onMqttMessage( { (void)payload; - if (isMyTopic(topic, DeviceTypeSerializer::CommandTopic)) { + if (compareTopics(topic, DeviceTypeSerializer::CommandTopic)) { bool state = (length == strlen(DeviceTypeSerializer::StateOn)); setState(state, true); } @@ -64,11 +65,15 @@ bool HASwitch::setState(bool state, bool force) return true; } + if (_beforeStateCallback) { + _beforeStateCallback(state, this); + } + if (publishState(state)) { _currentState = state; if (_stateCallback) { - _stateCallback(state, this); + _stateCallback(_currentState, this); } return true; @@ -79,7 +84,7 @@ bool HASwitch::setState(bool state, bool force) bool HASwitch::publishState(bool state) { - if (strlen(name()) == 0) { + if (strlen(uniqueId()) == 0) { return false; } @@ -107,8 +112,8 @@ uint16_t HASwitch::calculateSerializedLength(const char* serializedDevice) const uint16_t size = 0; size += DeviceTypeSerializer::calculateBaseJsonDataSize(); - size += DeviceTypeSerializer::calculateNameFieldSize(name()); - size += DeviceTypeSerializer::calculateUniqueIdFieldSize(name()); + size += DeviceTypeSerializer::calculateNameFieldSize(getName()); + size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); size += DeviceTypeSerializer::calculateRetainFieldSize(_retain); @@ -117,7 +122,7 @@ uint16_t HASwitch::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), DeviceTypeSerializer::CommandTopic, false ); @@ -134,7 +139,7 @@ uint16_t HASwitch::calculateSerializedLength(const char* serializedDevice) const { const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), DeviceTypeSerializer::StateTopic, false ); @@ -194,8 +199,8 @@ bool HASwitch::writeSerializedData(const char* serializedDevice) const } DeviceTypeSerializer::mqttWriteRetainField(_retain); - DeviceTypeSerializer::mqttWriteNameField(name()); - DeviceTypeSerializer::mqttWriteUniqueIdField(name()); + DeviceTypeSerializer::mqttWriteNameField(getName()); + DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); DeviceTypeSerializer::mqttWriteAvailabilityField(this); DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); DeviceTypeSerializer::mqttWriteEndJson(); diff --git a/src/device-types/HASwitch.h b/src/device-types/HASwitch.h index 5489cbee..04a18f15 100644 --- a/src/device-types/HASwitch.h +++ b/src/device-types/HASwitch.h @@ -13,16 +13,16 @@ class HASwitch : public BaseDeviceType /** * Initializes switch. * - * @param name Name of the switch. Recommendes characters: [a-z0-9\-_] + * @param uniqueId Unique ID of the switch. Recommendes characters: [a-z0-9\-_] * @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, + const char* uniqueId, bool initialState ); HASwitch( - const char* name, + const char* uniqueId, bool initialState, HAMqtt& mqtt ); // legacy constructor @@ -42,12 +42,6 @@ class HASwitch : public BaseDeviceType const uint16_t& length ) override; - /** - * Returns name of the switch assigned via constructor. - */ - inline const char* getName() const - { return _name; } - /** * Changes state of the switch and publishes MQTT message. * Please note that if a new value is the same as previous one, @@ -79,7 +73,7 @@ class HASwitch : public BaseDeviceType { return _currentState; } /** - * Registers callback that will be called each time the value of the switch changes. + * Registers callback that will be called each time the state of the switch changes. * Please note that it's not possible to register multiple callbacks for the same switch. * * @param callback @@ -87,6 +81,15 @@ class HASwitch : public BaseDeviceType inline void onStateChanged(HASWITCH_CALLBACK(callback)) { _stateCallback = callback; } + /** + * Registers callback that will be called before state of the switch changes. + * The state passed to callback is a new state that is going to be set. + * + * @param callback + */ + inline void onBeforeStateChanged(HASWITCH_CALLBACK(callback)) + { _beforeStateCallback = callback; } + /** * Sets icon of the switch, e.g. `mdi:home`. * @@ -110,6 +113,7 @@ class HASwitch : public BaseDeviceType bool writeSerializedData(const char* serializedDevice) const override; HASWITCH_CALLBACK(_stateCallback); + HASWITCH_CALLBACK(_beforeStateCallback); bool _currentState; const char* _icon; bool _retain; diff --git a/src/device-types/HATagScanner.cpp b/src/device-types/HATagScanner.cpp index 26de9a53..1edafa7a 100644 --- a/src/device-types/HATagScanner.cpp +++ b/src/device-types/HATagScanner.cpp @@ -5,21 +5,21 @@ #include "../HAMqtt.h" #include "../HADevice.h" -HATagScanner::HATagScanner(const char* name) : - BaseDeviceType("tag", name) +HATagScanner::HATagScanner(const char* uniqueId) : + BaseDeviceType("tag", uniqueId) { } -HATagScanner::HATagScanner(const char* name, HAMqtt& mqtt) : - HATagScanner(name) +HATagScanner::HATagScanner(const char* uniqueId, HAMqtt& mqtt) : + HATagScanner(uniqueId) { (void)mqtt; } void HATagScanner::onMqttConnected() { - if (strlen(name()) == 0) { + if (strlen(uniqueId()) == 0) { return; } @@ -28,13 +28,13 @@ void HATagScanner::onMqttConnected() bool HATagScanner::tagScanned(const char* tag) { - if (tag == nullptr || strlen(tag) == 0 || strlen(name()) == 0) { + if (tag == nullptr || strlen(tag) == 0 || strlen(uniqueId()) == 0) { return false; } const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), DeviceTypeSerializer::EventTopic ); if (topicSize == 0) { @@ -45,7 +45,7 @@ bool HATagScanner::tagScanned(const char* tag) DeviceTypeSerializer::generateTopic( topic, componentName(), - name(), + uniqueId(), DeviceTypeSerializer::EventTopic ); @@ -72,7 +72,7 @@ uint16_t HATagScanner::calculateSerializedLength( { const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( componentName(), - name(), + uniqueId(), DeviceTypeSerializer::EventTopic, false ); diff --git a/src/device-types/HATagScanner.h b/src/device-types/HATagScanner.h index ac193021..2b26d1ac 100644 --- a/src/device-types/HATagScanner.h +++ b/src/device-types/HATagScanner.h @@ -11,10 +11,10 @@ class HATagScanner : public BaseDeviceType /** * Initializes tag scanner with the given name. * - * @param name Name of the scanner. Recommendes characters: [a-z0-9\-_] + * @param uniqueId Unique ID of the scanner. Recommendes characters: [a-z0-9\-_] */ - HATagScanner(const char* name); - HATagScanner(const char* name, HAMqtt& mqtt); // legacy constructor + HATagScanner(const char* uniqueId); + HATagScanner(const char* uniqueId, HAMqtt& mqtt); // legacy constructor /** * Publishes configuration of the sensor to the MQTT. diff --git a/src/device-types/HATriggers.cpp b/src/device-types/HATriggers.cpp index 1bff911a..62b1ae39 100644 --- a/src/device-types/HATriggers.cpp +++ b/src/device-types/HATriggers.cpp @@ -132,7 +132,9 @@ void HATriggers::publishConfig() const uint16_t& topicLength = calculateTopicLength( componentName(), trigger, - DeviceTypeSerializer::ConfigTopic + DeviceTypeSerializer::ConfigTopic, + true, + true ); const uint16_t& dataLength = calculateSerializedLength( trigger, @@ -147,7 +149,8 @@ void HATriggers::publishConfig() topic, componentName(), trigger, - DeviceTypeSerializer::ConfigTopic + DeviceTypeSerializer::ConfigTopic, + true ); if (strlen(topic) == 0) { @@ -165,15 +168,17 @@ uint16_t HATriggers::calculateTopicLength( const char* component, const HATrigger *trigger, const char* suffix, - bool includeNullTerminator + bool includeNullTerminator, + bool isDiscoveryTopic ) const { - uint8_t length = strlen(trigger->type) + strlen(trigger->subtype) + 2; // + underscore and slash + uint8_t length = strlen(trigger->type) + strlen(trigger->subtype) + 2; // underscore and slash return DeviceTypeSerializer::calculateTopicLength( component, nullptr, suffix, - includeNullTerminator + includeNullTerminator, + isDiscoveryTopic ) + length; } @@ -181,11 +186,12 @@ uint16_t HATriggers::generateTopic( char* output, const char* component, const HATrigger *trigger, - const char* suffix + const char* suffix, + bool isDiscoveryTopic ) const { static const char Underscore[] PROGMEM = {"_"}; - uint8_t length = strlen(trigger->type) + strlen(trigger->subtype) + 2; // slash + null terminator + uint8_t length = strlen(trigger->type) + strlen(trigger->subtype) + 2; // underscore + null terminator char objectId[length]; strcpy(objectId, trigger->subtype); @@ -196,7 +202,8 @@ uint16_t HATriggers::generateTopic( output, component, objectId, - suffix + suffix, + isDiscoveryTopic ); } diff --git a/src/device-types/HATriggers.h b/src/device-types/HATriggers.h index 47bd0986..5732e47a 100644 --- a/src/device-types/HATriggers.h +++ b/src/device-types/HATriggers.h @@ -42,14 +42,16 @@ class HATriggers : public BaseDeviceType const char* component, const HATrigger *trigger, const char* suffix, - bool includeNullTerminator = true + bool includeNullTerminator = true, + bool isDiscoveryTopic = false ) const; uint16_t generateTopic( char* output, const char* component, const HATrigger *trigger, - const char* suffix + const char* suffix, + bool isDiscoveryTopic = false ) const; private: