Import ESP_UniClock (upstream 1189810)

Vendored from https://github.com/gpeter62/ESP_UniClock: the sketch
source, the web UI and the libraries the ESP8266 build needs, unpacked
from the bundled zips. Adds a PlatformIO project for the ESP-12. The
source is unchanged; local changes follow as separate commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-25 20:48:04 +01:00
1 parent 8efa810e4e
commit b347f1c63f
843 files changed
+122061

No files matched your search

@@ -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
@@ -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
@@ -0,0 +1,66 @@
#include <Ethernet.h>
#include <ArduinoHA.h>
#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();
}
}
@@ -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.
@@ -0,0 +1,59 @@
#include <Ethernet.h>
#include <ArduinoHA.h>
#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();
}
}
@@ -0,0 +1,46 @@
#include <Ethernet.h>
#include <ArduinoHA.h>
#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();
}
}
@@ -0,0 +1,49 @@
#include <Ethernet.h>
#include <ArduinoHA.h>
#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();
}
@@ -0,0 +1,53 @@
#include <Ethernet.h>
#include <ArduinoHA.h>
#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();
}
@@ -0,0 +1,102 @@
#include <ESP8266WiFi.h>
#include <ArduinoHA.h>
#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;
}
}
@@ -0,0 +1,50 @@
#include <Ethernet.h>
#include <ArduinoHA.h>
#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();
}
@@ -0,0 +1,56 @@
#include <Ethernet.h>
#include <ArduinoHA.h>
#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();
}
@@ -0,0 +1,42 @@
#include <Ethernet.h>
#include <ArduinoHA.h>
#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();
}
@@ -0,0 +1,52 @@
#include <Ethernet.h>
#include <ArduinoHA.h>
// This example uses JC Button library
// https://github.com/JChristensen/JC_Button
#include <JC_Button.h>
#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);
}
}
}
@@ -0,0 +1,53 @@
#include <WiFiNINA.h>
#include <ArduinoHA.h>
#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();
}
@@ -0,0 +1,60 @@
#include <ESP8266WiFi.h>
#include <ArduinoHA.h>
#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();
}
@@ -0,0 +1,54 @@
#include <Ethernet.h>
#include <ArduinoHA.h>
#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*
}
}
@@ -0,0 +1,10 @@
name=home-assistant-integration
version=1.3.0
author=Dawid Chyrzynski <dawid.chyrzynski@gmail.com>
maintainer=Dawid Chyrzynski <dawid.chyrzynski@gmail.com>
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
@@ -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
@@ -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
@@ -0,0 +1,198 @@
#include <Arduino.h>
#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
}
@@ -0,0 +1,53 @@
#ifndef AHA_HADEVICE_H
#define AHA_HADEVICE_H
#include <Arduino.h>
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
@@ -0,0 +1,331 @@
#include <PubSubClient.h>
#include "HAMqtt.h"
#include "HADevice.h"
#include "ArduinoHADefines.h"
#include "device-types/BaseDeviceType.h"
static const char* DefaultDiscoveryPrefix = "esp";
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<uint16_t>(length));
}
HAMqtt::HAMqtt(Client& netClient, HADevice& device) :
_netClient(netClient),
_device(device),
_messageCallback(nullptr),
_connectedCallback(nullptr),
_connectionFailedCallback(nullptr),
_initialized(false),
_discoveryPrefix(DefaultDiscoveryPrefix),
_dataPrefix(nullptr),
_mqtt(new PubSubClient(netClient)),
_username(nullptr),
_password(nullptr),
_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();
}
}
@@ -0,0 +1,243 @@
#ifndef AHA_HAMQTT_H
#define AHA_HAMQTT_H
#include <Client.h>
#include <IPAddress.h>
#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(Client& netClient, 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);
private:
static HAMqtt* _instance;
/**
* Attempts to connect to the MQTT broker.
* The method uses properties passed to the "begin" method.
*/
void connectToServer();
/**
* This method is called each time the connection with MQTT broker is acquired.
*/
void onConnectedLogic();
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
@@ -0,0 +1,67 @@
#ifdef ARDUINO_ARCH_SAMD
#include <avr/dtostrf.h>
#endif
#include <Arduino.h>
#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);
}
@@ -0,0 +1,33 @@
#ifndef AHA_HAUTILS_H
#define AHA_HAUTILS_H
#include <Arduino.h>
#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
@@ -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);
}
@@ -0,0 +1,72 @@
#ifndef AHA_BASEDEVICETYPE_H
#define AHA_BASEDEVICETYPE_H
#include <stdint.h>
#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
@@ -0,0 +1,421 @@
#include <Arduino.h>
#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);
}
@@ -0,0 +1,117 @@
#ifndef AHA_DEVICETYPESERIALIZER_H
#define AHA_DEVICETYPESERIALIZER_H
#include <stdint.h>
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large. Load diff
@@ -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
@@ -0,0 +1,218 @@
#ifdef ARDUINO_ARCH_SAMD
#include <avr/dtostrf.h>
#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
@@ -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<uint32_t>(value)); }
inline bool setValue(uint16_t value)
{ return setValue(static_cast<uint32_t>(value)); }
inline bool setValue(int8_t value)
{ return setValue(static_cast<int32_t>(value)); }
inline bool setValue(int16_t value)
{ return setValue(static_cast<int32_t>(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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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