Files
nixie-clock/firmware/uniclock/lib/arduino-home-assistant/src/HAMqtt.h
T
mangavalkandClaude Opus 5.5 9a7061383c Add Home Assistant controls over MQTT
Upgrade ArduinoHA from 1.3.0 to 2.1.0 and rewrite mqtt.ino for it.
The clock now shows up in Home Assistant (discovery prefix
"homeassistant") with:
- switches Tubes (blank the display, runtime only) and Auto dim
- numbers Day/Night brightness
- light Backlight (on/off, brightness, RGB mapped to the colour wheel)
- select LED effect, button Cathode protect
- sensors Light (LDR) and WiFi signal, plus the upstream sensors when
  the clock has them; master-clock subscriptions are kept

Settings changed from HA are saved to EEPROM 3 s after the last change.
Fixes the unbounded memcpy in the MQTT message handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:37:25 +01:00

466 lines
16 KiB
C++

#ifndef AHA_HAMQTT_H
#define AHA_HAMQTT_H
#include <Arduino.h>
#include <Client.h>
#include <IPAddress.h>
#include "ArduinoHADefines.h"
#define HAMQTT_CALLBACK(name) void (*name)()
#define HAMQTT_STATE_CALLBACK(name) void (*name)(ConnectionState state)
#define HAMQTT_MESSAGE_CALLBACK(name) void (*name)(const char* topic, const uint8_t* payload, uint16_t length)
#define HAMQTT_DEFAULT_PORT 1883
#ifdef ARDUINOHA_TEST
class PubSubClientMock;
#else
class PubSubClient;
#endif
#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__)
#define HAMQTT_DEFAULT_DEVICES_LIMIT 6
#else
#define HAMQTT_DEFAULT_DEVICES_LIMIT 24
#endif
class HADevice;
class HABaseDeviceType;
#if defined(ARDUINO_API_VERSION)
using namespace arduino;
#endif
/**
* This class is a wrapper for the PubSub API.
* It's a central point of the library where instances of all device types are stored.
*/
class HAMqtt
{
public:
enum ConnectionState {
StateConnecting = -5,
StateConnectionTimeout = -4,
StateConnectionLost = -3,
StateConnectionFailed = -2,
StateDisconnected = -1,
StateConnected = 0,
StateBadProtocol = 1,
StateBadClientId = 2,
StateUnavailable = 3,
StateBadCredentials = 4,
StateUnauthorized = 5
};
/**
* Returns existing instance (singleton) of the HAMqtt class.
* It may be a null pointer if the HAMqtt object was never constructed or it was destroyed.
*/
inline static HAMqtt* instance()
{ return _instance; }
#ifdef ARDUINOHA_TEST
explicit HAMqtt(
PubSubClientMock* pubSub,
HADevice& device,
const uint8_t maxDevicesTypesNb = HAMQTT_DEFAULT_DEVICES_LIMIT
);
#else
/**
* Creates a new instance of the HAMqtt class.
* Please note that only one instance of the class can be initialized at the same time.
*
* @param netClient The EthernetClient or WiFiClient that's going to be used for the network communication.
* @param device An instance of the HADevice class representing your device.
* @param maxDevicesTypesNb The maximum number of device types (sensors, switches, etc.) that you're going to implement.
*/
explicit HAMqtt(
Client& netClient,
HADevice& device,
const uint8_t maxDevicesTypesNb = HAMQTT_DEFAULT_DEVICES_LIMIT
);
#endif
/**
* Removes singleton of the HAMqtt class.
*/
~HAMqtt();
/**
* Sets the prefix of the Home Assistant discovery topics.
* It needs to match the prefix set in the HA admin panel.
* The default prefix is "homeassistant".
*
* @param prefix The discovery topics' prefix.
*/
inline void setDiscoveryPrefix(const char* prefix)
{ _discoveryPrefix = prefix; }
/**
* Returns the discovery topics' prefix.
*/
inline const char* getDiscoveryPrefix() const
{ return _discoveryPrefix; }
/**
* Sets prefix of the data topics.
* It may be useful if you want to pass MQTT traffic through a bridge.
* The default prefix is "aha".
*
* @param prefix The data topics' prefix.
*/
inline void setDataPrefix(const char* prefix)
{ _dataPrefix = prefix; }
/**
* Returns the data topics' prefix.
*/
inline const char* getDataPrefix() const
{ return _dataPrefix; }
/**
* Returns instance of the device assigned to the HAMqtt class.
* It's the same object (pointer) that was passed to the HAMqtt constructor.
*/
inline HADevice const* getDevice() const
{ return &_device; }
/**
* Registers a new callback method that will be called when the device receives an MQTT message.
* Please note that the callback is also fired by internal MQTT messages used by the library.
* You should always verify the topic of the received message.
*
* @param callback Callback method.
*/
inline void onMessage(HAMQTT_MESSAGE_CALLBACK(callback))
{ _messageCallback = callback; }
/**
* Registers a new callback method that will be called each time a connection to the MQTT broker is acquired.
* The callback is also fired after reconnecting to the broker.
* You can use this method to register topics' subscriptions.
*
* @param callback Callback method.
*/
inline void onConnected(HAMQTT_CALLBACK(callback))
{ _connectedCallback = callback; }
/**
* Registers a new callback method that will be called each time the connection to the MQTT broker is lost.
*
* @param callback Callback method.
*/
inline void onDisconnected(HAMQTT_CALLBACK(callback))
{ _disconnectedCallback = callback; }
/**
* Registers a new callback method that will be called each time the connection state changes.
*
* @param callback Callback method.
*/
inline void onStateChanged(HAMQTT_STATE_CALLBACK(callback))
{ _stateChangedCallback = callback; }
/**
* Returns the current state of the MQTT connection.
*/
inline ConnectionState getState() const
{ return _currentState; }
/**
* Sets parameters of the MQTT connection using the IP address and port.
* The library will try to connect to the broker in first loop cycle.
* Please note that the library automatically reconnects to the broker if connection is lost.
*
* @param serverIp IP address of the MQTT broker.
* @param serverPort Port of the MQTT broker.
* @param username Username for authentication. It can be nullptr if the anonymous connection needs to be performed.
* @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed.
*/
bool begin(
const IPAddress serverIp,
const uint16_t serverPort = HAMQTT_DEFAULT_PORT,
const char* username = nullptr,
const char* password = nullptr
);
/**
* Sets parameters of the MQTT connection using the IP address and the default port (1883).
* 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 username Username for authentication. It can be nullptr if the anonymous connection needs to be performed.
* @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed.
*/
bool begin(
const IPAddress serverIp,
const char* username,
const char* password
);
/**
* Sets parameters of the MQTT connection using the hostname and port.
* The library will try to connect to the broker in first loop cycle.
* Please note that the library automatically reconnects to the broker if connection is lost.
*
* @param serverHostname Hostname of the MQTT broker.
* @param serverPort Port of the MQTT broker.
* @param username Username for authentication. It can be nullptr if the anonymous connection needs to be performed.
* @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed.
*/
bool begin(
const char* serverHostname,
const uint16_t serverPort = HAMQTT_DEFAULT_PORT,
const char* username = nullptr,
const char* password = nullptr
);
/**
* Sets parameters of the MQTT connection using the hostname and the default port (1883).
* 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 serverHostname Hostname of the MQTT broker.
* @param username Username for authentication. It can be nullptr if the anonymous connection needs to be performed.
* @param password Password for authentication. It can be nullptr if the anonymous connection needs to be performed.
*/
bool begin(
const char* serverHostname,
const char* username,
const char* password
);
/**
* Closes the MQTT connection.
*/
bool disconnect();
/**
* This method should be called periodically inside the main loop of the firmware.
* It's safe to call this method in some interval (like 5ms).
*/
void loop();
/**
* Returns true if connection to the MQTT broker is established.
*/
bool isConnected() const;
/**
* Sets keep alive for the MQTT connection.
* By default it's 15 seconds.
*
* @param keepAlive Number of seconds to keep connection alive.
*/
void setKeepAlive(uint16_t keepAlive);
/**
* Sets the buffer size for the MQTT connection.
* By default it's 256 bytes.
*
* @param size Size of the buffer.
*/
bool setBufferSize(uint16_t size);
/**
* 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.
*
* @note The HAMqtt class doesn't take ownership of the given pointer.
* @param deviceType Instance of the device's type (HASwitch, HABinarySensor, etc.).
*/
void addDeviceType(HABaseDeviceType* deviceType);
/**
* Publishes the MQTT message with given topic and payload.
* Message won't be published if the connection with the MQTT broker is not established.
* In this case method returns false.
*
* @param topic The topic to publish.
* @param payload The payload to publish (it may be empty const char).
* @param retained Specifies whether message should be retained.
*/
bool publish(const char* topic, const char* payload, bool retained = false);
/**
* Begins publishing of a message with the given properties.
* When this method returns true the payload can be written using HAMqtt::writePayload method.
*
* @param topic Topic of the published message.
* @param payloadLength Length of the payload (bytes) that's going to be published.
* @param retained Specifies whether the published message should be retained.
*/
bool beginPublish(const char* topic, uint16_t payloadLength, bool retained = false);
/**
* Writes given string to the TCP stream.
* Please note that before writing any data the HAMqtt::beginPublish method
* needs to be called.
*
* @param data The string to publish.
* @param length Length of the data (bytes).
*/
void writePayload(const char* data, const uint16_t length);
/**
* Writes given data to the TCP stream.
* Please note that before writing any data the HAMqtt::beginPublish method
* needs to be called.
*
* @param data The data to publish.
* @param length Length of the data (bytes).
*/
void writePayload(const uint8_t* data, const uint16_t length);
/**
* Writes given progmem data to the TCP stream.
* Please note that before writing any data the HAMqtt::beginPublish method
* needs to be called.
*
* @param data Progmem data to publish.
*/
void writePayload(const __FlashStringHelper* data);
/**
* Finishes publishing of a message.
* After calling this method the message will be processed by the broker.
*/
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 the last will message that will be produced when the device disconnects from the broker.
* If you want to change availability of the device in Home Assistant panel
* please use enableLastWill() method from the HADevice class instead.
*
* @param lastWillTopic The topic to publish.
* @param lastWillMessage The message (payload) to publish.
* @param lastWillRetain Specifies whether the published message should be retained.
*/
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).
*
* @note Do not use this method on your own. It's only for the internal purpose.
* @param topic Topic of the message.
* @param payload Content of the message.
* @param length Length of the message.
*/
void processMessage(const char* topic, const uint8_t* payload, uint16_t length);
#ifdef ARDUINOHA_TEST
inline uint8_t getDevicesTypesNb() const
{ return _devicesTypesNb; }
inline HABaseDeviceType** getDevicesTypes() const
{ return _devicesTypes; }
#endif
private:
/// Interval between MQTT reconnects (milliseconds).
static const uint16_t ReconnectInterval = 10000;
/// Living instance of the HAMqtt class. It can be nullptr.
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();
/**
* Sets the state of the MQTT connection.
*/
void setState(ConnectionState state);
#ifdef ARDUINOHA_TEST
PubSubClientMock* _mqtt;
#else
/// Instance of the PubSubClient class. It's initialized in the constructor.
PubSubClient* _mqtt;
#endif
/// Instance of the HADevice passed to the constructor.
const HADevice& _device;
/// The callback method that will be called when an MQTT message is received.
HAMQTT_MESSAGE_CALLBACK(_messageCallback);
/// The callback method that will be called when the MQTT connection is acquired.
HAMQTT_CALLBACK(_connectedCallback);
/// The callback method that will be called when the MQTT connection is lost.
HAMQTT_CALLBACK(_disconnectedCallback);
/// The callback method that will be called when the MQTT connection state changes.
HAMQTT_STATE_CALLBACK(_stateChangedCallback);
/// Specifies whether the HAMqtt::begin method was ever called.
bool _initialized;
/// Teh discovery prefix that's used for the configuration messages.
const char* _discoveryPrefix;
/// The data prefix that's used for publishing data messages.
const char* _dataPrefix;
/// The username used for the authentication. It's set in the HAMqtt::begin method.
const char* _username;
/// The password used for the authentication. It's set in the HAMqtt::begin method.
const char* _password;
/// Time of the last connection attemps (milliseconds since boot).
uint32_t _lastConnectionAttemptAt;
/// The amount of registered devices types.
uint8_t _devicesTypesNb;
/// The maximum amount of devices types that can be registered.
uint8_t _maxDevicesTypesNb;
/// Pointers of all registered devices types (array of pointers).
HABaseDeviceType** _devicesTypes;
/// The last will topic set by HAMqtt::setLastWill
const char* _lastWillTopic;
/// The last will message set by HAMqtt::setLastWill
const char* _lastWillMessage;
/// The last will retain set by HAMqtt::setLastWill
bool _lastWillRetain;
/// The last known state of the MQTT connection.
ConnectionState _currentState;
};
#endif