From 8901d409ca6d2b13175e982cb1430808887bdba1 Mon Sep 17 00:00:00 2001 From: Erik Borowski Date: Sun, 1 Dec 2024 02:47:01 +0100 Subject: [PATCH] Add support for multiple light bars/remotes (#3), bump to version 0.2 --- Lightbar.ino | 61 +++++++- README.md | 21 +-- config-example.h | 31 +++- constants.h | 32 ++++ lightbar.cpp | 39 ++--- lightbar.h | 18 +-- mqtt.cpp | 386 +++++++++++++++++++++++++++++++---------------- mqtt.h | 33 ++-- radio.cpp | 149 +++++++++++++----- radio.h | 27 ++-- remote.cpp | 70 +++++++++ remote.h | 34 +++++ 12 files changed, 658 insertions(+), 243 deletions(-) create mode 100644 constants.h create mode 100644 remote.cpp create mode 100644 remote.h diff --git a/Lightbar.ino b/Lightbar.ino index 5ac1d22..997562f 100644 --- a/Lightbar.ino +++ b/Lightbar.ino @@ -1,24 +1,75 @@ +#include + +#include "constants.h" #include "config.h" +#include "radio.h" #include "lightbar.h" #include "mqtt.h" -Lightbar lightbar(INCOMING_SERIAL, OUTGOING_SERIAL, RADIO_PIN_CE, RADIO_PIN_CSN); -MQTT mqtt(&lightbar, WIFI_SSID, WIFI_PASSWORD, MQTT_SERVER, MQTT_PORT, MQTT_USER, MQTT_PASSWORD, MQTT_ROOT_TOPIC, HOME_ASSISTANT_DISCOVERY, HOME_ASSISTANT_DISCOVERY_PREFIX, HOME_ASSISTANT_DEVICE_NAME); +WiFiClient wifiClient; +Radio radio(RADIO_PIN_CE, RADIO_PIN_CSN); +MQTT mqtt(&wifiClient, MQTT_SERVER, MQTT_PORT, MQTT_USER, MQTT_PASSWORD, MQTT_ROOT_TOPIC, HOME_ASSISTANT_DISCOVERY, HOME_ASSISTANT_DISCOVERY_PREFIX); + +void setupWifi() +{ + Serial.print("[WiFi] Connecting to network \""); + Serial.print(WIFI_SSID); + Serial.print("\"..."); + + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + WiFi.setHostname(mqtt.getClientId().c_str()); + + uint retries = 0; + while (WiFi.status() != WL_CONNECTED) + { + delay(1000); + Serial.print("."); + retries++; + if (retries > 60) + ESP.restart(); + } + Serial.println(); + Serial.println("[WiFi] connected!"); + + Serial.print("[WiFi] IP address: "); + Serial.println(WiFi.localIP()); +} void setup() { Serial.begin(115200); Serial.println("##########################################"); - Serial.println("# LIGHTBAR2MQTT (Version 0.1) #"); + Serial.println("# LIGHTBAR2MQTT (Version " + constants::VERSION + ") #"); Serial.println("# https://github.com/ebinf/lightbar2mqtt #"); Serial.println("##########################################"); - lightbar.setup(); + radio.setup(); + + setupWifi(); + + for (int i = 0; i < sizeof(REMOTES) / sizeof(SerialWithName); i++) + { + Remote *remote = new Remote(&radio, REMOTES[i].serial, REMOTES[i].name); + mqtt.addRemote(remote); + } + + for (int i = 0; i < sizeof(LIGHTBARS) / sizeof(SerialWithName); i++) + { + Lightbar *lightbar = new Lightbar(&radio, LIGHTBARS[i].serial, LIGHTBARS[i].name); + mqtt.addLightbar(lightbar); + } + mqtt.setup(); } void loop() { + if (!WiFi.isConnected()) + { + Serial.println("[WiFi] connection lost!"); + setupWifi(); + } + mqtt.loop(); - lightbar.loop(); + radio.loop(); } \ No newline at end of file diff --git a/README.md b/README.md index a7ae320..2af1832 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This project would not have been possible without the work of these amazing peop - The light bar is represented as a `light` entity - The remote is represented as a `sensor` entity - Actions taken on the remote also trigger `device_automation`s. This allows you to trigger automations in Home Assistant based on actions taken on the remote +- Use multiple light bars or remotes with ease! Each one can be controlled/monitored individually and will also have separate entities in Home Assistant. ## Requirements @@ -54,14 +55,14 @@ Connect the nRF24 module to the ESP32 as follows. At least these pins work for m - [RF24](https://nrf24.github.io/RF24/) by TMRh20, _Version 1.4.10_ 5. Select your serial port and board. Upload the sketch to your ESP32. 6. Inspect the serial monitor (115200 baud). If everything is set up correctly, the ESP32 should connect to your WiFi and MQTT broker. -7. At this point, you probably don't know the serial of your remote. Just press/turn the remote and you should see the serial in the serial monitor. (E.g. `[Radio] Ignoring package with not matching serial: 0x7B7E12`) Copy it and paste it into the `config.h` file. +7. At this point, you probably don't know the serial of your remote. Just press/turn the remote and you should see the serial in the serial monitor. (E.g. `[Radio] Ignoring package with not matching serial: 0x7B7E12`) Copy it and paste it into the `config.h` file in the "Remotes" section. 8. Upload the sketch again. 9. If your Home Assistant has the MQTT integration set up, the light bar should be discovered automatically. 10. Enjoy controlling your light bar via MQTT! ### 3. Pairing Light bar and ESP32 (optional) -If you opt to use different values for `INCOMING_SERIAL` and `OUTGOING_SERIAL` in the `config.h` file, you need to pair the light bar with the ESP32. To do this, power-cycle the light bar and within 10 seconds either press the "Pair" button in Home Assistant or send a message to the pair topic (see below). The light bar should blink a few times if the pairing was successful. +If you opt to use different values for the serial and remote in the `config.h` file, you need to pair the light bar with the ESP32. To do this, power-cycle the light bar and within 10 seconds either press the "Pair" button in Home Assistant or send a message to the pair topic (see below). The light bar should blink a few times if the pairing was successful. ## Usage @@ -76,9 +77,11 @@ All MQTT topics are prefixed with a root topic. You can set this root topic in t ... ``` +In order to be able to control multiple light bars or remotes, each one has their own sub-topics, starting with the chosen serial (all lower case). + #### Light Bar -To control the light bar, you can send messages to the following topic: `/l2m_/lightbar/command` e.g. `lightbar2mqtt/l2m_1234567890AB/lightbar/command`. The payload should be a JSON object with the following keys: +To control the light bar, you can send messages to the following topic: `/l2m_/0x/command` e.g. `lightbar2mqtt/l2m_1234567890AB/0xabcdef/command`. The payload should be a JSON object with the following keys: - `state`: `"ON"` or `"OFF"` - `brightness`: `0` (off) to `15` (full brightness) @@ -96,7 +99,7 @@ Example: #### Remote -The remote sends its state to the following topic: `/l2m_/remote/state` e.g. `lightbar2mqtt/l2m_1234567890AB/remote/state`. The payload is a plain string with one the following values: +The remote sends its state to the following topic: `/l2m_/0x/state` e.g. `lightbar2mqtt/l2m_1234567890AB/0x123456/state`. The payload is a plain string with one the following values: - `press` - `turn_clockwise` @@ -107,7 +110,7 @@ The remote sends its state to the following topic: `/l2m_/l2m_/pair` e.g. `lightbar2mqtt/l2m_1234567890AB/pair`. The payload can be anything, it will be ignored. +To pair the light bar with the ESP32, send a message to the following topic: `/l2m_/0x/pair` e.g. `lightbar2mqtt/l2m_1234567890AB/0xabcdef/pair`. The payload can be anything, it will be ignored. Please note that the light bar needs to be power-cycled within 10 seconds _before_ sending the pairing message. @@ -117,21 +120,21 @@ The ESP32 sends its availability to the following topic: `/l2m_ ### Home Assistant -If your Home Assistant has the MQTT integration set up, the light bar should be discovered automatically. +If your Home Assistant has the MQTT integration set up, the light bar(s) and remote(s) should be discovered automatically. -Additionally, the above mentioned events from the remote are also available as `device_automation` triggers. You can use these triggers to create automations in Home Assistant based on the actions taken on the remote. To do so, create a new automation in Home Assistant and select "Device" as the trigger type. Select the light bar entity and the desired trigger, e.g. `"press" action`. +Additionally, the above mentioned events from the remote are also available as `device_automation` triggers. You can use these triggers to create automations in Home Assistant based on the actions taken on the remote. To do so, create a new automation in Home Assistant and select "Device" as the trigger type. Select the corresponding remote entity and the desired trigger, e.g. `"press" action`. ### Known Issues / Limitations - The light bar does not send its state to the ESP32. This means that if you change the state of the light bar via the controller, the ESP32 will not know about it. This is a limitation of the protocol used by the light bar. - There is no way of knowing whether the light bar is currently on or off. Therefore this project assumes that the light bar is on when the ESP32 starts. If you turn off the light bar (e.g. via the remote), this might lead to an inverted state in Home Assistant. Just turn the device on in Home Assistant and power-cycle the light bar to fix this. -- Sometimes actions taken on the remote are not recognized by the ESP32. When building automations in Home Assistant, don't rely on the remote events to be 100% accurate. Normally, the second or third try should work. +- Sometimes actions taken on the remote are not recognized by the ESP32. When building automations in Home Assistant, don't rely on the remote events to be 100% accurate. Normally, the second or third try should work. It is therefore also recommended to decouple the light bar and original remote, as otherwise some actions on the remote might change the state of the light bar but not trigger anything in Home Assistant. ## Contributing If you find a bug or have an idea for a new feature, feel free to open an issue or create a pull request. I'm happy to see this project grow and improve! -I've designed the code to be easily™ extendable. For example, it should be relatively easy to add support for multiple light bars or multiple remotes. +I've designed the code to be easily™ extendable. ~~For example, it should be relatively easy to add support for multiple light bars or multiple remotes.~~ ## License diff --git a/config-example.h b/config-example.h index 759bf22..1a8a398 100644 --- a/config-example.h +++ b/config-example.h @@ -1,3 +1,5 @@ +#include "constants.h" + /* -- nRF24 --------------------------------------------------------------------------------------------------- */ // The pin number to which the nRF24's Chip Enable (CE) pin is connected. #define RADIO_PIN_CE 4 @@ -5,16 +7,29 @@ // The pin number to which the nRF24's Chip Select Null (CSN) pin is connected. #define RADIO_PIN_CSN 5 -/* -- Light Bar ----------------------------------------------------------------------------------------------- */ -// The serial of the remote to listen to. +/* -- Light Bars ---------------------------------------------------------------------------------------------- */ +// All light bars that should be controlled by this controller. Each light bar must have a unique serial. +// Each entry consists of the serial and the name of the light bar. By default, up to 10 light bars can be added. +// +// If the serial is set to the same value as one remote's, the original remote will still control the light bar +// directly. To separate the light bar from the original remote, set this to a different value, e.g. 0xABCDEF. +// +// The name will be used in Home Assistant. +constexpr SerialWithName LIGHTBARS[] = { + {0xABCDEF, "Light Bar 1"}, +}; + +/* -- Remotes ------------------------------------------------------------------------------------------------- */ +// All remotes that this controller should listen to. Each remote must have a unique serial. +// Each entry consists of the serial and the name of the remote. By default, up to 10 remotes can be added. +// // If you don't know the serial of your remote, just set this to any value and flash your controller. Once // the controller is running, the serial of your remote will be printed to the console. -#define INCOMING_SERIAL 0x123456 - -// The serial, the controller should emulate. -// If set to the same value as INCOMING_SERIAL, the original remote will still control the ligtbar directly. -// To separate the light bar from the original remote, set this to a different value, e.g. 0xABCDEF. -#define OUTGOING_SERIAL 0xABCDEF +// +// The name will be used in Home Assistant. +constexpr SerialWithName REMOTES[] = { + {0x123456, "Remote 1"}, +}; /* -- WiFi ---------------------------------------------------------------------------------------------------- */ // The SSID of the WiFi network to connect to. diff --git a/constants.h b/constants.h new file mode 100644 index 0000000..df593c8 --- /dev/null +++ b/constants.h @@ -0,0 +1,32 @@ +#ifndef CONSTANTS_H +#define CONSTANTS_H + +#include +#include + +namespace constants +{ + // The version number of lightbar2mqtt. + const String VERSION = "0.2"; + + // The maximum number of light bars that can be connected to the controller. + const uint8_t MAX_LIGHTBARS = 10; + + // The maximum number of remotes that can be connected to the controller. + const uint8_t MAX_REMOTES = 10; + + // The maximum number of serials, the controller will be able to save latest package ids for. + // This should always >= MAX_REMOTES + MAX_LIGHTBARS. + const uint8_t MAX_SERIALS = 32; + + // The maximum number of command listeners that can be registered for a remote. + const uint8_t MAX_COMMAND_LISTENERS = 10; +}; + +struct SerialWithName +{ + uint32_t serial; + const char *name; +}; + +#endif \ No newline at end of file diff --git a/lightbar.cpp b/lightbar.cpp index e1f15c4..0330ec0 100644 --- a/lightbar.cpp +++ b/lightbar.cpp @@ -1,30 +1,41 @@ #include "lightbar.h" -Lightbar::Lightbar(uint32_t incoming_serial, uint32_t outgoing_serial, uint8_t ce, uint8_t csn) +Lightbar::Lightbar(Radio *radio, uint32_t serial, const char *name) { - this->radio = new Radio(ce, csn); - this->radio->setOutgoingSerial(outgoing_serial); - this->incoming_serial = incoming_serial; + this->radio = radio; + this->serial = serial; + this->name = name; + + this->serialString = "0x" + String(this->serial, HEX); } Lightbar::~Lightbar() { - delete this->radio; } -void Lightbar::setup() +uint32_t Lightbar::getSerial() { - this->radio->setup(); + return this->serial; +} + +const String Lightbar::getSerialString() +{ + return this->serialString; +} + +const char *Lightbar::getName() +{ + return this->name; } void Lightbar::sendRawCommand(Command command, byte options) { - this->radio->sendCommand(command, options); + this->radio->sendCommand(serial, command, options); } void Lightbar::sendRawCommand(Command command) { - this->radio->sendCommand(command); + this->radio->sendCommand(serial, command); } void Lightbar::onOff() @@ -93,14 +104,4 @@ void Lightbar::setBrightness(uint8_t value) // for details. this->sendRawCommand(Lightbar::Command::DIMMER, 0x0 - 16); this->sendRawCommand(Lightbar::Command::BRIGHTER, (byte)value); -} - -void Lightbar::loop() -{ - this->radio->loop(); -} - -bool Lightbar::registerCommandListener(std::function callback) -{ - return this->radio->addIncomingSerial(this->incoming_serial, callback); } \ No newline at end of file diff --git a/lightbar.h b/lightbar.h index 7ed5c29..4ea7eba 100644 --- a/lightbar.h +++ b/lightbar.h @@ -1,14 +1,16 @@ #ifndef LIGHTBAR_H #define LIGHTBAR_H -#include "radio.h"; +#include "radio.h" class Lightbar { public: - Lightbar(uint32_t incoming_serial, uint32_t outgoing_serial, uint8_t ce, uint8_t csn); + Lightbar(Radio *radio, uint32_t serial, const char *name); ~Lightbar(); - void setup(); + uint32_t getSerial(); + const String getSerialString(); + const char *getName(); enum Command { @@ -34,16 +36,12 @@ public: void setMiredTemperature(uint mireds); void setBrightness(uint8_t value); - void loop(); - - bool registerCommandListener(std::function callback); - - static void callback(uint32_t serial, byte command, byte options); - private: Radio *radio; bool onState = false; - uint32_t incoming_serial; + uint32_t serial; + String serialString; + const char *name; }; #endif \ No newline at end of file diff --git a/mqtt.cpp b/mqtt.cpp index e206801..c2fe54b 100644 --- a/mqtt.cpp +++ b/mqtt.cpp @@ -1,12 +1,10 @@ -#include "mqtt.h" - #include #include -MQTT::MQTT(Lightbar *lightbar, const char *wifiSsid, const char *wifiPassword, const char *mqttServer, int mqttPort, const char *mqttUser, const char *mqttPassword, const char *mqttRootTopic, bool homeAssistantAutoDiscovery, const char *homeAssistantAutoDiscoveryPrefix, const char *homeAssistantDeviceName) +#include "mqtt.h" + +MQTT::MQTT(WiFiClient *wifiClient, const char *mqttServer, int mqttPort, const char *mqttUser, const char *mqttPassword, const char *mqttRootTopic, bool homeAssistantAutoDiscovery, const char *homeAssistantAutoDiscoveryPrefix) { - this->wifiSsid = wifiSsid; - this->wifiPassword = wifiPassword; this->mqttServer = mqttServer; this->mqttPort = mqttPort; this->mqttUser = mqttUser; @@ -14,12 +12,10 @@ MQTT::MQTT(Lightbar *lightbar, const char *wifiSsid, const char *wifiPassword, c this->mqttRootTopic = String(mqttRootTopic); this->homeAssistantDiscovery = homeAssistantAutoDiscovery; this->homeAssistantDiscoveryPrefix = String(homeAssistantAutoDiscoveryPrefix); - this->homeAssistantDeviceName = String(homeAssistantDeviceName); - this->wifiClient = new WiFiClient(); - this->client = new PubSubClient(*this->wifiClient); - this->lightbar = lightbar; - this->lightbar->registerCommandListener(std::bind(&MQTT::sendAction, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->remoteCommandHandler = std::bind(&MQTT::sendAction, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + this->client = new PubSubClient(*wifiClient); String mac = ""; unsigned char mac_base[6] = {0}; @@ -30,12 +26,22 @@ MQTT::MQTT(Lightbar *lightbar, const char *wifiSsid, const char *wifiPassword, c mac = buffer; } this->clientId = "l2m_" + mac; + this->combinedRootTopic = this->mqttRootTopic + "/" + this->clientId; } MQTT::~MQTT() { delete this->client; - delete this->wifiClient; +} + +const String MQTT::getCombinedRootTopic() +{ + return this->combinedRootTopic; +} + +const String MQTT::getClientId() +{ + return this->clientId; } void MQTT::onMessage(char *topic, byte *payload, unsigned int length) @@ -49,93 +55,40 @@ void MQTT::onMessage(char *topic, byte *payload, unsigned int length) } Serial.println(); - if (!strcmp(topic, String(this->mqttRootTopic + "/" + this->clientId + "/pair").c_str())) - { - this->lightbar->pair(); - return; - } - JSONVar command = JSON.parse(String(payload, length)); - if (JSON.typeof(command) != "object") - return; - - if (command.hasOwnProperty("state")) + Lightbar *lightbar = nullptr; + for (int i = 0; i < this->lightbarCount; i++) { - const char *state = command["state"]; - this->lightbar->setOnOff(strcmp(state, "ON")); - } - - if (command.hasOwnProperty("brightness")) - { - this->lightbar->setBrightness((uint8_t)command["brightness"]); - } - - if (command.hasOwnProperty("color_temp")) - { - this->lightbar->setMiredTemperature((uint)command["color_temp"]); - } -} - -void MQTT::setupWifi() -{ - Serial.print("[WiFi] Connecting to network \""); - Serial.print(this->wifiSsid); - Serial.print("\"..."); - - WiFi.begin(this->wifiSsid, this->wifiPassword); - WiFi.setHostname(this->clientId.c_str()); - - uint retries = 0; - while (WiFi.status() != WL_CONNECTED) - { - delay(1000); - Serial.print("."); - retries++; - if (retries > 60) - ESP.restart(); - } - Serial.println(); - Serial.println("[WiFi] connected!"); - - Serial.print("[WiFi] IP address: "); - Serial.println(WiFi.localIP()); -} - -void MQTT::setupMqtt() -{ - this->client->setServer(this->mqttServer, this->mqttPort); - this->client->setCallback(std::bind(&MQTT::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); - - while (!this->client->connected()) - { - Serial.println("[MQTT] Connecting to MQTT broker..."); - uint retries = 0; - if (this->client->connect(this->clientId.c_str(), this->mqttUser, this->mqttPassword, String(this->mqttRootTopic + "/" + this->clientId + "/availability").c_str(), 1, true, "offline")) + lightbar = this->lightbars[i]; + if (!strcmp(topic, String(this->getCombinedRootTopic() + "/" + lightbar->getSerialString() + "/pair").c_str())) { - Serial.println("[MQTT] connected!"); - this->client->publish(String(this->mqttRootTopic + "/" + this->clientId + "/availability").c_str(), "online", true); - this->client->subscribe(String(this->mqttRootTopic + "/" + this->clientId + "/lightbar/command").c_str()); - this->client->subscribe(String(this->mqttRootTopic + "/" + this->clientId + "/pair").c_str()); + lightbar->pair(); + return; } - else + + if (strcmp(topic, String(this->getCombinedRootTopic() + "/" + lightbar->getSerialString() + "/command").c_str())) + continue; + + if (JSON.typeof(command) != "object") + continue; + + if (command.hasOwnProperty("state")) { - Serial.print("[MQTT] Connection failed! rc="); - Serial.print(this->client->state()); - Serial.println(" try again in 1 second"); - while (WiFi.status() != WL_CONNECTED) - { - this->setupWifi(); - } - delay(1000); - retries++; - if (retries > 60) - ESP.restart(); + const char *state = command["state"]; + lightbar->setOnOff(strcmp(state, "ON")); + } + + if (command.hasOwnProperty("brightness")) + { + lightbar->setBrightness((uint8_t)command["brightness"]); + } + + if (command.hasOwnProperty("color_temp")) + { + lightbar->setMiredTemperature((uint)command["color_temp"]); } } - - if (homeAssistantDiscovery) - this->sendHomeAssistantDiscoveryMessages(); } void MQTT::setup() @@ -143,33 +96,156 @@ void MQTT::setup() Serial.print("[MQTT] Device ID: "); Serial.println(this->clientId); Serial.print("[MQTT] Root Topic: "); - Serial.println(this->mqttRootTopic + "/" + this->clientId); + Serial.println(this->getCombinedRootTopic()); - this->setupWifi(); - this->setupMqtt(); + this->client->setServer(this->mqttServer, this->mqttPort); + this->client->setCallback(std::bind(&MQTT::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + + while (!this->client->connected()) + { + Serial.println("[MQTT] Connecting to MQTT broker..."); + uint retries = 0; + if (this->client->connect(this->clientId.c_str(), this->mqttUser, this->mqttPassword, String(this->getCombinedRootTopic() + "/availability").c_str(), 1, true, "offline")) + { + Serial.println("[MQTT] connected!"); + this->client->publish(String(this->getCombinedRootTopic() + "/availability").c_str(), "online", true); + this->client->subscribe(String(this->getCombinedRootTopic() + "/+/command").c_str()); + this->client->subscribe(String(this->getCombinedRootTopic() + "/+/pair").c_str()); + } + else + { + Serial.print("[MQTT] Connection failed! rc="); + Serial.print(this->client->state()); + Serial.println(" try again in 1 second"); + delay(1000); + retries++; + if (retries > 60) + ESP.restart(); + } + } + + this->sendAllHomeAssistantDiscoveryMessages(); } -void MQTT::sendHomeAssistantDiscoveryMessages() +bool MQTT::addLightbar(Lightbar *lightbar) { + if (this->lightbarCount >= constants::MAX_LIGHTBARS) + { + Serial.println("[MQTT] Could not add light bar, because too many light bars are saved!"); + Serial.println("[MQTT] Please check if you actually want to save more than " + String(constants::MAX_LIGHTBARS, DEC) + " light bars."); + Serial.println("[MQTT] If you do, increase MAX_LIGHTBARS in constants.h and recompile."); + return false; + } + this->lightbars[this->lightbarCount] = lightbar; + this->lightbarCount++; + this->sendHomeAssistantLightbarDiscoveryMessages(lightbar); + return true; +} + +bool MQTT::removeLightbar(Lightbar *lightbar) +{ + for (int i = 0; i < this->lightbarCount; i++) + { + if (this->lightbars[i] == lightbar) + { + for (int j = i; j < this->lightbarCount - 1; j++) + { + this->lightbars[j] = this->lightbars[j + 1]; + } + this->lightbarCount--; + return true; + } + } + return false; +} + +bool MQTT::addRemote(Remote *remote) +{ + if (this->remoteCount >= constants::MAX_REMOTES) + { + Serial.println("[MQTT] Could not add remote, because too many remotes are saved!"); + Serial.println("[MQTT] Please check if you actually want to save more than " + String(constants::MAX_REMOTES, DEC) + " remotes."); + Serial.println("[MQTT] If you do, increase MAX_REMOTES in constants.h and recompile."); + return false; + } + this->remotes[this->remoteCount] = remote; + this->remoteCount++; + remote->registerCommandListener(this->remoteCommandHandler); + this->sendHomeAssistantRemoteDiscoveryMessages(remote); + return true; +} + +bool MQTT::removeRemote(Remote *remote) +{ + for (int i = 0; i < this->remoteCount; i++) + { + if (this->remotes[i] == remote) + { + this->remotes[i]->registerCommandListener(this->remoteCommandHandler); + for (int j = i; j < this->remoteCount - 1; j++) + { + this->remotes[j] = this->remotes[j + 1]; + } + this->remoteCount--; + return true; + } + } + return false; +} + +void MQTT::sendAllHomeAssistantDiscoveryMessages() +{ + if (!this->homeAssistantDiscovery) + return; + for (int i = 0; i < this->lightbarCount; i++) + { + this->sendHomeAssistantLightbarDiscoveryMessages(this->lightbars[i]); + } + for (int i = 0; i < this->remoteCount; i++) + { + this->sendHomeAssistantRemoteDiscoveryMessages(this->remotes[i]); + } +} + +void MQTT::sendHomeAssistantLightbarDiscoveryMessages(Lightbar *lightbar) +{ + if (!this->homeAssistantDiscovery) + return; + + Serial.print("[MQTT] Sending lightbar discovery messages for "); + Serial.println(lightbar->getSerialString()); + + const String topicClient = this->clientId + "_" + lightbar->getSerialString(); const String baseConfig = R"json( "schema": "json", "o": { "name": "lightbar2mqtt", - "sw_version": "0.1", + "sw_version": ")json" + + constants::VERSION + + R"json(", "support_url": "https://github.com/ebinf/lightbar2mqtt" }, - "~": ")json" + this->mqttRootTopic + + "~": ")json" + this->getCombinedRootTopic() + "/" + - this->clientId + + lightbar->getSerialString() + R"json(", - "availability_topic": "~/availability", - "dev": { - "ids": ")json" + this->clientId + + "availability_topic": ")json" + + this->getCombinedRootTopic() + R"json(/availability", + "dev": + { + "ids" : ")json" + topicClient + R"json(", - "name": ")json" + homeAssistantDeviceName + + "name": ")json" + + lightbar->getName() + R"json(", - "mdl": "MJGJD01YL", - "mf": "Xiaomi" + "mdl": "Mi Computer Monitor Light Bar (MJGJD01YL)", + "mf": "Xiaomi", + "sw": "lightbar2mqtt )json" + + constants::VERSION + + R"json(", + "sn": ")json" + + lightbar->getSerialString() + + R"json(" },)json"; String rendevous_str = "{" + @@ -181,30 +257,16 @@ void MQTT::sendHomeAssistantDiscoveryMessages() "brightness": true, "brightness_scale": 15, "name": "Light bar", - "cmd_t": "~/lightbar/command", - "uniq_id": ")json" + this->clientId + + "cmd_t": "~/command", + "uniq_id": ")json" + topicClient + R"json(_lightbar", "max_mireds": 370, "min_mireds":153, - "p": "light" + "p": "light", + "icon": "mdi:wall-sconce-flat" )json" + "}"; - this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/light/" + this->clientId + "/lightbar/config").c_str(), rendevous_str.length(), true); - this->client->print(rendevous_str); - this->client->endPublish(); - - rendevous_str = "{" + - baseConfig + - R"json( - "name": "Remote", - "state_topic": "~/remote/state", - "uniq_id": ")json" + - this->clientId + R"json(_remote", - "value_template": "{{ value }}", - "p": "sensor" - )json" + "}"; - - this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/sensor/" + this->clientId + "/remote/config").c_str(), rendevous_str.length(), true); + this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/light/" + topicClient + "/lightbar/config").c_str(), rendevous_str.length(), true); this->client->print(rendevous_str); this->client->endPublish(); @@ -214,10 +276,66 @@ void MQTT::sendHomeAssistantDiscoveryMessages() "name": "Pair", "cmd_t": "~/pair", "uniq_id": ")json" + - this->clientId + R"json(_pair", + topicClient + R"json(_pair", "p": "button" )json" + "}"; - this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/button/" + this->clientId + "/pair/config").c_str(), rendevous_str.length(), true); + this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/button/" + topicClient + "/pair/config").c_str(), rendevous_str.length(), true); + this->client->print(rendevous_str); + this->client->endPublish(); +} + +void MQTT::sendHomeAssistantRemoteDiscoveryMessages(Remote *remote) +{ + if (!this->homeAssistantDiscovery) + return; + + Serial.print("[MQTT] Sending remote discovery messages for "); + Serial.println(remote->getSerialString()); + + const String topicClient = this->clientId + "_" + remote->getSerialString(); + const String baseConfig = R"json( + "schema": "json", + "o": { + "name": "lightbar2mqtt", + "sw_version": ")json" + + constants::VERSION + + R"json(", + "support_url": "https://github.com/ebinf/lightbar2mqtt" + }, + "~": ")json" + this->getCombinedRootTopic() + + "/" + + remote->getSerialString() + + R"json(", + "availability_topic": ")json" + + this->getCombinedRootTopic() + R"json(/availability", + "dev": { + "ids": ")json" + topicClient + + R"json(", + "name": ")json" + remote->getName() + + R"json(", + "mdl": "Mi Computer Monitor Light Bar Remote Control (MJGJD01YL)", + "mf": "Xiaomi", + "sw": "lightbar2mqtt )json" + + constants::VERSION + + R"json(", + "sn": ")json" + remote->getSerialString() + + R"json(" + },)json"; + + String rendevous_str = "{" + + baseConfig + + R"json( + "name": "Remote", + "state_topic": "~/state", + "uniq_id": ")json" + + topicClient + R"json(_remote", + "value_template": "{{ value }}", + "enabled_by_default": true, + "entity_category": "diagnostic", + "icon": "mdi:gesture-double-tap" + )json" + "}"; + + this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/sensor/" + topicClient + "/remote/config").c_str(), rendevous_str.length(), true); this->client->print(rendevous_str); this->client->endPublish(); @@ -241,11 +359,11 @@ void MQTT::sendHomeAssistantDiscoveryMessages() "subtype": ")json" + cmd + R"json(", "type": "action", - "topic": "~/remote/state", + "topic": "~/state", "p": "device_automation" )json" + "}"; - this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/device_automation/" + this->clientId + "/action_" + cmd + "/config").c_str(), rendevous_str.length(), true); + this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/device_automation/" + topicClient + "/action_" + cmd + "/config").c_str(), rendevous_str.length(), true); this->client->print(rendevous_str); this->client->endPublish(); } @@ -256,14 +374,14 @@ void MQTT::loop() if (!this->client->connected()) { Serial.println("[MQTT] connection lost!"); - this->setupMqtt(); + this->setup(); } this->client->loop(); } -void MQTT::sendAction(uint32_t serial, byte command, byte options) +void MQTT::sendAction(Remote *remote, byte command, byte options) { - const char *action; + String action; switch ((uint8_t)command) { case Lightbar::Command::ON_OFF: @@ -293,5 +411,13 @@ void MQTT::sendAction(uint32_t serial, byte command, byte options) default: return; } - this->client->publish(String(this->mqttRootTopic + "/" + this->clientId + "/remote/state").c_str(), action); + + String topic = String(this->getCombinedRootTopic() + "/" + remote->getSerialString() + "/state"); + Serial.print("[MQTT] Sending message ("); + Serial.print(topic); + Serial.print("): "); + Serial.println(action); + this->client->publish(topic.c_str(), action.c_str()); + delay(200); + this->client->publish(topic.c_str(), NULL); } \ No newline at end of file diff --git a/mqtt.h b/mqtt.h index 88150bf..e52fd62 100644 --- a/mqtt.h +++ b/mqtt.h @@ -1,27 +1,40 @@ #include #include + +#include "constants.h" #include "lightbar.h" +#include "remote.h" #ifndef MQTT_H #define MQTT_H +class Remote; +class Lightbar; + class MQTT { public: - MQTT(Lightbar *lightbar, const char *wifiSsid, const char *wifiPassword, const char *mqttServer, int mqttPort, const char *mqttUser, const char *mqttPassword, const char *mqttRootTopic, bool homeAssistantAutoDiscovery, const char *homeAssistantAutoDiscoveryPrefix, const char *homeAssistantDeviceName); + MQTT(WiFiClient *wifiClient, const char *mqttServer, int mqttPort, const char *mqttUser, const char *mqttPassword, const char *mqttRootTopic, bool homeAssistantAutoDiscovery, const char *homeAssistantAutoDiscoveryPrefix); ~MQTT(); void setup(); void loop(); + bool addLightbar(Lightbar *lightbar); + bool removeLightbar(Lightbar *lightbar); + bool addRemote(Remote *remote); + bool removeRemote(Remote *remote); void onMessage(char *topic, byte *payload, unsigned int length); - void sendAction(uint32_t serial, byte command, byte options); + void sendAction(Remote *remote, byte command, byte options); + const String getCombinedRootTopic(); + const String getClientId(); private: WiFiClient *wifiClient; PubSubClient *client; String clientId; - Lightbar *lightbar; - const char *wifiSsid; - const char *wifiPassword; + Lightbar *lightbars[constants::MAX_LIGHTBARS]; + int lightbarCount = 0; + Remote *remotes[constants::MAX_REMOTES]; + int remoteCount = 0; const char *mqttServer; int mqttPort = 1883; const char *mqttUser = ""; @@ -29,11 +42,13 @@ private: String mqttRootTopic = "lightbar2mqtt"; bool homeAssistantDiscovery = true; String homeAssistantDiscoveryPrefix = "homeassistant"; - String homeAssistantDeviceName = "Mi Computer Monitor Light Bar"; - void setupWifi(); - void setupMqtt(); - void sendHomeAssistantDiscoveryMessages(); + String combinedRootTopic; + std::function remoteCommandHandler; + + void sendAllHomeAssistantDiscoveryMessages(); + void sendHomeAssistantLightbarDiscoveryMessages(Lightbar *lightbar); + void sendHomeAssistantRemoteDiscoveryMessages(Remote *remote); }; #endif \ No newline at end of file diff --git a/radio.cpp b/radio.cpp index 1439e05..be9c589 100644 --- a/radio.cpp +++ b/radio.cpp @@ -21,27 +21,52 @@ Radio::~Radio() this->radio.powerDown(); } -void Radio::setOutgoingSerial(uint32_t serial) +bool Radio::addRemote(Remote *remote) { - this->outgoing_serial = serial; -} - -bool Radio::addIncomingSerial(uint32_t serial, std::function callback) -{ - if (this->num_remotes >= MAX_REMOTES) + if (this->num_remotes >= constants::MAX_REMOTES) + { + Serial.println("[Radio] Could not add remote, because too many remotes are saved!"); + Serial.println("[Radio] Please check if you actually want to save more than " + String(constants::MAX_REMOTES, DEC) + " remotes."); + Serial.println("[Radio] If you do, increase MAX_REMOTES in constants.h and recompile."); return false; - this->remotes[this->num_remotes].serial = serial; - this->remotes[this->num_remotes].last_received_package_id = 0; - this->remotes[this->num_remotes].callback = callback; + } + if (this->num_package_ids >= constants::MAX_SERIALS) + { + Serial.println("[Radio] Could not add remote, because too many serials are saved!"); + Serial.println("[Radio] Please check if you actually want to save more than " + String(constants::MAX_SERIALS, DEC) + " serials."); + Serial.println("[Radio] If you do, increase MAX_SERIALS in constants.h and recompile."); + return false; + } + this->remotes[this->num_remotes] = remote; this->num_remotes++; + this->package_ids[this->num_package_ids].serial = remote->getSerial(); + this->package_ids[this->num_package_ids].package_id = 0; + this->num_package_ids++; + Serial.print("[Radio] Remote "); + Serial.print(remote->getSerialString()); + Serial.println(" added!"); + return true; } -bool Radio::removeIncomingSerial(uint32_t serial) +bool Radio::removeRemote(Remote *remote) { for (int i = 0; i < this->num_remotes; i++) { - if (this->remotes[i].serial == serial) + if (this->remotes[i] == remote) { + for (int j = 0; j < this->num_package_ids; j++) + { + if (this->package_ids[j].serial == remote->getSerial()) + { + for (int k = j; k < this->num_package_ids - 1; k++) + { + this->package_ids[k] = this->package_ids[k + 1]; + } + this->num_package_ids--; + break; + } + } + for (int j = i; j < this->num_remotes - 1; j++) { this->remotes[j] = this->remotes[j + 1]; @@ -53,15 +78,39 @@ bool Radio::removeIncomingSerial(uint32_t serial) return false; } -void Radio::sendCommand(byte command, byte options) +void Radio::sendCommand(uint32_t serial, byte command, byte options) { + PackageIdForSerial *package_id = nullptr; + for (int i = 0; i < this->num_package_ids; i++) + { + if (this->package_ids[i].serial == serial) + { + package_id = &this->package_ids[i]; + break; + } + } + if (package_id == nullptr) + { + if (this->num_package_ids >= constants::MAX_SERIALS) + { + Serial.println("[Radio] Could not send command, because too many serials are saved!"); + Serial.println("[Radio] Please check if you actually want to save more than " + String(constants::MAX_SERIALS, DEC) + " serials."); + Serial.println("[Radio] If you do, increase MAX_SERIALS in constants.h and recompile."); + return; + } + package_id = &this->package_ids[this->num_package_ids]; + package_id->serial = serial; + package_id->package_id = 0; + this->num_package_ids++; + } + byte data[17] = {0}; memcpy(data, Radio::preamble, sizeof(Radio::preamble)); - data[8] = (this->outgoing_serial & 0xFF0000) >> 16; - data[9] = (this->outgoing_serial & 0x00FF00) >> 8; - data[10] = this->outgoing_serial & 0x0000FF; + data[8] = (serial & 0xFF0000) >> 16; + data[9] = (serial & 0x00FF00) >> 8; + data[10] = serial & 0x0000FF; data[11] = 0xFF; - data[12] = this->last_sent_package_id; + data[12] = ++package_id->package_id; data[13] = command; data[14] = options; @@ -71,8 +120,6 @@ void Radio::sendCommand(byte command, byte options) data[15] = (checksum & 0xFF00) >> 8; data[16] = checksum & 0x00FF; - this->last_sent_package_id += 1; - Serial.print("[Radio] Sending command: 0x"); for (int i = 0; i < 17; i++) { @@ -89,9 +136,9 @@ void Radio::sendCommand(byte command, byte options) this->radio.startListening(); } -void Radio::sendCommand(byte command) +void Radio::sendCommand(uint32_t serial, byte command) { - return this->sendCommand(command, 0x0); + return this->sendCommand(serial, command, 0x0); } void Radio::setup() @@ -135,10 +182,12 @@ void Radio::loop() delay(1000); } - // Only continue if there is a package available. - if (!this->radio.available()) - return; + if (this->radio.available()) + this->handlePackage(); +} +void Radio::handlePackage() +{ // Read raw data, append a 5 and shift it. See // https://github.com/lamperez/xiaomi-lightbar-nrf24?tab=readme-ov-file#baseband-packet-format // on why that is necessary. @@ -169,33 +218,51 @@ void Radio::loop() } // Check if package is coming from a observed remote. - bool found = false; + Remote *remote = nullptr; uint32_t serial = data[8] << 16 | data[9] << 8 | data[10]; for (int i = 0; i < this->num_remotes; i++) { - Remote remote = this->remotes[i]; - if (serial != remote.serial) - continue; - found = true; - - // Make sure the same package was not handled before. - uint8_t package_id = data[12]; - if (package_id <= remote.last_received_package_id && package_id > remote.last_received_package_id - 64) + if (serial == this->remotes[i]->getSerial()) { - Serial.println("[Radio] Ignoring package with too low package number!"); - continue; + remote = this->remotes[i]; + break; } - remote.last_received_package_id = package_id; - - Serial.println("[Radio] Package received!"); - remote.callback(remote.serial, data[13], data[14]); } - if (!found) + if (remote == nullptr) { - Serial.print("[Radio] Ignoring package with not matching serial: 0x"); + Serial.print("[Radio] Ignoring package with unknown serial: 0x"); Serial.print(serial, HEX); Serial.println(""); + return; } + + // Make sure the same package was not handled before. + uint8_t package_id = data[12]; + PackageIdForSerial *package_id_for_serial = nullptr; + for (int i = 0; i < this->num_package_ids; i++) + { + if (this->package_ids[i].serial == serial) + { + package_id_for_serial = &this->package_ids[i]; + break; + } + } + if (package_id_for_serial == nullptr) + { + Serial.print("[Radio] Could not find latest package id for serial 0x"); + Serial.print(serial, HEX); + Serial.println("!"); + return; + } + if (package_id <= package_id_for_serial->serial && package_id > package_id_for_serial->serial - 64) + { + Serial.println("[Radio] Ignoring package with too low package number!"); + return; + } + package_id_for_serial->package_id = package_id; + + Serial.println("[Radio] Package received!"); + remote->callback(data[13], data[14]); } \ No newline at end of file diff --git a/radio.h b/radio.h index 5287516..c9a6e2a 100644 --- a/radio.h +++ b/radio.h @@ -4,13 +4,15 @@ #include #include -#define MAX_REMOTES 10 +#include "constants.h" +#include "remote.h" -struct Remote +class Remote; + +struct PackageIdForSerial { uint32_t serial; - uint8_t last_received_package_id; - std::function callback; + uint8_t package_id; }; class Radio @@ -19,19 +21,18 @@ public: Radio(uint8_t ce, uint8_t csn); ~Radio(); void setup(); - void sendCommand(byte command, byte options); - void sendCommand(byte command); + void sendCommand(uint32_t serial, byte command, byte options); + void sendCommand(uint32_t serial, byte command); void loop(); - void setOutgoingSerial(uint32_t serial); - bool addIncomingSerial(uint32_t serial, std::function callback); - bool removeIncomingSerial(uint32_t serial); + bool addRemote(Remote *remote); + bool removeRemote(Remote *remote); private: RF24 radio; - uint8_t last_sent_package_id = 0; - uint32_t outgoing_serial; + PackageIdForSerial package_ids[constants::MAX_SERIALS]; + uint8_t num_package_ids = 0; - Remote remotes[MAX_REMOTES]; + Remote *remotes[constants::MAX_REMOTES]; uint8_t num_remotes = 0; static const uint64_t address = 0xAAAAAAAAAAAA; @@ -40,6 +41,8 @@ private: // For details on how these parameters were chosen, see // https://github.com/lamperez/xiaomi-lightbar-nrf24?tab=readme-ov-file#crc-checksum CRC16 crc = CRC16(0x1021, 0xfffe, 0x0000, false, false); + + void handlePackage(); }; #endif \ No newline at end of file diff --git a/remote.cpp b/remote.cpp new file mode 100644 index 0000000..063c484 --- /dev/null +++ b/remote.cpp @@ -0,0 +1,70 @@ +#include "remote.h" + +Remote::Remote(Radio *radio, uint32_t serial, const char *name) +{ + this->radio = radio; + this->serial = serial; + this->name = name; + + this->radio->addRemote(this); + + this->serialString = "0x" + String(this->serial, HEX); +} + +Remote::~Remote() +{ +} + +uint32_t Remote::getSerial() +{ + return this->serial; +} + +String Remote::getSerialString() +{ + return this->serialString; +} + +const char *Remote::getName() +{ + return this->name; +} + +void Remote::callback(byte command, byte options) +{ + for (int i = 0; i < this->numCommandListeners; i++) + { + this->commandListeners[i](this, command, options); + } +} + +bool Remote::registerCommandListener(std::function callback) +{ + if (this->numCommandListeners >= constants::MAX_COMMAND_LISTENERS) + { + Serial.println("[Remote] Could not add command listener to remote, because too many are saved!"); + Serial.println("[Remote] Please check if you actually want to save more than " + String(constants::MAX_COMMAND_LISTENERS, DEC) + " command listeners."); + Serial.println("[Remote] If you do, increase MAX_COMMAND_LISTENERS in constants.h and recompile."); + return false; + } + this->commandListeners[this->numCommandListeners] = callback; + this->numCommandListeners++; + return true; +} + +bool Remote::unregisterCommandListener(std::function callback) +{ + for (int i = 0; i < this->numCommandListeners; i++) + { + if (this->commandListeners[i].target() == callback.target()) + { + for (int j = i; j < this->numCommandListeners - 1; j++) + { + this->commandListeners[j] = this->commandListeners[j + 1]; + } + this->numCommandListeners--; + return true; + } + } + return false; +} \ No newline at end of file diff --git a/remote.h b/remote.h new file mode 100644 index 0000000..9578da0 --- /dev/null +++ b/remote.h @@ -0,0 +1,34 @@ +#ifndef REMOTE_H +#define REMOTE_H + +#include "constants.h" +#include "radio.h" + +class Radio; + +class Remote +{ +public: + Remote(Radio *radio, uint32_t serial, const char *name); + ~Remote(); + + uint32_t getSerial(); + String getSerialString(); + const char *getName(); + + bool registerCommandListener(std::function callback); + bool unregisterCommandListener(std::function callback); + + void callback(byte command, byte options); + +private: + Radio *radio; + uint32_t serial; + const char *name; + String serialString; + + std::function commandListeners[constants::MAX_COMMAND_LISTENERS]; + uint8_t numCommandListeners = 0; +}; + +#endif \ No newline at end of file