diff --git a/lib/arduino-home-assistant-main/CHANGELOG.md b/lib/arduino-home-assistant-main/CHANGELOG.md new file mode 100644 index 0000000..4573d10 --- /dev/null +++ b/lib/arduino-home-assistant-main/CHANGELOG.md @@ -0,0 +1,37 @@ +# 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](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](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 [Advanced Availability example](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/lib/arduino-home-assistant-main/README.md b/lib/arduino-home-assistant-main/README.md new file mode 100644 index 0000000..9957e95 --- /dev/null +++ b/lib/arduino-home-assistant-main/README.md @@ -0,0 +1,85 @@ +# Arduino Home Assistant integration + +ArduinoHA allows to integrate an Arduino/ESP based device with Home Assistant using MQTT. +The library is designed to use as little resources (RAM/flash) as possible. +Initially it was optimized to work on Arduino Uno with Ethernet Shield, +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 +* 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) +* [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 advanced](examples/mqtt-advanced/mqtt-advanced.ino) + +## Tested boards + +* Arduino Uno +* Arduino Mega +* NodeMCU +* ESP-01 +* Generic ESP8266/ESP8255 +* Arduino Nano 33 IoT (SAMD) + +## Tested devices + +* 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 + +## Tested Arduino Shields + +* Arduino Ethernet Shield (WizNet W5100) + +## Roadmap + +* FAQ + Home Assistant setup instructions +* Documentation of the library +* Unit tests +* Reduce flash memory usage +* Add support for HA lights + +## Unsupported features + +The library doesn't support all features of the MQTT integration. +If you need support for a new feature please open a new issue in the repository. + +# License + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License v3.0 as published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/gpl.html diff --git a/lib/arduino-home-assistant-main/examples/advanced-availability/advanced-availability.ino b/lib/arduino-home-assistant-main/examples/advanced-availability/advanced-availability.ino new file mode 100644 index 0000000..64df5bf --- /dev/null +++ b/lib/arduino-home-assistant-main/examples/advanced-availability/advanced-availability.ino @@ -0,0 +1,66 @@ +#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" 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); + +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"); + + 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 + // 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/lib/arduino-home-assistant-main/examples/availability/README.md b/lib/arduino-home-assistant-main/examples/availability/README.md new file mode 100644 index 0000000..76f471d --- /dev/null +++ b/lib/arduino-home-assistant-main/examples/availability/README.md @@ -0,0 +1,33 @@ +# Home Assistant availability example + +This example shows how to use "availability" feature of the Home Assistant. +It's supported only by following device types: +* Binary sensor +* Sensor +* Switch +* HVACs +* Fans + +## Initialization and usage + +Availability feature is turned off by default. In order to turn it on for the specific +sensor/switch you need to set initial state of the unit by calling `setAvailability` method. + +Example: +```cpp +... +HABinarySensor sensor("input", "door", true, mqtt); + +void setup() { + // ... + + // method must be called before `mqtt.begin(...)` + sensor.setAvailability(false); // offline + + mqtt.begin(BROKER_ADDR); +} +... +``` + +After availability is set for the very first time you can call `setAvailability` in any time. +Each time the method is called, the device will publish MQTT message to the broker. diff --git a/lib/arduino-home-assistant-main/examples/availability/availability.ino b/lib/arduino-home-assistant-main/examples/availability/availability.ino new file mode 100644 index 0000000..d3325ab --- /dev/null +++ b/lib/arduino-home-assistant-main/examples/availability/availability.ino @@ -0,0 +1,59 @@ +#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" 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); + +void setup() { + pinMode(INPUT_PIN, INPUT_PULLUP); + lastInputState = digitalRead(INPUT_PIN); + + // you don't need to verify return status + 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(); + lastAvailabilityToggleAt = millis(); + + // set device's details (optional) + device.setName("Arduino"); + device.setSoftwareVersion("1.0.0"); + + sensor.setName("Door sensor"); // optional + + 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) { + sensor.setAvailability(!sensor.isOnline()); + lastAvailabilityToggleAt = millis(); + } +} diff --git a/lib/arduino-home-assistant-main/examples/binary-sensor/binary-sensor.ino b/lib/arduino-home-assistant-main/examples/binary-sensor/binary-sensor.ino new file mode 100644 index 0000000..8b0862f --- /dev/null +++ b/lib/arduino-home-assistant-main/examples/binary-sensor/binary-sensor.ino @@ -0,0 +1,46 @@ +#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(); +bool lastInputState = false; + +EthernetClient client; +HADevice device(mac, sizeof(mac)); +HAMqtt mqtt(client, device); + +// "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); + +void setup() { + pinMode(INPUT_PIN, INPUT_PULLUP); + lastInputState = digitalRead(INPUT_PIN); + + // you don't need to verify return status + Ethernet.begin(mac); + + // set device's details (optional) + device.setName("Arduino"); + device.setSoftwareVersion("1.0.0"); + + sensor.setName("Door sensor"); // optional + + 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(); + } +} diff --git a/lib/arduino-home-assistant-main/examples/cover/cover.ino b/lib/arduino-home-assistant-main/examples/cover/cover.ino new file mode 100644 index 0000000..adfc920 --- /dev/null +++ b/lib/arduino-home-assistant-main/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/lib/arduino-home-assistant-main/examples/fan/fan.ino b/lib/arduino-home-assistant-main/examples/fan/fan.ino new file mode 100644 index 0000000..dddfdcf --- /dev/null +++ b/lib/arduino-home-assistant-main/examples/fan/fan.ino @@ -0,0 +1,53 @@ +#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. +// "ventilation" is unique ID of the fan. You should define your own ID. +HAFan fan("ventilation", HAFan::SpeedsFeature); + +void onStateChanged(bool state) { + Serial.print("State: "); + Serial.println(state); +} + +void onSpeedChanged(uint16_t speed) { + Serial.print("Speed: "); + Serial.println(speed); +} + +void setup() { + Serial.begin(9600); + + // 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) + fan.setName("Bathroom"); + fan.setRetain(true); + fan.setSpeedRangeMin(1); + fan.setSpeedRangeMax(100); + + // handle fan states + fan.onStateChanged(onStateChanged); + fan.onSpeedChanged(onSpeedChanged); + + mqtt.begin(BROKER_ADDR); +} + +void loop() { + Ethernet.maintain(); + mqtt.loop(); +} diff --git a/lib/arduino-home-assistant-main/examples/hvac/hvac.ino b/lib/arduino-home-assistant-main/examples/hvac/hvac.ino new file mode 100644 index 0000000..5bd2d90 --- /dev/null +++ b/lib/arduino-home-assistant-main/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/lib/arduino-home-assistant-main/examples/led-switch/led-switch.ino b/lib/arduino-home-assistant-main/examples/led-switch/led-switch.ino new file mode 100644 index 0000000..52b61c6 --- /dev/null +++ b/lib/arduino-home-assistant-main/examples/led-switch/led-switch.ino @@ -0,0 +1,50 @@ +#include +#include + +#define LED_PIN 9 +#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); +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) +{ + digitalWrite(LED_PIN, (state ? HIGH : LOW)); +} + +void setup() { + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + + // you don't need to verify return status + Ethernet.begin(mac); + + // set device's details (optional) + device.setName("Arduino"); + device.setSoftwareVersion("1.0.0"); + + // 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); +} + +void loop() { + Ethernet.maintain(); + mqtt.loop(); +} diff --git a/lib/arduino-home-assistant-main/examples/mqtt-advanced/mqtt-advanced.ino b/lib/arduino-home-assistant-main/examples/mqtt-advanced/mqtt-advanced.ino new file mode 100644 index 0000000..e2c7c62 --- /dev/null +++ b/lib/arduino-home-assistant-main/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/lib/arduino-home-assistant-main/examples/mqtt-with-credentials/mqtt-with-credentials.ino b/lib/arduino-home-assistant-main/examples/mqtt-with-credentials/mqtt-with-credentials.ino new file mode 100644 index 0000000..c4d5a92 --- /dev/null +++ b/lib/arduino-home-assistant-main/examples/mqtt-with-credentials/mqtt-with-credentials.ino @@ -0,0 +1,42 @@ +#include +#include + +#define LED_PIN 9 +#define BROKER_ADDR IPAddress(192,168,0,17) +#define BROKER_USERNAME "user" // replace with your credentials +#define BROKER_PASSWORD "pass" + +byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; + +EthernetClient client; +HADevice device(mac, sizeof(mac)); +HAMqtt mqtt(client, device); +HASwitch led("led", false); // "led" is unique ID of the switch. You should define your own ID. + +void onSwitchStateChanged(bool state, HASwitch* s) +{ + digitalWrite(LED_PIN, (state ? HIGH : LOW)); +} + +void setup() { + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + + // you don't need to verify return status + Ethernet.begin(mac); + + // set device's details (optional) + device.setName("Arduino"); + device.setSoftwareVersion("1.0.0"); + + // handle switch state + led.onStateChanged(onSwitchStateChanged); + led.setName("My LED"); // optional + + mqtt.begin(BROKER_ADDR, BROKER_USERNAME, BROKER_PASSWORD); +} + +void loop() { + Ethernet.maintain(); + mqtt.loop(); +} diff --git a/lib/arduino-home-assistant-main/examples/multi-state-button/multi-state-button.ino b/lib/arduino-home-assistant-main/examples/multi-state-button/multi-state-button.ino new file mode 100644 index 0000000..219efef --- /dev/null +++ b/lib/arduino-home-assistant-main/examples/multi-state-button/multi-state-button.ino @@ -0,0 +1,52 @@ +#include +#include + +// This example uses JC Button library +// https://github.com/JChristensen/JC_Button +#include + +#define BUTTON_PIN 9 +#define BUTTON_NAME "mybtn" +#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); +HATriggers triggers; +Button btn(BUTTON_PIN); +bool holdingBtn = false; + +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"); + + // setup triggers + triggers.add("button_short_press", BUTTON_NAME); + triggers.add("button_long_press", BUTTON_NAME); + btn.begin(); + + mqtt.begin(BROKER_ADDR); +} + +void loop() { + Ethernet.maintain(); + mqtt.loop(); + btn.read(); + + if (btn.pressedFor(3000) && !holdingBtn) { + triggers.trigger("button_long_press", BUTTON_NAME); + holdingBtn = true; + } else if (btn.wasReleased()) { + if (holdingBtn) { + holdingBtn = false; + } else { + triggers.trigger("button_short_press", BUTTON_NAME); + } + } +} diff --git a/lib/arduino-home-assistant-main/examples/nano33iot/nano33iot.ino b/lib/arduino-home-assistant-main/examples/nano33iot/nano33iot.ino new file mode 100644 index 0000000..1156194 --- /dev/null +++ b/lib/arduino-home-assistant-main/examples/nano33iot/nano33iot.ino @@ -0,0 +1,53 @@ +#include +#include + +#define LED_PIN 9 +#define BROKER_ADDR IPAddress(192,168,0,17) +#define WIFI_SSID "MyNetwork" +#define WIFI_PASSWORD "MyPassword" + +WiFiClient client; +HADevice device; +HAMqtt mqtt(client, device); +HASwitch led("led", false); // "led" is unique ID of the switch. You should define your own ID. + +void onSwitchStateChanged(bool state, HASwitch* s) +{ + digitalWrite(LED_PIN, (state ? HIGH : LOW)); +} + +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)); + + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + + // 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("Nano 33 IoT"); + device.setSoftwareVersion("1.0.0"); + + // handle switch state + led.onStateChanged(onSwitchStateChanged); + led.setName("My LED"); // optional + + mqtt.begin(BROKER_ADDR); +} + +void loop() { + mqtt.loop(); +} diff --git a/lib/arduino-home-assistant-main/examples/nodemcu/nodemcu.ino b/lib/arduino-home-assistant-main/examples/nodemcu/nodemcu.ino new file mode 100644 index 0000000..6a38d62 --- /dev/null +++ b/lib/arduino-home-assistant-main/examples/nodemcu/nodemcu.ino @@ -0,0 +1,60 @@ +#include +#include + +#define LED_PIN D0 +#define BROKER_ADDR IPAddress(192,168,0,17) +#define WIFI_SSID "MyNetwork" +#define WIFI_PASSWORD "MyPassword" + +WiFiClient client; +HADevice device; +HAMqtt mqtt(client, device); +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) +{ + digitalWrite(LED_PIN, (state ? HIGH : LOW)); +} + +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)); + + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + + // 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"); + + // handle switch state + led.onBeforeStateChanged(onBeforeSwitchStateChanged); // optional + led.onStateChanged(onSwitchStateChanged); + led.setName("My LED"); // optional + + mqtt.begin(BROKER_ADDR); +} + +void loop() { + mqtt.loop(); +} diff --git a/lib/arduino-home-assistant-main/examples/sensor/sensor.ino b/lib/arduino-home-assistant-main/examples/sensor/sensor.ino new file mode 100644 index 0000000..95c166b --- /dev/null +++ b/lib/arduino-home-assistant-main/examples/sensor/sensor.ino @@ -0,0 +1,54 @@ +#include +#include + +#define BROKER_ADDR IPAddress(192,168,0,17) + +byte mac[] = {0x00, 0x10, 0xFA, 0x6E, 0x38, 0x4A}; +unsigned long lastSentAt = millis(); +double lastValue = 0; + +EthernetClient client; +HADevice device(mac, sizeof(mac)); +HAMqtt mqtt(client, device); +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 + Ethernet.begin(mac); + + // set device's details (optional) + device.setName("Arduino"); + device.setSoftwareVersion("1.0.0"); + + // configure sensor (optional) + temp.setUnitOfMeasurement("°C"); + temp.setDeviceClass("temperature"); + temp.setIcon("mdi:home"); + temp.setName("Home temperature"); + + mqtt.begin(BROKER_ADDR); +} + +void loop() { + Ethernet.maintain(); + mqtt.loop(); + + if ((millis() - lastSentAt) >= 5000) { + 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/lib/arduino-home-assistant-main/library.properties b/lib/arduino-home-assistant-main/library.properties new file mode 100644 index 0000000..d4d9667 --- /dev/null +++ b/lib/arduino-home-assistant-main/library.properties @@ -0,0 +1,10 @@ +name=home-assistant-integration +version=1.3.0 +author=Dawid Chyrzynski +maintainer=Dawid Chyrzynski +sentence=Home Assistant MQTT integration for Arduino +paragraph=Lightweight library that provides easy to use API for integrating your Arduino/ESP based device with Home Assistant. +category=Communication +url=https://github.com/dawidchyrzynski/arduino-home-assistant +architectures=* +depends=PubSubClient diff --git a/lib/arduino-home-assistant-main/src/ArduinoHA.h b/lib/arduino-home-assistant-main/src/ArduinoHA.h new file mode 100644 index 0000000..696ddec --- /dev/null +++ b/lib/arduino-home-assistant-main/src/ArduinoHA.h @@ -0,0 +1,16 @@ +#ifndef AHA_ARDUINOHA_H +#define AHA_ARDUINOHA_H + +#include "HADevice.h" +#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" +#include "device-types/HASwitch.h" +#include "device-types/HATagScanner.h" +#include "device-types/HATriggers.h" + +#endif diff --git a/lib/arduino-home-assistant-main/src/ArduinoHADefines.h b/lib/arduino-home-assistant-main/src/ArduinoHADefines.h new file mode 100644 index 0000000..d0cf0cb --- /dev/null +++ b/lib/arduino-home-assistant-main/src/ArduinoHADefines.h @@ -0,0 +1,23 @@ +// Turns on debug information of the ArduinoHA core. +// 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_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/lib/arduino-home-assistant-main/src/HADevice.cpp b/lib/arduino-home-assistant-main/src/HADevice.cpp new file mode 100644 index 0000000..f7b57f0 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/HADevice.cpp @@ -0,0 +1,198 @@ +#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), \ + _sharedAvailability(false), \ + _availabilityTopic(nullptr), \ + _available(true) // device will be available by default + +HADevice::HADevice() : + _uniqueId(nullptr), + HADEVICE_INIT +{ + +} + +HADevice::HADevice(const char* uniqueId) : + _uniqueId(uniqueId), + HADEVICE_INIT +{ + +} + +HADevice::HADevice(const byte* uniqueId, const uint16_t& length) : + _uniqueId(HAUtils::byteArrayToStr(uniqueId, length)), + HADEVICE_INIT +{ + +} + +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) { + return false; + } + + _uniqueId = HAUtils::byteArrayToStr(uniqueId, 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 = + 3 + // opening and closing bracket + null terminator + strlen(_uniqueId) + 8; // 8 - length of the JSON data for this field + + if (_manufacturer != nullptr) { + size += strlen(_manufacturer) + 8; // 8 - length of the JSON data for this field + } + + if (_model != nullptr) { + size += strlen(_model) + 9; // 9 - length of the JSON data for this field + } + + if (_name != nullptr) { + size += strlen(_name) + 10; // 10 - length of the JSON data for this field + } + + if (_softwareVersion != nullptr) { + size += strlen(_softwareVersion) + 8; // 8 - length of the JSON data for this field + } + + return size; +} + +uint16_t HADevice::serialize(char* dst) const +{ + static const char QuotationSign[] PROGMEM = {"\""}; + + { + static const char DataBefore[] PROGMEM = {"{\"ids\":\""}; + + strcpy_P(dst, DataBefore); + strcat(dst, _uniqueId); + strcat_P(dst, QuotationSign); + } + + if (_manufacturer != nullptr) { + static const char DataBefore[] PROGMEM = {",\"mf\":\""}; + + strcat_P(dst, DataBefore); + strcat(dst, _manufacturer); + strcat_P(dst, QuotationSign); + } + + if (_model != nullptr) { + static const char DataBefore[] PROGMEM = {",\"mdl\":\""}; + + strcat_P(dst, DataBefore); + strcat(dst, _model); + strcat_P(dst, QuotationSign); + } + + if (_name != nullptr) { + static const char DataBefore[] PROGMEM = {",\"name\":\""}; + + strcat_P(dst, DataBefore); + strcat(dst, _name); + strcat_P(dst, QuotationSign); + } + + if (_softwareVersion != nullptr) { + static const char DataBefore[] PROGMEM = {",\"sw\":\""}; + + strcat_P(dst, DataBefore); + strcat(dst, _softwareVersion); + strcat_P(dst, QuotationSign); + } + + { + static const char Data[] PROGMEM = {"}"}; + strcat_P(dst, Data); + } + + return strlen(dst) + 1; // size with null terminator +} diff --git a/lib/arduino-home-assistant-main/src/HADevice.h b/lib/arduino-home-assistant-main/src/HADevice.h new file mode 100644 index 0000000..0c5c2b4 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/HADevice.h @@ -0,0 +1,53 @@ +#ifndef AHA_HADEVICE_H +#define AHA_HADEVICE_H + +#include + +class HADevice +{ +public: + HADevice(); + HADevice(const char* uniqueId); + HADevice(const byte* uniqueId, const uint16_t& length); + + inline const char* getUniqueId() const + { return _uniqueId; } + + inline void setManufacturer(const char* manufacturer) + { _manufacturer = manufacturer; } + + inline void setModel(const char* model) + { _model = model; } + + inline void setName(const char* name) + { _name = name; } + + 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; + +private: + const char* _uniqueId; + const char* _manufacturer; + const char* _model; + const char* _name; + const char* _softwareVersion; + bool _sharedAvailability; + char* _availabilityTopic; + bool _available; +}; + +#endif diff --git a/lib/arduino-home-assistant-main/src/HAMqtt.cpp b/lib/arduino-home-assistant-main/src/HAMqtt.cpp new file mode 100644 index 0000000..be0710c --- /dev/null +++ b/lib/arduino-home-assistant-main/src/HAMqtt.cpp @@ -0,0 +1,331 @@ +#include + +#include "HAMqtt.h" +#include "HADevice.h" +#include "ArduinoHADefines.h" +#include "device-types/BaseDeviceType.h" + +static const char* DefaultDiscoveryPrefix = "homeassistant"; +HAMqtt* HAMqtt::_instance = nullptr; + +void onMessageReceived(char* topic, uint8_t* payload, unsigned int length) +{ + if (HAMqtt::instance() == nullptr || length > UINT16_MAX) { + return; + } + + HAMqtt::instance()->processMessage(topic, payload, static_cast(length)); +} + +HAMqtt::HAMqtt(PubSubClient& client, HADevice& device) : + // _netClient(netClient), + _device(device), + _messageCallback(nullptr), + _connectedCallback(nullptr), + _connectionFailedCallback(nullptr), + _initialized(false), + _discoveryPrefix(DefaultDiscoveryPrefix), + _dataPrefix(nullptr), + _mqtt(client), + _username(nullptr), + _password(nullptr), + _lastConnectionAttemptAt(0), + _devicesTypesNb(0), + _devicesTypes(nullptr), + _lastWillTopic(nullptr), + _lastWillMessage(nullptr), + _lastWillRetain(false) +{ + _instance = this; +} + +bool HAMqtt::begin( + const IPAddress& serverIp, + 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(serverIp); + 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(serverIp, serverPort); + // _mqtt.setCallback(onMessageReceived); + + return true; +} + +bool HAMqtt::begin( + const IPAddress& serverIp, + const char* username, + const char* 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 && !_mqtt.loop()) { + connectToServer(); + } +} + +bool HAMqtt::isConnected() +{ + return _mqtt.connected(); +} + +void HAMqtt::addDeviceType(BaseDeviceType* deviceType) +{ + BaseDeviceType** data = (BaseDeviceType**)realloc( + _devicesTypes, + sizeof(BaseDeviceType*) * (_devicesTypesNb + 1) + ); + + if (data != nullptr) { + _devicesTypes = data; + _devicesTypes[_devicesTypesNb] = deviceType; + _devicesTypesNb++; + } +} + +bool HAMqtt::publish(const char* topic, const char* payload, bool retained) +{ + if (!isConnected()) { + return false; + } + +#if defined(ARDUINOHA_DEBUG) + Serial.print(F("Publishing: ")); + Serial.print(topic); + Serial.print(F(", len: ")); + Serial.print(strlen(payload)); + Serial.println(); +#endif + + _mqtt.beginPublish(topic, strlen(payload), retained); + _mqtt.write((const uint8_t*)(payload), strlen(payload)); + return _mqtt.endPublish(); +} + +bool HAMqtt::beginPublish( + const char* topic, + uint16_t payloadLength, + bool retained +) +{ +#if defined(ARDUINOHA_DEBUG) + Serial.print(F("Publishing: ")); + Serial.print(topic); + Serial.print(F(", len: ")); + Serial.print(payloadLength); + Serial.println(); +#endif + + return _mqtt.beginPublish(topic, payloadLength, retained); +} + +bool HAMqtt::writePayload(const char* data, uint16_t length) +{ + return (_mqtt.write((const uint8_t*)(data), length) > 0); +} + +bool HAMqtt::writePayload_P(const char* src) +{ + char data[strlen_P(src) + 1]; + strcpy_P(data, src); + + return _mqtt.write((const uint8_t*)(data), strlen(data)); +} + +bool HAMqtt::endPublish() +{ + return _mqtt.endPublish(); +} + +bool HAMqtt::subscribe(const char* topic) +{ +#if defined(ARDUINOHA_DEBUG) + Serial.print(F("Subscribing topic: ")); + Serial.print(topic); + Serial.println(); +#endif + + return _mqtt.subscribe(topic); +} + +void HAMqtt::processMessage(char* topic, uint8_t* payload, uint16_t length) +{ +#if defined(ARDUINOHA_DEBUG) + Serial.print(F("Received message on topic: ")); + Serial.print(topic); + Serial.print(F(", payload length: ")); + Serial.print(length); + Serial.println(); +#endif + + if (_messageCallback) { + _messageCallback(topic, payload, length); + } + + for (uint8_t i = 0; i < _devicesTypesNb; i++) { + _devicesTypes[i]->onMqttMessage(topic, payload, length); + } +} + +void HAMqtt::connectToServer() +{ + if (_lastConnectionAttemptAt > 0 && + (millis() - _lastConnectionAttemptAt) < ReconnectInterval) { + return; + } + + _lastConnectionAttemptAt = millis(); + +#if defined(ARDUINOHA_DEBUG) + Serial.print(F("Connecting to the MQTT broker... Client ID: ")); + Serial.print(_device.getUniqueId()); + Serial.println(); +#endif + + _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 + + onConnectedLogic(); + } else { +#if defined(ARDUINOHA_DEBUG) + Serial.println(F("Failed to connect to the broker")); + + if (_connectionFailedCallback) { + _connectionFailedCallback(); + } +#endif + } +} + +void HAMqtt::onConnectedLogic() +{ + if (_connectedCallback) { + _connectedCallback(); + } + + _device.publishAvailability(); + + for (uint8_t i = 0; i < _devicesTypesNb; i++) { + _devicesTypes[i]->onMqttConnected(); + } +} diff --git a/lib/arduino-home-assistant-main/src/HAMqtt.h b/lib/arduino-home-assistant-main/src/HAMqtt.h new file mode 100644 index 0000000..17e3f70 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/HAMqtt.h @@ -0,0 +1,245 @@ +#ifndef AHA_HAMQTT_H +#define AHA_HAMQTT_H + +#include +#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; +class HADevice; +class BaseDeviceType; + +class HAMqtt +{ +public: + static const uint16_t ReconnectInterval = 5000; // ms + + inline static HAMqtt* instance() + { return _instance; } + + HAMqtt(PubSubClient& client, HADevice& device); + + /** + * 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; } + + /** + * Returns discovery prefix. + */ + 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. + * + * @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. + * 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 uint16_t& serverPort = HAMQTT_DEFAULT_PORT, + const char* username = nullptr, + const char* password = nullptr + ); + 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. + */ + void loop(); + + /** + * Returns true if connection to the MQTT broker is established. + */ + bool isConnected(); + + /** + * Adds a new device's type to the MQTT. + * Each time the connection with MQTT broker is acquired, the HAMqtt class + * calls "onMqttConnected" method in all devices' types instances. + * + * @param deviceType Instance of the device's type (eg. HATriggers). + */ + void addDeviceType(BaseDeviceType* deviceType); + + /** + * Publishes MQTT message with given topic and payload. + * Message won't be published if connection with MQTT broker is not established. + * In this case method returns false. + * + * @param topic Topic to publish. + * @param payload Payload to publish (it may be empty const char). + * @param retained Determines whether message should be retained. + */ + bool publish(const char* topic, const char* payload, bool retained = false); + + bool beginPublish(const char* topic, uint16_t payloadLength, bool retained = false); + bool writePayload(const char* data, uint16_t length); + bool writePayload_P(const char* src); + bool endPublish(); + + /** + * Subscribes to the given topic. + * Whenever a new message is received the onMqttMessage callback in all + * devices types is called. + * + * Please note that you need to subscribe topic each time the connection + * with the broker is acquired. + * + * @param topic Topic to subscribe + */ + 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). + * + * @param topic Topic of the message. + * @param payload Content of the message. + * @param length Length of the message. + */ + void processMessage(char* topic, uint8_t* payload, uint16_t length); + + /** + * This method is called each time the connection with MQTT broker is acquired. + */ + void onConnectedLogic(); + +private: + static HAMqtt* _instance; + + /** + * Attempts to connect to the MQTT broker. + * The method uses properties passed to the "begin" method. + */ + void connectToServer(); + + + + // 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; + uint32_t _lastConnectionAttemptAt; + uint8_t _devicesTypesNb; + BaseDeviceType** _devicesTypes; + const char* _lastWillTopic; + const char* _lastWillMessage; + bool _lastWillRetain; +}; + +#endif diff --git a/lib/arduino-home-assistant-main/src/HAUtils.cpp b/lib/arduino-home-assistant-main/src/HAUtils.cpp new file mode 100644 index 0000000..5120048 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/HAUtils.cpp @@ -0,0 +1,67 @@ +#ifdef ARDUINO_ARCH_SAMD +#include +#endif + +#include + +#include "HAUtils.h" + +bool HAUtils::endsWith(const char* str, const char* suffix) +{ + if (str == nullptr || suffix == nullptr) { + return false; + } + + const uint16_t& lenstr = strlen(str); + const uint16_t& lensuffix = strlen(suffix); + + if (lensuffix > lenstr) { + return false; + } + + return (strncmp(str + lenstr - lensuffix, suffix, lensuffix) == 0); +} + +void HAUtils::byteArrayToStr( + char* dst, + const byte* src, + const uint16_t& length +) +{ + const uint16_t& finalLength = (length * 2); + static const char map[] PROGMEM = {"0123456789abcdef"}; + + for (uint8_t i = 0; i < length; i++) { + dst[i*2] = pgm_read_byte(&map[((char)src[i] & 0XF0) >> 4]); + dst[i*2+1] = pgm_read_byte(&map[((char)src[i] & 0x0F)]); + } + + dst[finalLength] = '\0'; +} + +char* HAUtils::byteArrayToStr( + const byte* src, + const uint16_t& length +) +{ + char* dst = (char*)malloc((length * 2) + 1); // include null terminator + byteArrayToStr(dst, src, length); + + return dst; +} + +void HAUtils::tempToStr( + char* dst, + const double& temp +) +{ + 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/lib/arduino-home-assistant-main/src/HAUtils.h b/lib/arduino-home-assistant-main/src/HAUtils.h new file mode 100644 index 0000000..39db0fa --- /dev/null +++ b/lib/arduino-home-assistant-main/src/HAUtils.h @@ -0,0 +1,33 @@ +#ifndef AHA_HAUTILS_H +#define AHA_HAUTILS_H + +#include + +#define AHA_SERIALIZED_TEMP_SIZE 8 + +class HAUtils +{ +public: + static bool endsWith( + const char* str, + const char* suffix + ); + static void byteArrayToStr( + char* dst, + const byte* src, + const uint16_t& length + ); + static char* byteArrayToStr( + const byte* src, + const uint16_t& length + ); + static void tempToStr( + char* dst, + const double& temp + ); + static double strToTemp( + const char* src + ); +}; + +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.cpp b/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.cpp new file mode 100644 index 0000000..86756d2 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.cpp @@ -0,0 +1,163 @@ +#include "BaseDeviceType.h" +#include "../HAMqtt.h" +#include "../HADevice.h" +#include "../HAUtils.h" + +BaseDeviceType::BaseDeviceType( + const char* componentName, + const char* uniqueId +) : + _componentName(componentName), + _uniqueId(uniqueId), + _availability(AvailabilityDefault), + _name(nullptr) +{ + mqtt()->addDeviceType(this); +} + +BaseDeviceType::~BaseDeviceType() +{ + +} + +void BaseDeviceType::setAvailability(bool online) +{ + _availability = (online ? AvailabilityOnline : AvailabilityOffline); + 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(), + uniqueId(), + DeviceTypeSerializer::ConfigTopic, + true, + true + ); + const uint16_t& dataLength = calculateSerializedLength(serializedDevice); + + if (topicLength == 0 || dataLength == 0) { + return; + } + + char topic[topicLength]; + DeviceTypeSerializer::generateTopic( + topic, + componentName(), + uniqueId(), + DeviceTypeSerializer::ConfigTopic, + true + ); + + 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() || + strlen(uniqueId()) == 0 || + strlen(componentName()) == 0 || + device->isSharedAvailabilityEnabled()) { + return; + } + + const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( + componentName(), + uniqueId(), + DeviceTypeSerializer::AvailabilityTopic + ); + if (topicSize == 0) { + return; + } + + char topic[topicSize]; + DeviceTypeSerializer::generateTopic( + topic, + componentName(), + uniqueId(), + DeviceTypeSerializer::AvailabilityTopic + ); + + if (strlen(topic) == 0) { + return; + } + + mqtt()->publish( + topic, + ( + _availability == AvailabilityOnline ? + DeviceTypeSerializer::Online : + DeviceTypeSerializer::Offline + ), + true + ); +} + +bool BaseDeviceType::compareTopics(const char* topic, const char* expectedTopic) +{ + if (topic == nullptr || expectedTopic == nullptr) { + return false; + } + + if (strlen(uniqueId()) == 0) { + return false; + } + + static const char Slash[] PROGMEM = {"/"}; + + // 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, uniqueId()); + strcat_P(suffix, Slash); + strcat(suffix, expectedTopic); + + return HAUtils::endsWith(topic, suffix); +} diff --git a/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.h b/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.h new file mode 100644 index 0000000..a46a8d4 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/BaseDeviceType.h @@ -0,0 +1,72 @@ +#ifndef AHA_BASEDEVICETYPE_H +#define AHA_BASEDEVICETYPE_H + +#include + +#include "../ArduinoHADefines.h" +#include "DeviceTypeSerializer.h" + +class HAMqtt; + +class BaseDeviceType +{ +public: + BaseDeviceType( + const char* componentName, + const char* uniqueId + ); + virtual ~BaseDeviceType(); + + inline const char* uniqueId() const + { return _uniqueId; } + + inline const char* componentName() const + { return _componentName; } + + inline bool isAvailabilityConfigured() const + { return (_availability != AvailabilityDefault); } + + 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: + 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 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 _uniqueId; + +private: + enum Availability { + AvailabilityDefault = 0, + AvailabilityOnline, + AvailabilityOffline + }; + + Availability _availability; + const char* _name; + + friend class HAMqtt; +}; + +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.cpp b/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.cpp new file mode 100644 index 0000000..dc3fc3a --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.cpp @@ -0,0 +1,421 @@ +#include + +#include "DeviceTypeSerializer.h" +#include "BaseDeviceType.h" +#include "../HAMqtt.h" +#include "../HADevice.h" + +static const char CharSlash[] PROGMEM = {"/"}; +static const char CharUnderscore[] PROGMEM = {"_"}; +static const char CharQuotation[] PROGMEM = {"\""}; + +const char* DeviceTypeSerializer::ConfigTopic = "config"; +const char* DeviceTypeSerializer::EventTopic = "event"; +const char* DeviceTypeSerializer::AvailabilityTopic = "avail"; +const char* DeviceTypeSerializer::StateTopic = "state"; +const char* DeviceTypeSerializer::CommandTopic = "cmd"; +const char* DeviceTypeSerializer::Online = "online"; +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 isDiscoveryTopic +) +{ + const char* prefix = getTopicPrefix(isDiscoveryTopic); + if (prefix == nullptr) { + return 0; + } + + uint16_t size = + 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 + } + + if (includeNullTerminator) { + size += 1; + } + + return size; +} + +uint16_t DeviceTypeSerializer::generateTopic( + char* output, + const char* component, + const char* objectId, + const char* suffix, + bool isDiscoveryTopic +) +{ + const char* prefix = getTopicPrefix(isDiscoveryTopic); + if (prefix == nullptr) { + return 0; + } + + strcpy(output, prefix); + strcat_P(output, CharSlash); + + 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, suffix); + return strlen(output) + 1; // size with null terminator +} + +uint16_t DeviceTypeSerializer::calculateBaseJsonDataSize() +{ + return 2; // opening and closing brackets of the JSON data +} + +uint16_t DeviceTypeSerializer::calculateNameFieldSize(const char* name) +{ + if (name == nullptr) { + return 0; + } + + // Field format: ,"name":"[NAME]" + return strlen(name) + 10; // 10 - length of the JSON decorators for this field +} + +uint16_t DeviceTypeSerializer::calculateUniqueIdFieldSize( + const char* uniqueId +) +{ + HADevice const* device = HAMqtt::instance()->getDevice(); + if (device == nullptr || uniqueId == nullptr) { + return 0; + } + + // Field format: ,"uniq_id":"[DEVICE ID]_[UNIQUE ID]" + return ( + strlen(device->getUniqueId()) + + strlen(uniqueId) + + 14 // 14 - length of the JSON decorators for this field + ); +} + +uint16_t DeviceTypeSerializer::calculateAvailabilityFieldSize( + const BaseDeviceType* const dt +) +{ + 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( + (sharedAvailability ? nullptr : dt->componentName()), + (sharedAvailability ? nullptr : dt->uniqueId()), + AvailabilityTopic, + false + ); + + if (availabilityTopicLength == 0) { + return 0; + } + + // Field format: ,"avty_t":"[TOPIC]" + 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 +) +{ + if (serializedDevice == nullptr) { + return 0; + } + + // Field format: ,"dev":[DEVICE] + return strlen(serializedDevice) + 7; // 7 - length of the JSON decorators for this field +} + +void DeviceTypeSerializer::mqttWriteBeginningJson() +{ + static const char Data[] PROGMEM = {"{"}; + HAMqtt::instance()->writePayload_P(Data); +} + +void DeviceTypeSerializer::mqttWriteEndJson() +{ + static const char Data[] PROGMEM = {"}"}; + HAMqtt::instance()->writePayload_P(Data); +} + +void DeviceTypeSerializer::mqttWriteConstCharField( + const char* prefix, + const char* value, + bool quoteSuffix +) +{ + if (prefix == nullptr || value == nullptr) { + return; + } + + HAMqtt::instance()->writePayload_P(prefix); + HAMqtt::instance()->writePayload(value, strlen(value)); + + if (quoteSuffix) { + HAMqtt::instance()->writePayload_P(CharQuotation); + } +} + +void DeviceTypeSerializer::mqttWriteNameField(const char* name) +{ + if (name == nullptr) { + return; + } + + static const char Prefix[] PROGMEM = {",\"name\":\""}; + mqttWriteConstCharField(Prefix, name); +} + +void DeviceTypeSerializer::mqttWriteUniqueIdField( + const char* uniqueId +) +{ + if (uniqueId == nullptr) { + return; + } + + HADevice const* device = HAMqtt::instance()->getDevice(); + if (device == nullptr) { + return; + } + + static const char Prefix[] PROGMEM = {",\"uniq_id\":\""}; + + 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, finalUniqueId); +} + +void DeviceTypeSerializer::mqttWriteAvailabilityField( + const BaseDeviceType* const dt +) +{ + 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( + (sharedAvailability ? nullptr : dt->componentName()), + (sharedAvailability ? nullptr : dt->uniqueId()), + AvailabilityTopic + ); + if (topicSize == 0) { + return; + } + + char availabilityTopic[topicSize]; + generateTopic( + availabilityTopic, + (sharedAvailability ? nullptr : dt->componentName()), + (sharedAvailability ? nullptr : dt->uniqueId()), + AvailabilityTopic + ); + + if (strlen(availabilityTopic) == 0) { + return; + } + + static const char Prefix[] PROGMEM = {",\"avty_t\":\""}; + mqttWriteConstCharField(Prefix, availabilityTopic); +} + +void DeviceTypeSerializer::mqttWriteRetainField( + bool retain +) +{ + if (!retain) { + return; + } + + static const char Prefix[] PROGMEM = {",\"ret\":"}; + mqttWriteConstCharField( + Prefix, + "true", + false + ); +} + +void DeviceTypeSerializer::mqttWriteDeviceField( + const char* serializedDevice +) +{ + if (serializedDevice == nullptr) { + return; + } + + static const char Data[] PROGMEM = {",\"dev\":"}; + + 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->uniqueId() , + topicSuffix + ); + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + DeviceTypeSerializer::generateTopic( + topic, + dt->componentName(), + dt->uniqueId(), + 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->uniqueId(), + topicSuffix + ); + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + generateTopic( + topic, + dt->componentName(), + dt->uniqueId(), + 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->uniqueId(), + topicSuffix + ); + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + generateTopic( + topic, + dt->componentName(), + dt->uniqueId(), + topicSuffix + ); + + if (strlen(topic) == 0) { + return false; + } + + return HAMqtt::instance()->subscribe(topic); +} diff --git a/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.h b/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.h new file mode 100644 index 0000000..d269b45 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/DeviceTypeSerializer.h @@ -0,0 +1,117 @@ +#ifndef AHA_DEVICETYPESERIALIZER_H +#define AHA_DEVICETYPESERIALIZER_H + +#include + +class HADevice; +class BaseDeviceType; + +class DeviceTypeSerializer +{ +public: + static const char* ConfigTopic; + static const char* EventTopic; + static const char* AvailabilityTopic; + static const char* StateTopic; + static const char* CommandTopic; + static const char* Online; + static const char* Offline; + static const char* StateOn; + static const char* StateOff; + + static const char* getTopicPrefix(bool isDiscoveryTopic); + + /** + * Calculates length of the topic with given parameters. + * 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 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: [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, + bool isDiscoveryTopic = false + ); + + static uint16_t calculateBaseJsonDataSize(); + static uint16_t calculateNameFieldSize( + const char* name + ); + static uint16_t calculateUniqueIdFieldSize( + const char* uniqueId + ); + static uint16_t calculateAvailabilityFieldSize( + const BaseDeviceType* const dt + ); + static uint16_t calculateRetainFieldSize( + bool retain + ); + static uint16_t calculateDeviceFieldSize( + const char* serializedDevice + ); + + static void mqttWriteBeginningJson(); + static void mqttWriteEndJson(); + static void mqttWriteConstCharField( + const char* prefix, + const char* value, + bool quoteSuffix = true + ); + static void mqttWriteNameField( + const char* name + ); + static void mqttWriteUniqueIdField( + const char* uniqueId + ); + static void mqttWriteAvailabilityField( + const BaseDeviceType* const dt + ); + static void mqttWriteRetainField( + bool retain + ); + static void mqttWriteDeviceField( + 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/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.cpp b/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.cpp new file mode 100644 index 0000000..ee7db85 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.cpp @@ -0,0 +1,194 @@ +#include "HABinarySensor.h" +#ifdef ARDUINOHA_BINARY_SENSOR + +#include "../ArduinoHADefines.h" +#include "../HAMqtt.h" +#include "../HADevice.h" + +HABinarySensor::HABinarySensor( + const char* uniqueId, + bool initialState +) : + BaseDeviceType("binary_sensor", uniqueId), + _class(nullptr), + _currentState(initialState) +{ + +} + +HABinarySensor::HABinarySensor( + const char* uniqueId, + bool initialState, + HAMqtt& mqtt +) : + HABinarySensor(uniqueId, initialState) +{ + (void)mqtt; +} + +HABinarySensor::HABinarySensor( + const char* uniqueId, + const char* deviceClass, + bool initialState +) : + BaseDeviceType("binary_sensor", uniqueId), + _class(deviceClass), + _currentState(initialState) +{ + +} + +HABinarySensor::HABinarySensor( + const char* uniqueId, + const char* deviceClass, + bool initialState, + HAMqtt& mqtt +) : + HABinarySensor(uniqueId, deviceClass, initialState) +{ + (void)mqtt; +} + +void HABinarySensor::onMqttConnected() +{ + if (strlen(uniqueId()) == 0) { + return; + } + + publishConfig(); + publishState(_currentState); + publishAvailability(); +} + +bool HABinarySensor::setState(bool state) +{ + if (state == _currentState) { + return true; + } + + if (publishState(state)) { + _currentState = state; + return true; + } + + return false; +} + +bool HABinarySensor::publishState(bool state) +{ + if (strlen(uniqueId()) == 0) { + return false; + } + + const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( + componentName(), + uniqueId(), + DeviceTypeSerializer::StateTopic + ); + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + DeviceTypeSerializer::generateTopic( + topic, + componentName(), + uniqueId(), + DeviceTypeSerializer::StateTopic + ); + + if (strlen(topic) == 0) { + return false; + } + + return mqtt()->publish( + topic, + ( + state ? + DeviceTypeSerializer::StateOn : + DeviceTypeSerializer::StateOff + ), + true + ); +} + +uint16_t HABinarySensor::calculateSerializedLength( + const char* serializedDevice +) const +{ + if (serializedDevice == nullptr) { + return 0; + } + + const HADevice* device = mqtt()->getDevice(); + if (device == 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); + + // 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 + 11; // 11 - length of the JSON decorators for this field + } + + // device class + if (_class != nullptr) { + // Field format: ,"dev_cla":"[CLASS]" + size += strlen(_class) + 13; // 13 - length of the JSON decorators for this field + } + + return size; // exludes null terminator +} + +bool HABinarySensor::writeSerializedData(const char* serializedDevice) const +{ + if (serializedDevice == nullptr) { + return false; + } + + DeviceTypeSerializer::mqttWriteBeginningJson(); + + // state topic + { + static const char Prefix[] PROGMEM = {"\"stat_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + DeviceTypeSerializer::StateTopic + ); + } + + // device class + if (_class != nullptr) { + static const char Prefix[] PROGMEM = {",\"dev_cla\":\""}; + DeviceTypeSerializer::mqttWriteConstCharField(Prefix, _class); + } + + DeviceTypeSerializer::mqttWriteNameField(getName()); + DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); + + return true; +} + +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.h b/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.h new file mode 100644 index 0000000..c886518 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HABinarySensor.h @@ -0,0 +1,81 @@ +#ifndef AHA_HABINARYSENSOR_H +#define AHA_HABINARYSENSOR_H + +#include "BaseDeviceType.h" + +#ifdef ARDUINOHA_BINARY_SENSOR + +class HABinarySensor : public BaseDeviceType +{ +public: + /** + * Initializes binary sensor. + * + * @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* uniqueId, + bool initialState + ); + HABinarySensor( + const char* uniqueId, + bool initialState, + HAMqtt& mqtt + ); // legacy constructor + + /** + * 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 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* uniqueId, + const char* deviceClass, + bool initialState + ); + HABinarySensor( + const char* uniqueId, + const char* deviceClass, + bool initialState, + HAMqtt& mqtt + ); + + /** + * Publishes configuration of the sensor to the MQTT. + */ + virtual void onMqttConnected() override; + + /** + * Changes state of the sensor and publishes MQTT message. + * Please note that if a new value is the same as previous one, + * the MQTT message won't be published. + * + * @param state New state of the sensor. + * @returns Returns true if MQTT message has been published successfully. + */ + bool setState(bool state); + + /** + * Returns last known state of the sensor. + * If setState method wasn't called the initial value will be returned. + */ + inline bool getState() const + { return _currentState; } + +private: + bool publishState(bool state); + 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/lib/arduino-home-assistant-main/src/device-types/HACover.cpp b/lib/arduino-home-assistant-main/src/device-types/HACover.cpp new file mode 100644 index 0000000..48d6270 --- /dev/null +++ b/lib/arduino-home-assistant-main/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/lib/arduino-home-assistant-main/src/device-types/HACover.h b/lib/arduino-home-assistant-main/src/device-types/HACover.h new file mode 100644 index 0000000..056da73 --- /dev/null +++ b/lib/arduino-home-assistant-main/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/lib/arduino-home-assistant-main/src/device-types/HAFan.cpp b/lib/arduino-home-assistant-main/src/device-types/HAFan.cpp new file mode 100644 index 0000000..47995e7 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HAFan.cpp @@ -0,0 +1,343 @@ +#include "HAFan.h" +#ifdef ARDUINOHA_FAN + +#include "../HAMqtt.h" +#include "../HADevice.h" + +const char* HAFan::PercentageCommandTopic = {"sct"}; +const char* HAFan::PercentageStateTopic = {"sst"}; + +HAFan::HAFan(const char* uniqueId, uint8_t features) : + BaseDeviceType("fan", uniqueId), + _features(features), + _currentState(false), + _stateCallback(nullptr), + _currentSpeed(0), + _speedCallback(nullptr), + _retain(false), + _speedRangeMin(1), + _speedRangeMax(100) +{ + +} + +HAFan::HAFan(const char* uniqueId, uint8_t features, HAMqtt& mqtt) : + HAFan(uniqueId, features) +{ + (void)mqtt; +} + +void HAFan::onMqttConnected() +{ + if (strlen(uniqueId()) == 0) { + return; + } + + publishConfig(); + publishAvailability(); + + DeviceTypeSerializer::mqttSubscribeTopic( + this, + DeviceTypeSerializer::CommandTopic + ); + + if (_features & SpeedsFeature) { + DeviceTypeSerializer::mqttSubscribeTopic( + this, + PercentageCommandTopic + ); + } + + if (!_retain) { + publishState(_currentState); + publishSpeed(_currentSpeed); + } +} + +void HAFan::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t& length +) +{ + (void)payload; + + if (compareTopics(topic, DeviceTypeSerializer::CommandTopic)) { + bool state = (length == strlen(DeviceTypeSerializer::StateOn)); + setState(state, true); + } 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); + } + } +} + +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(uint16_t speed) +{ + if (publishSpeed(speed)) { + _currentSpeed = speed; + + if (_speedCallback) { + _speedCallback(_currentSpeed); + } + + return true; + } + + return false; +} + +bool HAFan::publishState(bool state) +{ + if (strlen(uniqueId()) == 0) { + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + DeviceTypeSerializer::StateTopic, + ( + state ? + DeviceTypeSerializer::StateOn : + DeviceTypeSerializer::StateOff + ) + ); +} + +bool HAFan::publishSpeed(uint16_t speed) +{ + if (strlen(uniqueId()) == 0) { + 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, + PercentageStateTopic, + str + ); +} + +uint16_t HAFan::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 + } + + // speeds + if (_features & SpeedsFeature) { + // percentage command topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + uniqueId(), + PercentageCommandTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"pct_cmd_t":"[TOPIC]" + size += topicLength + 15; // 15 - length of the JSON decorators for this field + } + + // percentage state topic + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + uniqueId(), + PercentageStateTopic, + false + ); + + if (topicLength == 0) { + return 0; + } + + // Field format: ,"pct_stat_t":"[TOPIC]" + size += topicLength + 16; // 16 - length of the JSON decorators for this field + } + + // speed range min + if (_speedRangeMin != 1) { + uint8_t digitsNb = floor(log10(_speedRangeMin)) + 1; + + // Field format: ,"spd_rng_min":[VALUE] + size += digitsNb + 15; // 15 - length of the JSON decorators for this field + } + + // speed range max + if (_speedRangeMax != 100) { + uint8_t digitsNb = floor(log10(_speedRangeMax)) + 1; + + // Field format: ,"spd_rng_max":[VALUE] + size += digitsNb + 15; // 15 - length of the JSON decorators for this field + } + } + + return size; // 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) { + // percentage command topic + { + static const char Prefix[] PROGMEM = {",\"pct_cmd_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + PercentageCommandTopic + ); + } + + // percentage state topic + { + static const char Prefix[] PROGMEM = {",\"pct_stat_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + PercentageStateTopic + ); + } + + // 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, + 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::mqttWriteNameField(getName()); + DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteRetainField(_retain); + DeviceTypeSerializer::mqttWriteEndJson(); + + return true; +} + +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HAFan.h b/lib/arduino-home-assistant-main/src/device-types/HAFan.h new file mode 100644 index 0000000..76685ea --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HAFan.h @@ -0,0 +1,156 @@ +#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)(uint16_t) +#define HAFAN_STATE_CALLBACK_SPEED_DEPRECATED(name) void (*name)(Speed) + +class HAFan : public BaseDeviceType +{ +public: + static const char* PercentageCommandTopic; + static const char* PercentageStateTopic; + + enum Features { + DefaultFeatures = 0, + SpeedsFeature = 1 + }; + + // @deprecated + 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 + */ + AHA_DEPRECATED(inline void setSpeeds(uint8_t speeds)) + { (void)speeds; } + + /** + * Sets speed of the fan. + * + * @param speed + */ + bool setSpeed(uint16_t speed); + + /** + * Returns current speed of the fan. + */ + inline uint16_t 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; } + + AHA_DEPRECATED(inline void onSpeedChanged(HAFAN_STATE_CALLBACK_SPEED_DEPRECATED(callback))) + { (void)callback; } + + /** + * 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 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(uint16_t speed); + uint16_t calculateSerializedLength(const char* serializedDevice) const override; + bool writeSerializedData(const char* serializedDevice) const override; + + const uint8_t _features; + bool _currentState; + HAFAN_STATE_CALLBACK_BOOL(_stateCallback); + uint16_t _currentSpeed; + HAFAN_STATE_CALLBACK_SPEED(_speedCallback); + bool _retain; + uint16_t _speedRangeMin; + uint16_t _speedRangeMax; +}; + +#endif +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HAHVAC.cpp b/lib/arduino-home-assistant-main/src/device-types/HAHVAC.cpp new file mode 100644 index 0000000..aadc25d --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HAHVAC.cpp @@ -0,0 +1,1078 @@ +#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__), + _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(uniqueId()) == 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) && + compareTopics(topic, AuxCommandTopic)) { + bool state = (length == strlen(DeviceTypeSerializer::StateOn)); + setAuxHeatingState(state); + } else if ((_features & AwayModeFeature) && + compareTopics(topic, AwayCommandTopic)) { + bool state = (length == strlen(DeviceTypeSerializer::StateOn)); + setAwayState(state); + } else if ((_features & HoldFeature) && + compareTopics(topic, HoldCommandTopic)) { + bool state = (length == strlen(DeviceTypeSerializer::StateOn)); + setHoldState(state); + } else if (compareTopics(topic, TargetTemperatureCommandTopic)) { + char src[length + 1]; + memset(src, 0, sizeof(src)); + memcpy(src, payload, length); + + setTargetTemperature(HAUtils::strToTemp(src)); + } else if (compareTopics(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(uniqueId()) == 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(uniqueId()) == 0) { + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + AuxStateTopic, + ( + state ? + DeviceTypeSerializer::StateOn : + DeviceTypeSerializer::StateOff + ) + ); +} + +bool HAHVAC::publishAwayState(bool state) +{ + if (!(_features & AwayModeFeature) || + strlen(uniqueId()) == 0) { + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + AwayStateTopic, + ( + state ? + DeviceTypeSerializer::StateOn : + DeviceTypeSerializer::StateOff + ) + ); +} + +bool HAHVAC::publishHoldState(bool state) +{ + if (!(_features & HoldFeature) || + strlen(uniqueId()) == 0) { + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + HoldStateTopic, + ( + state ? + DeviceTypeSerializer::StateOn : + DeviceTypeSerializer::StateOff + ) + ); +} + +bool HAHVAC::publishCurrentTemperature(double temperature) +{ + if (strlen(uniqueId()) == 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(uniqueId()) == 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(uniqueId()) == 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::calculateNameFieldSize(getName()); + size += DeviceTypeSerializer::calculateUniqueIdFieldSize(uniqueId()); + size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); + size += DeviceTypeSerializer::calculateAvailabilityFieldSize(this); + size += DeviceTypeSerializer::calculateRetainFieldSize(_retain); + + // current temperature + { + const uint16_t& topicLength = DeviceTypeSerializer::calculateTopicLength( + componentName(), + uniqueId(), + 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(), + uniqueId(), + 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(), + uniqueId(), + 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(), + uniqueId(), + 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(), + uniqueId(), + 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(), + uniqueId(), + 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(), + uniqueId(), + 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(), + uniqueId(), + 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(), + uniqueId(), + 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(), + uniqueId(), + 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(), + uniqueId(), + 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(), + uniqueId(), + 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::mqttWriteNameField(getName()); + DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteRetainField(_retain); + DeviceTypeSerializer::mqttWriteEndJson(); + + return true; +} + +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HAHVAC.h b/lib/arduino-home-assistant-main/src/device-types/HAHVAC.h new file mode 100644 index 0000000..44c4ecb --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HAHVAC.h @@ -0,0 +1,336 @@ +#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 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; + uint8_t _modes; + HAHVAC_STATE_CALLBACK_MODE(_modeChangedCallback); + Mode _currentMode; + bool _retain; +}; + +#endif +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HASensor.cpp b/lib/arduino-home-assistant-main/src/device-types/HASensor.cpp new file mode 100644 index 0000000..cc75f3c --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HASensor.cpp @@ -0,0 +1,218 @@ +#ifdef ARDUINO_ARCH_SAMD +#include +#endif + +#include "HASensor.h" +#ifdef ARDUINOHA_SENSOR + +#include "../HAMqtt.h" +#include "../HADevice.h" + +HASensor::HASensor(const char* uniqueId) : + BaseDeviceType("sensor", uniqueId), + _class(nullptr), + _units(nullptr), + _icon(nullptr) +{ + +} + +HASensor::HASensor(const char* uniqueId, HAMqtt& mqtt) : + HASensor(uniqueId) +{ + (void)mqtt; +} + +void HASensor::onMqttConnected() +{ + if (strlen(uniqueId()) == 0) { + return; + } + + publishConfig(); + publishAvailability(); +} + +bool HASensor::setValue(const char* value) +{ + 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(uniqueId()) == 0 || value == nullptr) { + return false; + } + + if (!mqtt()->isConnected()) { + return false; + } + + const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( + componentName(), + uniqueId(), + DeviceTypeSerializer::StateTopic + ); + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + DeviceTypeSerializer::generateTopic( + topic, + componentName(), + uniqueId(), + DeviceTypeSerializer::StateTopic + ); + + if (strlen(topic) == 0) { + return false; + } + + return mqtt()->publish(topic, value, true); +} + +uint16_t HASensor::calculateSerializedLength( + const char* serializedDevice +) const +{ + if (serializedDevice == nullptr) { + return 0; + } + + const HADevice* device = mqtt()->getDevice(); + if (device == 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); + + { + const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( + componentName(), + uniqueId(), + DeviceTypeSerializer::StateTopic, + false + ); + + if (topicSize == 0) { + return 0; + } + + // Format: "stat_t":"[TOPIC]" + size += topicSize + 11; // 11 - length of the JSON decorators for this field + } + + // device class + if (_class != nullptr) { + // Field format: ,"dev_cla":"[CLASS]" + size += strlen(_class) + 13; // 13 - length of the JSON decorators for this field + } + + // units of measurement + if (_units != nullptr) { + // Format: ,"unit_of_meas":"[UNITS]" + 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 +} + +bool HASensor::writeSerializedData(const char* serializedDevice) const +{ + if (serializedDevice == nullptr) { + return false; + } + + DeviceTypeSerializer::mqttWriteBeginningJson(); + + // state topic + { + static const char Prefix[] PROGMEM = {"\"stat_t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + DeviceTypeSerializer::StateTopic + ); + } + + // device class + if (_class != nullptr) { + static const char Prefix[] PROGMEM = {",\"dev_cla\":\""}; + DeviceTypeSerializer::mqttWriteConstCharField(Prefix, _class); + } + + // units of measurement + if (_units != nullptr) { + static const char Prefix[] PROGMEM = {",\"unit_of_meas\":\""}; + DeviceTypeSerializer::mqttWriteConstCharField(Prefix, _units); + } + + // icon + if (_icon != nullptr) { + static const char Prefix[] PROGMEM = {",\"ic\":\""}; + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + _icon + ); + } + + DeviceTypeSerializer::mqttWriteNameField(getName()); + DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); + + return true; +} + +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HASensor.h b/lib/arduino-home-assistant-main/src/device-types/HASensor.h new file mode 100644 index 0000000..908dc72 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HASensor.h @@ -0,0 +1,90 @@ +#ifndef AHA_HASENSOR_H +#define AHA_HASENSOR_H + +#include "BaseDeviceType.h" + +#ifdef ARDUINOHA_SENSOR + +class HASensor : public BaseDeviceType +{ +public: + /** + * Initializes binary sensor. + * + * @param uniqueId Unique ID of the sensor. Recommendes characters: [a-z0-9\-_] + */ + HASensor( + const char* uniqueId + ); + HASensor( + const char* uniqueId, + HAMqtt& mqtt + ); // legacy constructor + + /** + * Publishes configuration of the sensor to the MQTT. + */ + virtual void onMqttConnected() override; + + /** + * Publishes new value of the sensor. + * Please note that connection to MQTT broker must be acquired. + * Otherwise method will return false. + * + * @param state Value to publish. + * @returns Returns true if MQTT message has been published successfully. + */ + 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)); } + + /** + * 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 void setDeviceClass(const char* className) + { _class = className; } + + /** + * Defines the units of measurement of the sensor, if any. + * + * @param units For example: °C, % + */ + 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: + 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; + const char* _icon; +}; + +#endif +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HASwitch.cpp b/lib/arduino-home-assistant-main/src/device-types/HASwitch.cpp new file mode 100644 index 0000000..e62b91a --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HASwitch.cpp @@ -0,0 +1,211 @@ +#include "HASwitch.h" +#ifdef ARDUINOHA_SWITCH + +#include "../ArduinoHADefines.h" +#include "../HAMqtt.h" +#include "../HADevice.h" + +HASwitch::HASwitch(const char* uniqueId, bool initialState) : + BaseDeviceType("switch", uniqueId), + _stateCallback(nullptr), + _beforeStateCallback(nullptr), + _currentState(initialState), + _icon(nullptr), + _retain(false) +{ + +} + +HASwitch::HASwitch( + const char* uniqueId, + bool initialState, + HAMqtt& mqtt +) : + HASwitch(uniqueId, initialState) +{ + (void)mqtt; +} + +void HASwitch::onMqttConnected() +{ + if (strlen(uniqueId()) == 0) { + return; + } + + publishConfig(); + publishAvailability(); + + if (!_retain) { + publishState(_currentState); + } + + DeviceTypeSerializer::mqttSubscribeTopic( + this, + DeviceTypeSerializer::CommandTopic + ); +} + +void HASwitch::onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t& length +) +{ + (void)payload; + + if (compareTopics(topic, DeviceTypeSerializer::CommandTopic)) { + bool state = (length == strlen(DeviceTypeSerializer::StateOn)); + setState(state, true); + } +} + +bool HASwitch::setState(bool state, bool force) +{ + if (!force && _currentState == state) { + return true; + } + + if (_beforeStateCallback) { + _beforeStateCallback(state, this); + } + + if (publishState(state)) { + _currentState = state; + + if (_stateCallback) { + _stateCallback(_currentState, this); + } + + return true; + } + + return false; +} + +bool HASwitch::publishState(bool state) +{ + if (strlen(uniqueId()) == 0) { + return false; + } + + return DeviceTypeSerializer::mqttPublishMessage( + this, + DeviceTypeSerializer::StateTopic, + ( + state ? + DeviceTypeSerializer::StateOn : + DeviceTypeSerializer::StateOff + ) + ); +} + +uint16_t HASwitch::calculateSerializedLength(const char* serializedDevice) const +{ + if (serializedDevice == nullptr) { + return 0; + } + + const HADevice* device = mqtt()->getDevice(); + if (device == 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); + + // cmd 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 + } + + // 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 +} + +bool HASwitch::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 + ); + } + + // icon + if (_icon != nullptr) { + static const char Prefix[] PROGMEM = {",\"ic\":\""}; + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + _icon + ); + } + + DeviceTypeSerializer::mqttWriteRetainField(_retain); + DeviceTypeSerializer::mqttWriteNameField(getName()); + DeviceTypeSerializer::mqttWriteUniqueIdField(uniqueId()); + DeviceTypeSerializer::mqttWriteAvailabilityField(this); + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); + + return true; +} + +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HASwitch.h b/lib/arduino-home-assistant-main/src/device-types/HASwitch.h new file mode 100644 index 0000000..04a18f1 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HASwitch.h @@ -0,0 +1,123 @@ +#ifndef AHA_HASWITCH_H +#define AHA_HASWITCH_H + +#include "BaseDeviceType.h" + +#ifdef ARDUINOHA_SWITCH + +#define HASWITCH_CALLBACK(name) void (*name)(bool, HASwitch*) + +class HASwitch : public BaseDeviceType +{ +public: + /** + * Initializes switch. + * + * @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* uniqueId, + bool initialState + ); + HASwitch( + const char* uniqueId, + bool initialState, + HAMqtt& mqtt + ); // legacy constructor + + /** + * Publishes configuration of the sensor to the MQTT. + */ + virtual void onMqttConnected() override; + + /** + * Processes message received from the MQTT broker. + * The method updates state of the switch (if message matches switch'es topic). + */ + virtual void onMqttMessage( + const char* topic, + const uint8_t* payload, + const uint16_t& length + ) override; + + /** + * Changes state of the switch 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 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 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 switch. + * 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 switch changes. + * Please note that it's not possible to register multiple callbacks for the same switch. + * + * @param callback + */ + 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`. + * + * @param icon Material Design Icon name with mdi: prefix. + */ + inline void setIcon(const char* icon) + { _icon = icon; } + + /** + * 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); + HASWITCH_CALLBACK(_beforeStateCallback); + bool _currentState; + const char* _icon; + bool _retain; +}; + +#endif +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HATagScanner.cpp b/lib/arduino-home-assistant-main/src/device-types/HATagScanner.cpp new file mode 100644 index 0000000..1edafa7 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HATagScanner.cpp @@ -0,0 +1,111 @@ +#include "HATagScanner.h" +#ifdef ARDUINOHA_TAG_SCANNER + +#include "../ArduinoHADefines.h" +#include "../HAMqtt.h" +#include "../HADevice.h" + +HATagScanner::HATagScanner(const char* uniqueId) : + BaseDeviceType("tag", uniqueId) +{ + +} + +HATagScanner::HATagScanner(const char* uniqueId, HAMqtt& mqtt) : + HATagScanner(uniqueId) +{ + (void)mqtt; +} + +void HATagScanner::onMqttConnected() +{ + if (strlen(uniqueId()) == 0) { + return; + } + + publishConfig(); +} + +bool HATagScanner::tagScanned(const char* tag) +{ + if (tag == nullptr || strlen(tag) == 0 || strlen(uniqueId()) == 0) { + return false; + } + + const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( + componentName(), + uniqueId(), + DeviceTypeSerializer::EventTopic + ); + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + DeviceTypeSerializer::generateTopic( + topic, + componentName(), + uniqueId(), + DeviceTypeSerializer::EventTopic + ); + + if (strlen(topic) == 0) { + return false; + } + + return mqtt()->publish(topic, tag); +} + +uint16_t HATagScanner::calculateSerializedLength( + const char* serializedDevice +) const +{ + if (serializedDevice == nullptr) { + return 0; + } + + uint16_t size = 0; + size += DeviceTypeSerializer::calculateBaseJsonDataSize(); + size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); + + // event topic + { + const uint16_t& topicSize = DeviceTypeSerializer::calculateTopicLength( + componentName(), + uniqueId(), + DeviceTypeSerializer::EventTopic, + false + ); + + // Format: "t":"[TOPIC]" + size += topicSize + 6; // 6 - length of the JSON decorators for this field + } + + return size; // exludes null terminator +} + +bool HATagScanner::writeSerializedData(const char* serializedDevice) const +{ + if (serializedDevice == nullptr) { + return false; + } + + DeviceTypeSerializer::mqttWriteBeginningJson(); + + // topic + { + static const char Prefix[] PROGMEM = {"\"t\":\""}; + DeviceTypeSerializer::mqttWriteTopicField( + this, + Prefix, + DeviceTypeSerializer::EventTopic + ); + } + + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); + + return true; +} + +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HATagScanner.h b/lib/arduino-home-assistant-main/src/device-types/HATagScanner.h new file mode 100644 index 0000000..2b26d1a --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HATagScanner.h @@ -0,0 +1,43 @@ +#ifndef AHA_HATAGSCANNER_H +#define AHA_HATAGSCANNER_H + +#include "BaseDeviceType.h" + +#ifdef ARDUINOHA_TAG_SCANNER + +class HATagScanner : public BaseDeviceType +{ +public: + /** + * Initializes tag scanner with the given name. + * + * @param uniqueId Unique ID of the scanner. Recommendes characters: [a-z0-9\-_] + */ + HATagScanner(const char* uniqueId); + HATagScanner(const char* uniqueId, HAMqtt& mqtt); // legacy constructor + + /** + * Publishes configuration of the sensor to the MQTT. + */ + virtual void onMqttConnected() override; + + /** + * Tag scanner doesn't support availability. Nothing to do here. + */ + virtual void setAvailability(bool online) override { (void)online; } + + /** + * Sends "tag scanned" event to the MQTT (Home Assistant). + * Based on this event HA may perform user-defined automation. + * + * @param tag Value of the scanned tag. + */ + bool tagScanned(const char* tag); + +private: + uint16_t calculateSerializedLength(const char* serializedDevice) const override; + bool writeSerializedData(const char* serializedDevice) const override; +}; + +#endif +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HATriggers.cpp b/lib/arduino-home-assistant-main/src/device-types/HATriggers.cpp new file mode 100644 index 0000000..62b1ae3 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HATriggers.cpp @@ -0,0 +1,321 @@ +#include "HATriggers.h" +#ifdef ARDUINOHA_TRIGGERS + +#include "../ArduinoHADefines.h" +#include "../HAMqtt.h" +#include "../HADevice.h" + +HATriggers::HATriggers() : + BaseDeviceType("device_automation", nullptr), + _triggers(nullptr), + _triggersNb(0) +{ + +} + +HATriggers::HATriggers(HAMqtt& mqtt) : + HATriggers() +{ + (void)mqtt; +} + +HATriggers::~HATriggers() +{ + if (_triggers != nullptr) { + free(_triggers); + } +} + +void HATriggers::onMqttConnected() +{ + publishConfig(); +} + +bool HATriggers::add(const char* type, const char* subtype) +{ + if (mqtt()->getDevice() == nullptr) { + return false; + } + + HATrigger* triggers = (HATrigger*)realloc(_triggers, sizeof(HATrigger) * (_triggersNb + 1)); + if (triggers == nullptr) { + return false; + } + +#if defined(ARDUINOHA_DEBUG) + Serial.print(F("Adding HATrigger: ")); + Serial.print(type); + Serial.print(F(" ")); + Serial.print(subtype); + Serial.println(); +#endif + + _triggers = triggers; + _triggers[_triggersNb].type = type; + _triggers[_triggersNb].subtype = subtype; + + _triggersNb++; + return true; +} + +bool HATriggers::trigger(const char* type, const char* subtype) +{ + HATrigger* trigger = nullptr; + for (uint8_t i = 0; i < _triggersNb; i++) { + if (strcmp(_triggers[i].type, type) == 0 && + strcmp(_triggers[i].subtype, subtype) == 0) { + trigger = &_triggers[i]; + break; + } + } + + if (trigger == nullptr) { + return false; + } + + const uint16_t& topicSize = calculateTopicLength( + componentName(), + trigger, + DeviceTypeSerializer::EventTopic + ); + + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + generateTopic( + topic, + componentName(), + trigger, + DeviceTypeSerializer::EventTopic + ); + + if (strlen(topic) == 0) { + 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, ""); +} + +void HATriggers::publishConfig() +{ + const HADevice* device = mqtt()->getDevice(); + if (device == nullptr) { + return; // device is required for triggers + } + + const uint16_t& deviceLength = device->calculateSerializedLength(); + if (deviceLength == 0) { + return; + } + + char serializedDevice[deviceLength]; + if (device->serialize(serializedDevice) == 0) { + return; + } + + for (uint8_t i = 0; i < _triggersNb; i++) { + const HATrigger* trigger = &_triggers[i]; + if (trigger == nullptr) { + continue; + } + + const uint16_t& topicLength = calculateTopicLength( + componentName(), + trigger, + DeviceTypeSerializer::ConfigTopic, + true, + true + ); + const uint16_t& dataLength = calculateSerializedLength( + trigger, + serializedDevice + ); + if (topicLength == 0 || dataLength == 0) { + continue; + } + + char topic[topicLength]; + generateTopic( + topic, + componentName(), + trigger, + DeviceTypeSerializer::ConfigTopic, + true + ); + + if (strlen(topic) == 0) { + continue; + } + + if (mqtt()->beginPublish(topic, dataLength, true)) { + writeSerializedTrigger(trigger, serializedDevice); + mqtt()->endPublish(); + } + } +} + +uint16_t HATriggers::calculateTopicLength( + const char* component, + const HATrigger *trigger, + const char* suffix, + bool includeNullTerminator, + bool isDiscoveryTopic +) const +{ + uint8_t length = strlen(trigger->type) + strlen(trigger->subtype) + 2; // underscore and slash + return DeviceTypeSerializer::calculateTopicLength( + component, + nullptr, + suffix, + includeNullTerminator, + isDiscoveryTopic + ) + length; +} + +uint16_t HATriggers::generateTopic( + char* output, + const char* component, + const HATrigger *trigger, + const char* suffix, + bool isDiscoveryTopic +) const +{ + static const char Underscore[] PROGMEM = {"_"}; + uint8_t length = strlen(trigger->type) + strlen(trigger->subtype) + 2; // underscore + null terminator + char objectId[length]; + + strcpy(objectId, trigger->subtype); + strcat_P(objectId, Underscore); + strcat(objectId, trigger->type); + + return DeviceTypeSerializer::generateTopic( + output, + component, + objectId, + suffix, + isDiscoveryTopic + ); +} + +uint16_t HATriggers::calculateSerializedLength( + const HATrigger* trigger, + const char* serializedDevice +) const +{ + if (serializedDevice == nullptr) { + return 0; + } + + uint16_t size = 0; + size += DeviceTypeSerializer::calculateBaseJsonDataSize(); + size += DeviceTypeSerializer::calculateDeviceFieldSize(serializedDevice); + + // automation type + { + // Format: "atype":"trigger" + size += 17; + } + + // topic + { + const uint16_t& topicSize = calculateTopicLength( + componentName(), + trigger, + DeviceTypeSerializer::EventTopic, + false + ); + if (topicSize == 0) { + return 0; + } + + // Format: ,"t":"[TOPIC]" + size += topicSize + 7; // 7 - length of the JSON decorators for this field + } + + // type + { + // Format: ,"type":"[TYPE]" + size += strlen(trigger->type) + 10; // 10 - length of the JSON decorators for this field + } + + // subtype + { + // Format: ,"stype":"[SUBTYPE]" + size += strlen(trigger->subtype) + 11; // 11 - length of the JSON decorators for this field; + } + + return size; // exludes null terminator +} + +bool HATriggers::writeSerializedTrigger( + const HATrigger* trigger, + const char* serializedDevice +) const +{ + if (serializedDevice == nullptr) { + return false; + } + + DeviceTypeSerializer::mqttWriteBeginningJson(); + + // automation type + { + static const char Data[] PROGMEM = {"\"atype\":\"trigger\""}; + mqtt()->writePayload_P(Data); + } + + // topic + { + const uint16_t& topicSize = calculateTopicLength( + componentName(), + trigger, + DeviceTypeSerializer::EventTopic + ); + if (topicSize == 0) { + return false; + } + + char topic[topicSize]; + generateTopic( + topic, + componentName(), + trigger, + DeviceTypeSerializer::EventTopic + ); + + static const char Prefix[] PROGMEM = {",\"t\":\""}; + DeviceTypeSerializer::mqttWriteConstCharField( + Prefix, + topic + ); + } + + // type + { + static const char Prefix[] PROGMEM = {",\"type\":\""}; + DeviceTypeSerializer::mqttWriteConstCharField(Prefix, trigger->type); + } + + // subtype + { + static const char Prefix[] PROGMEM = {",\"stype\":\""}; + DeviceTypeSerializer::mqttWriteConstCharField(Prefix, trigger->subtype); + } + + DeviceTypeSerializer::mqttWriteDeviceField(serializedDevice); + DeviceTypeSerializer::mqttWriteEndJson(); + + return true; +} + +#endif diff --git a/lib/arduino-home-assistant-main/src/device-types/HATriggers.h b/lib/arduino-home-assistant-main/src/device-types/HATriggers.h new file mode 100644 index 0000000..5732e47 --- /dev/null +++ b/lib/arduino-home-assistant-main/src/device-types/HATriggers.h @@ -0,0 +1,73 @@ +#ifndef AHA_HATRIGGERS_H +#define AHA_HATRIGGERS_H + +#include "BaseDeviceType.h" + +#ifdef ARDUINOHA_TRIGGERS + +struct HATrigger { + const char* type; + const char* subtype; +} __attribute__((packed)); + +class HATriggers : public BaseDeviceType +{ +public: + HATriggers(); + HATriggers(HAMqtt& mqtt); // legacy constructor + virtual ~HATriggers(); + + virtual void onMqttConnected() override; + + /** + * Triggers dont't support availability. Nothing to do here. + */ + 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, + const char* suffix, + bool includeNullTerminator = true, + bool isDiscoveryTopic = false + ) const; + + uint16_t generateTopic( + char* output, + const char* component, + const HATrigger *trigger, + const char* suffix, + bool isDiscoveryTopic = false + ) const; + +private: + uint16_t calculateSerializedLength( + const HATrigger* trigger, + const char* serializedDevice + ) const; + + bool writeSerializedTrigger( + const HATrigger* trigger, + const char* serializedDevice + ) const; + + HATrigger* _triggers; + uint8_t _triggersNb; +}; + +#endif +#endif diff --git a/platformio.ini b/platformio.ini index 64f0b6d..13eab6a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -16,3 +16,11 @@ lib_deps = ; PubSubClient ; ArduinoJson@5.13.4 ; WiFiManager +upload_speed = 460800 + +; upload_protocol = espota +; upload_port = 192.168.1.244 +; upload_flags = +; -a'12345678' + + \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 0ff1a6b..da8ac3c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include #include #include +#include /* @@ -41,7 +42,7 @@ const int stepper_1 = D1; const int stepper_2 = D2; const int stepper_3 = D3; const int stepper_4 = D4; -const int onboard_led = 1; +const int onboard_led = 2; // Global Variable WiFiClient espClient; @@ -62,12 +63,17 @@ void callibrateMode(); void ICACHE_RAM_ATTR btnUpPressed (); void ICACHE_RAM_ATTR btnDownPressed (); +//HA +HADevice device(service_name.c_str()); +HAMqtt haMqtt(client, device); +HACover haCover(service_name.c_str()); // unique ID of the cover. You should define your own ID. void setup_ota() { // Set OTA Password, and change it in platformio.ini - ArduinoOTA.setPassword("ESP8266_PASSWORD"); + ArduinoOTA.setPassword("12345678"); + ArduinoOTA.setHostname(service_name.c_str()); ArduinoOTA.onStart([]() {}); ArduinoOTA.onEnd([]() {}); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {}); @@ -135,51 +141,69 @@ void stopMoving() saveStatus(); } -void btnUpPressed() -{ - while (digitalRead(btnUp) == LOW) - { - delay(0); +void handleButtons(){ + if (digitalRead(btnDown) == LOW){ + if (stepper.getStepsLeft() != 0) + { + stopMoving(); + return; + } + setPosition(0); + } + + if (digitalRead(btnUp) == LOW){ + if (stepper.getStepsLeft() != 0) + { + stopMoving(); + return; + } + setPosition(100); } - if (stepper.getStepsLeft() != 0) + + while (!digitalRead(btnDown) || !digitalRead(btnUp)) { - stopMoving(); - return; + delay(10); } + +} - setPosition(100); +void updateHomeKitTargetPosition(unsigned int targetPosition){ String value; String message; char data[100]; - message = "{\"name\" : \"" + device_name + "\", \"service_name\" : \"" + service_name + "\", \"characteristic\" : \"TargetPosition\", \"value\" : " + String(100) + "}"; + message = "{\"name\" : \"" + device_name + "\", \"service_name\" : \"" + service_name + "\", \"characteristic\" : \"TargetPosition\", \"value\" : " + String(targetPosition) + "}"; message.toCharArray(data, (message.length() + 1)); client.publish(mqtt_device_value_to_set_topic, data); } -void btnDownPressed() -{ - while (digitalRead(btnDown) == LOW) - { - delay(0); - - } - if (stepper.getStepsLeft() != 0) - { - stopMoving(); - return; - } +void onHACoverCommand(HACover::CoverCommand cmd) { + + + Serial.println("received command from HA"); + if (cmd == HACover::CommandOpen) { + Serial.println("Command: Open"); + haCover.setState(HACover::StateOpening); + + setPosition(75); + } else if (cmd == HACover::CommandClose) { + Serial.println("Command: Close"); + haCover.setState(HACover::StateClosing); + setPosition(0); + } else if (cmd == HACover::CommandStop) { + Serial.println("Command: Stop"); + haCover.setState(HACover::StateStopped); + setPosition(currentPositionPercent); + } - setPosition(0); - String value; - String message; - char data[100]; - message = "{\"name\" : \"" + device_name + "\", \"service_name\" : \"" + service_name + "\", \"characteristic\" : \"TargetPosition\", \"value\" : " + String(0) + "}"; - message.toCharArray(data, (message.length() + 1)); - client.publish(mqtt_device_value_to_set_topic, data); } void callback(char *topic, byte *payload, unsigned int length) { + //HA Lib topic + if (strcmp(topic,mqtt_device_value_from_set_topic) != 0){ + haMqtt.processMessage(topic,payload,length); + return; + } char c_payload[length]; memcpy(c_payload, payload, length); @@ -202,7 +226,7 @@ void callback(char *topic, byte *payload, unsigned int length) return; } - blink(); + // blink(); const char *characteristic = root["characteristic"]; if (strcmp(characteristic, "TargetPosition") == 0) @@ -222,10 +246,13 @@ void updateServerValue() message = "{\"name\" : \"" + device_name + "\", \"service_name\" : \"" + service_name + "\", \"characteristic\" : \"CurrentPosition\", \"value\" : " + String(currentPositionPercent) + "}"; message.toCharArray(data, (message.length() + 1)); client.publish(mqtt_device_value_to_set_topic, data); + haCover.setPosition(currentPositionPercent); } void setPosition(unsigned int positionPercent) { + updateHomeKitTargetPosition(positionPercent); + if (isInvert) { positionPercent = 100 - positionPercent; @@ -242,6 +269,7 @@ void setPosition(unsigned int positionPercent) targetPositionStep = 0; } + Serial.print("targetPositionStep = "); Serial.println(targetPositionStep); Serial.print("positionPercent = "); @@ -335,17 +363,35 @@ void callibrateMode() void setup() { - // WiFi.disconnect(); - // Serial.begin(9600); + ESP.wdtEnable(10000); + delay(500); //Stabilise voltage + + pinMode(stepper_1, OUTPUT); + pinMode(stepper_2, OUTPUT); + pinMode(stepper_3, OUTPUT); + pinMode(stepper_4, OUTPUT); + + digitalWrite(stepper_1, 0); + digitalWrite(stepper_2, 0); + digitalWrite(stepper_3, 0); + digitalWrite(stepper_4, 0); + + + Serial.begin(9600); + Serial.println("Start"); stepper = CheapStepper(stepper_1, stepper_2, stepper_3, stepper_4); stepper.begin(); stepper.setRpm(upRPM); + Serial.println("Stepper init OK"); + // Setup buttons pinMode(btnUp, INPUT_PULLUP); pinMode(btnDown, INPUT_PULLUP); EEPROM.begin(512); + Serial.println("EEPROM init OK"); + delay(5000); @@ -372,6 +418,8 @@ void setup() EEPROM.commit(); delay(300); } + Serial.println("LOAD Data init OK"); + // Setup networking WiFiManager wifiManager; @@ -387,17 +435,21 @@ void setup() ESP.reset(); //Restart if it can't connect. } - // setup_ota(); + setup_ota(); client.setServer(mqtt_server, mqtt_port); client.setCallback(callback); - //Attach interrupt for manual button controls - attachInterrupt(digitalPinToInterrupt(btnUp), btnUpPressed, FALLING); - attachInterrupt(digitalPinToInterrupt(btnDown), btnDownPressed, FALLING); - //Turn off led - pinMode(onboard_led, OUTPUT); - digitalWrite(onboard_led, HIGH); + //init HA + device.setName(device_name.c_str()); + device.setManufacturer("Magi"); + device.setModel("IKEA TUPPLUR Blind"); + haCover.onCommand(onHACoverCommand); + if (client.connected()){ + haMqtt.onConnectedLogic(); + } + + } void saveStatus() @@ -425,6 +477,7 @@ void saveStatus() // EEPROM.end(); delay(500); lastSavedValue = currentPositionStep; + haCover.setState(HACover::StateStopped); } } @@ -434,6 +487,9 @@ void loop() if (!client.connected()) { reconnect(); + if (client.connected()){ + haMqtt.onConnectedLogic(); + } } client.loop(); ArduinoOTA.handle(); @@ -443,9 +499,9 @@ void loop() if (stepper.getStepsLeft() != 0) { if (moveClockwise) - currentPositionStep = abs(targetPositionStep + abs(stepper.getStepsLeft())); + currentPositionStep = targetPositionStep + abs(stepper.getStepsLeft()); else - currentPositionStep = abs(targetPositionStep - abs(stepper.getStepsLeft())); + currentPositionStep = targetPositionStep - abs(stepper.getStepsLeft()); } currentPositionPercent = (int)round(((float)currentPositionStep / (float)maxPositionStep) * 100.0); @@ -453,4 +509,7 @@ void loop() currentPositionPercent = 100 - currentPositionPercent; saveStatus(); + ESP.wdtFeed(); + + handleButtons(); } \ No newline at end of file