├── .gitignore ├── remote.h ├── constants.h ├── lightbar.h ├── LICENSE ├── radio.h ├── mqtt.h ├── Lightbar.ino ├── remote.cpp ├── lightbar.cpp ├── config-example.h ├── README.md ├── radio.cpp └── mqtt.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | config.h 2 | .DS_Store 3 | .vscode -------------------------------------------------------------------------------- /remote.h: -------------------------------------------------------------------------------- 1 | #ifndef REMOTE_H 2 | #define REMOTE_H 3 | 4 | #include "constants.h" 5 | #include "radio.h" 6 | 7 | class Radio; 8 | 9 | class Remote 10 | { 11 | public: 12 | Remote(Radio *radio, uint32_t serial, const char *name); 13 | ~Remote(); 14 | 15 | uint32_t getSerial(); 16 | String getSerialString(); 17 | const char *getName(); 18 | 19 | bool registerCommandListener(std::function callback); 20 | bool unregisterCommandListener(std::function callback); 21 | 22 | void callback(byte command, byte options); 23 | 24 | private: 25 | Radio *radio; 26 | uint32_t serial; 27 | const char *name; 28 | String serialString; 29 | 30 | std::function commandListeners[constants::MAX_COMMAND_LISTENERS]; 31 | uint8_t numCommandListeners = 0; 32 | }; 33 | 34 | #endif -------------------------------------------------------------------------------- /constants.h: -------------------------------------------------------------------------------- 1 | #ifndef CONSTANTS_H 2 | #define CONSTANTS_H 3 | 4 | #include 5 | #include 6 | 7 | namespace constants 8 | { 9 | // The version number of lightbar2mqtt. 10 | const String VERSION = "0.2"; 11 | 12 | // The maximum number of light bars that can be connected to the controller. 13 | const uint8_t MAX_LIGHTBARS = 10; 14 | 15 | // The maximum number of remotes that can be connected to the controller. 16 | const uint8_t MAX_REMOTES = 10; 17 | 18 | // The maximum number of serials, the controller will be able to save latest package ids for. 19 | // This should always >= MAX_REMOTES + MAX_LIGHTBARS. 20 | const uint8_t MAX_SERIALS = 32; 21 | 22 | // The maximum number of command listeners that can be registered for a remote. 23 | const uint8_t MAX_COMMAND_LISTENERS = 10; 24 | }; 25 | 26 | struct SerialWithName 27 | { 28 | uint32_t serial; 29 | const char *name; 30 | }; 31 | 32 | #endif -------------------------------------------------------------------------------- /lightbar.h: -------------------------------------------------------------------------------- 1 | #ifndef LIGHTBAR_H 2 | #define LIGHTBAR_H 3 | 4 | #include "radio.h" 5 | 6 | class Lightbar 7 | { 8 | public: 9 | Lightbar(Radio *radio, uint32_t serial, const char *name); 10 | ~Lightbar(); 11 | uint32_t getSerial(); 12 | const String getSerialString(); 13 | const char *getName(); 14 | 15 | enum Command 16 | { 17 | ON_OFF = 0x01, 18 | COOLER = 0x02, 19 | WARMER = 0x03, 20 | BRIGHTER = 0x04, 21 | DIMMER = 0x05, 22 | RESET = 0x06 23 | }; 24 | 25 | void sendRawCommand(Command command, byte options); 26 | void sendRawCommand(Command command); 27 | void onOff(); 28 | void brighter(); 29 | void dimmer(); 30 | void warmer(); 31 | void cooler(); 32 | void reset(); 33 | void pair(); 34 | void setOnOff(bool on); 35 | void setTemperature(uint8_t value); 36 | void setMiredTemperature(uint mireds); 37 | void setBrightness(uint8_t value); 38 | 39 | private: 40 | Radio *radio; 41 | bool onState = false; 42 | uint32_t serial; 43 | String serialString; 44 | const char *name; 45 | }; 46 | 47 | #endif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Erik Borowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /radio.h: -------------------------------------------------------------------------------- 1 | #ifndef RADIO_H 2 | #define RADIO_H 3 | 4 | #include 5 | #include 6 | 7 | #include "constants.h" 8 | #include "remote.h" 9 | 10 | class Remote; 11 | 12 | struct PackageIdForSerial 13 | { 14 | uint32_t serial; 15 | uint8_t package_id; 16 | }; 17 | 18 | class Radio 19 | { 20 | public: 21 | Radio(uint8_t ce, uint8_t csn); 22 | ~Radio(); 23 | void setup(); 24 | void sendCommand(uint32_t serial, byte command, byte options); 25 | void sendCommand(uint32_t serial, byte command); 26 | void loop(); 27 | bool addRemote(Remote *remote); 28 | bool removeRemote(Remote *remote); 29 | 30 | private: 31 | RF24 radio; 32 | PackageIdForSerial package_ids[constants::MAX_SERIALS]; 33 | uint8_t num_package_ids = 0; 34 | 35 | Remote *remotes[constants::MAX_REMOTES]; 36 | uint8_t num_remotes = 0; 37 | 38 | static const uint64_t address = 0xAAAAAAAAAAAA; 39 | static constexpr byte preamble[8] = {0x53, 0x39, 0x14, 0xDD, 0x1C, 0x49, 0x34, 0x12}; 40 | 41 | // For details on how these parameters were chosen, see 42 | // https://github.com/lamperez/xiaomi-lightbar-nrf24?tab=readme-ov-file#crc-checksum 43 | CRC16 crc = CRC16(0x1021, 0xfffe, 0x0000, false, false); 44 | 45 | void handlePackage(); 46 | }; 47 | 48 | #endif -------------------------------------------------------------------------------- /mqtt.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "constants.h" 5 | #include "lightbar.h" 6 | #include "remote.h" 7 | 8 | #ifndef MQTT_H 9 | #define MQTT_H 10 | 11 | class Remote; 12 | class Lightbar; 13 | 14 | class MQTT 15 | { 16 | public: 17 | MQTT(WiFiClient *wifiClient, const char *mqttServer, int mqttPort, const char *mqttUser, const char *mqttPassword, const char *mqttRootTopic, bool homeAssistantAutoDiscovery, const char *homeAssistantAutoDiscoveryPrefix); 18 | ~MQTT(); 19 | void setup(); 20 | void loop(); 21 | bool addLightbar(Lightbar *lightbar); 22 | bool removeLightbar(Lightbar *lightbar); 23 | bool addRemote(Remote *remote); 24 | bool removeRemote(Remote *remote); 25 | void onMessage(char *topic, byte *payload, unsigned int length); 26 | void sendAction(Remote *remote, byte command, byte options); 27 | const String getCombinedRootTopic(); 28 | const String getClientId(); 29 | 30 | private: 31 | WiFiClient *wifiClient; 32 | PubSubClient *client; 33 | String clientId; 34 | Lightbar *lightbars[constants::MAX_LIGHTBARS]; 35 | int lightbarCount = 0; 36 | Remote *remotes[constants::MAX_REMOTES]; 37 | int remoteCount = 0; 38 | const char *mqttServer; 39 | int mqttPort = 1883; 40 | const char *mqttUser = ""; 41 | const char *mqttPassword = ""; 42 | String mqttRootTopic = "lightbar2mqtt"; 43 | bool homeAssistantDiscovery = true; 44 | String homeAssistantDiscoveryPrefix = "homeassistant"; 45 | 46 | String combinedRootTopic; 47 | std::function remoteCommandHandler; 48 | 49 | void sendAllHomeAssistantDiscoveryMessages(); 50 | void sendHomeAssistantLightbarDiscoveryMessages(Lightbar *lightbar); 51 | void sendHomeAssistantRemoteDiscoveryMessages(Remote *remote); 52 | }; 53 | 54 | #endif -------------------------------------------------------------------------------- /Lightbar.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "constants.h" 4 | #include "config.h" 5 | #include "radio.h" 6 | #include "lightbar.h" 7 | #include "mqtt.h" 8 | 9 | WiFiClient wifiClient; 10 | Radio radio(RADIO_PIN_CE, RADIO_PIN_CSN); 11 | MQTT mqtt(&wifiClient, MQTT_SERVER, MQTT_PORT, MQTT_USER, MQTT_PASSWORD, MQTT_ROOT_TOPIC, HOME_ASSISTANT_DISCOVERY, HOME_ASSISTANT_DISCOVERY_PREFIX); 12 | 13 | void setupWifi() 14 | { 15 | Serial.print("[WiFi] Connecting to network \""); 16 | Serial.print(WIFI_SSID); 17 | Serial.print("\"..."); 18 | 19 | WiFi.begin(WIFI_SSID, WIFI_PASSWORD); 20 | WiFi.setHostname(mqtt.getClientId().c_str()); 21 | 22 | uint retries = 0; 23 | while (WiFi.status() != WL_CONNECTED) 24 | { 25 | delay(1000); 26 | Serial.print("."); 27 | retries++; 28 | if (retries > 60) 29 | ESP.restart(); 30 | } 31 | Serial.println(); 32 | Serial.println("[WiFi] connected!"); 33 | 34 | Serial.print("[WiFi] IP address: "); 35 | Serial.println(WiFi.localIP()); 36 | } 37 | 38 | void setup() 39 | { 40 | Serial.begin(115200); 41 | Serial.println("##########################################"); 42 | Serial.println("# LIGHTBAR2MQTT (Version " + constants::VERSION + ") #"); 43 | Serial.println("# https://github.com/ebinf/lightbar2mqtt #"); 44 | Serial.println("##########################################"); 45 | 46 | radio.setup(); 47 | 48 | setupWifi(); 49 | 50 | for (int i = 0; i < sizeof(REMOTES) / sizeof(SerialWithName); i++) 51 | { 52 | Remote *remote = new Remote(&radio, REMOTES[i].serial, REMOTES[i].name); 53 | mqtt.addRemote(remote); 54 | } 55 | 56 | for (int i = 0; i < sizeof(LIGHTBARS) / sizeof(SerialWithName); i++) 57 | { 58 | Lightbar *lightbar = new Lightbar(&radio, LIGHTBARS[i].serial, LIGHTBARS[i].name); 59 | mqtt.addLightbar(lightbar); 60 | } 61 | 62 | mqtt.setup(); 63 | } 64 | 65 | void loop() 66 | { 67 | if (!WiFi.isConnected()) 68 | { 69 | Serial.println("[WiFi] connection lost!"); 70 | setupWifi(); 71 | } 72 | 73 | mqtt.loop(); 74 | radio.loop(); 75 | } -------------------------------------------------------------------------------- /remote.cpp: -------------------------------------------------------------------------------- 1 | #include "remote.h" 2 | 3 | Remote::Remote(Radio *radio, uint32_t serial, const char *name) 4 | { 5 | this->radio = radio; 6 | this->serial = serial; 7 | this->name = name; 8 | 9 | this->radio->addRemote(this); 10 | 11 | this->serialString = "0x" + String(this->serial, HEX); 12 | } 13 | 14 | Remote::~Remote() 15 | { 16 | } 17 | 18 | uint32_t Remote::getSerial() 19 | { 20 | return this->serial; 21 | } 22 | 23 | String Remote::getSerialString() 24 | { 25 | return this->serialString; 26 | } 27 | 28 | const char *Remote::getName() 29 | { 30 | return this->name; 31 | } 32 | 33 | void Remote::callback(byte command, byte options) 34 | { 35 | for (int i = 0; i < this->numCommandListeners; i++) 36 | { 37 | this->commandListeners[i](this, command, options); 38 | } 39 | } 40 | 41 | bool Remote::registerCommandListener(std::function callback) 42 | { 43 | if (this->numCommandListeners >= constants::MAX_COMMAND_LISTENERS) 44 | { 45 | Serial.println("[Remote] Could not add command listener to remote, because too many are saved!"); 46 | Serial.println("[Remote] Please check if you actually want to save more than " + String(constants::MAX_COMMAND_LISTENERS, DEC) + " command listeners."); 47 | Serial.println("[Remote] If you do, increase MAX_COMMAND_LISTENERS in constants.h and recompile."); 48 | return false; 49 | } 50 | this->commandListeners[this->numCommandListeners] = callback; 51 | this->numCommandListeners++; 52 | return true; 53 | } 54 | 55 | bool Remote::unregisterCommandListener(std::function callback) 56 | { 57 | for (int i = 0; i < this->numCommandListeners; i++) 58 | { 59 | if (this->commandListeners[i].target() == callback.target()) 60 | { 61 | for (int j = i; j < this->numCommandListeners - 1; j++) 62 | { 63 | this->commandListeners[j] = this->commandListeners[j + 1]; 64 | } 65 | this->numCommandListeners--; 66 | return true; 67 | } 68 | } 69 | return false; 70 | } -------------------------------------------------------------------------------- /lightbar.cpp: -------------------------------------------------------------------------------- 1 | #include "lightbar.h" 2 | 3 | Lightbar::Lightbar(Radio *radio, uint32_t serial, const char *name) 4 | { 5 | this->radio = radio; 6 | this->serial = serial; 7 | this->name = name; 8 | 9 | this->serialString = "0x" + String(this->serial, HEX); 10 | } 11 | 12 | Lightbar::~Lightbar() 13 | { 14 | } 15 | 16 | uint32_t Lightbar::getSerial() 17 | { 18 | return this->serial; 19 | } 20 | 21 | const String Lightbar::getSerialString() 22 | { 23 | return this->serialString; 24 | } 25 | 26 | const char *Lightbar::getName() 27 | { 28 | return this->name; 29 | } 30 | 31 | void Lightbar::sendRawCommand(Command command, byte options) 32 | { 33 | this->radio->sendCommand(serial, command, options); 34 | } 35 | 36 | void Lightbar::sendRawCommand(Command command) 37 | { 38 | this->radio->sendCommand(serial, command); 39 | } 40 | 41 | void Lightbar::onOff() 42 | { 43 | this->sendRawCommand(Lightbar::Command::ON_OFF); 44 | onState = !onState; 45 | } 46 | 47 | void Lightbar::setOnOff(bool on) 48 | { 49 | if (onState != on) 50 | this->onOff(); 51 | } 52 | 53 | void Lightbar::brighter() 54 | { 55 | this->sendRawCommand(Lightbar::Command::BRIGHTER); 56 | } 57 | 58 | void Lightbar::dimmer() 59 | { 60 | this->sendRawCommand(Lightbar::Command::DIMMER); 61 | } 62 | 63 | void Lightbar::warmer() 64 | { 65 | this->sendRawCommand(Lightbar::Command::WARMER); 66 | } 67 | 68 | void Lightbar::cooler() 69 | { 70 | this->sendRawCommand(Lightbar::Command::COOLER); 71 | } 72 | 73 | void Lightbar::reset() 74 | { 75 | this->sendRawCommand(Lightbar::Command::RESET); 76 | } 77 | 78 | void Lightbar::pair() 79 | { 80 | this->sendRawCommand(Lightbar::Command::RESET); 81 | } 82 | 83 | void Lightbar::setTemperature(uint8_t value) 84 | { 85 | // Send max value first, then set to the desired value. See 86 | // https://github.com/lamperez/xiaomi-lightbar-nrf24?tab=readme-ov-file#command-codes 87 | // for details. 88 | this->sendRawCommand(Lightbar::Command::COOLER, 0x0 - 16); 89 | this->sendRawCommand(Lightbar::Command::WARMER, (byte)value); 90 | } 91 | 92 | void Lightbar::setMiredTemperature(uint mireds) 93 | { 94 | mireds = max(mireds, (uint)153); 95 | mireds = min(mireds, (uint)370); 96 | float amount = ((1 - ((mireds - 153) * 1.0 / (370 - 153) * 1.0)) * 15) + 0.5; 97 | this->setTemperature((uint8_t)amount); 98 | } 99 | 100 | void Lightbar::setBrightness(uint8_t value) 101 | { 102 | // Send max value first, then set to the desired value. See 103 | // https://github.com/lamperez/xiaomi-lightbar-nrf24?tab=readme-ov-file#command-codes 104 | // for details. 105 | this->sendRawCommand(Lightbar::Command::DIMMER, 0x0 - 16); 106 | this->sendRawCommand(Lightbar::Command::BRIGHTER, (byte)value); 107 | } -------------------------------------------------------------------------------- /config-example.h: -------------------------------------------------------------------------------- 1 | #include "constants.h" 2 | 3 | /* -- nRF24 --------------------------------------------------------------------------------------------------- */ 4 | // The pin number to which the nRF24's Chip Enable (CE) pin is connected. 5 | #define RADIO_PIN_CE 4 6 | 7 | // The pin number to which the nRF24's Chip Select Null (CSN) pin is connected. 8 | #define RADIO_PIN_CSN 5 9 | 10 | /* -- Light Bars ---------------------------------------------------------------------------------------------- */ 11 | // All light bars that should be controlled by this controller. Each light bar must have a unique serial. 12 | // Each entry consists of the serial and the name of the light bar. By default, up to 10 light bars can be added. 13 | // 14 | // If the serial is set to the same value as one remote's, the original remote will still control the light bar 15 | // directly. To separate the light bar from the original remote, set this to a different value, e.g. 0xABCDEF. 16 | // 17 | // The name will be used in Home Assistant. 18 | constexpr SerialWithName LIGHTBARS[] = { 19 | {0xABCDEF, "Light Bar 1"}, 20 | }; 21 | 22 | /* -- Remotes ------------------------------------------------------------------------------------------------- */ 23 | // All remotes that this controller should listen to. Each remote must have a unique serial. 24 | // Each entry consists of the serial and the name of the remote. By default, up to 10 remotes can be added. 25 | // 26 | // If you don't know the serial of your remote, just set this to any value and flash your controller. Once 27 | // the controller is running, the serial of your remote will be printed to the console. 28 | // 29 | // The name will be used in Home Assistant. 30 | constexpr SerialWithName REMOTES[] = { 31 | {0x123456, "Remote 1"}, 32 | }; 33 | 34 | /* -- WiFi ---------------------------------------------------------------------------------------------------- */ 35 | // The SSID of the WiFi network to connect to. 36 | #define WIFI_SSID "" 37 | 38 | // The password of the WiFi network to connect to. 39 | #define WIFI_PASSWORD "" 40 | 41 | /* -- MQTT ---------------------------------------------------------------------------------------------------- */ 42 | // The IP address of the MQTT broker to connect to. 43 | #define MQTT_SERVER "192.168.1.1" 44 | 45 | // The port of the MQTT broker to connect to. 46 | #define MQTT_PORT 1883 47 | 48 | // The user to use to connect to the MQTT broker. 49 | // Set this to NULL if no user is required. 50 | #define MQTT_USER "" 51 | 52 | // The password to use to connect to the MQTT broker. 53 | // Set this to NULL if no password is required. 54 | #define MQTT_PASSWORD "" 55 | 56 | // The root topic used for communicating with this controller. 57 | // Normally, you don't need to change this. Only if you want to archive a specific structure in your MQTT broker. 58 | // This has nothing to do with Home Assistant's discovery feature. 59 | // Each controller will use its own unique subtopic consisting of the controllers MAC address. Therefore, you can 60 | // have multiple controllers in your network without any conflicts. 61 | #define MQTT_ROOT_TOPIC "lightbar2mqtt" 62 | 63 | /* -- Home Assistant Device Discovery ------------------------------------------------------------------------- */ 64 | // Whether to send Home Assistant discovery messages. 65 | #define HOME_ASSISTANT_DISCOVERY true 66 | 67 | // The prefix to use for Home Assistant discovery messages. 68 | // This must match the prefix used in your Home Assistant configuration. The default is "homeassistant". 69 | #define HOME_ASSISTANT_DISCOVERY_PREFIX "homeassistant" 70 | 71 | // The name of the device to use in Home Assistant. 72 | // This is the name that will be displayed in the Home Assistant UI. Of course, you can change this in the UI 73 | // later on. But if you want to have a specific name from the beginning or make it easier to identify the device, 74 | // you can set it here. 75 | #define HOME_ASSISTANT_DEVICE_NAME "Mi Computer Monitor Light Bar" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lightbar2mqtt 2 | 3 | Control your [Xiaomi Mi Computer Monitor Light Bar](https://www.mi.com/global/product/mi-computer-monitor-light-bar/) with MQTT and add it to Home Assistant! All you need is a ESP32, a nRF24 module and a light bar of course. 4 | 5 | ## Acknowledgements 6 | 7 | This project is heavily based on the amazing reverse engineering work done by [lamperez](https://github.com/lamperez). Take a look at their [repository](https://github.com/lamperez/xiaomi-lightbar-nrf24) for more information on the protocol used, the reverse engineering process and a Python version running on Raspberry Pi. It is licensed under the GNU General Public License v3.0. 8 | 9 | I also took some inspiration from [Ben Allen](https://github.com/benallen-dev) and their [repository](https://github.com/benallen-dev/xiaomi-lightbar), licensed under the MIT license. 10 | 11 | This project would not have been possible without the work of these amazing people! Thank you! 12 | 13 | ## Features 14 | 15 | - Control power state, brightness and color temperature via simple MQTT messages 16 | - Receive state updates of the remote via MQTT as well 17 | - Either use the controller aditionally or decouple light bar and remote to gain full control 18 | - Integrates with Home Assistant – either manually or with zero effort using the automatic discovery feature 19 | - The light bar is represented as a `light` entity 20 | - The remote is represented as a `sensor` entity 21 | - 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 22 | - Use multiple light bars or remotes with ease! Each one can be controlled/monitored individually and will also have separate entities in Home Assistant. 23 | 24 | ## Requirements 25 | 26 | - a Xiaomi Mi Computer Monitor Light Bar, Model MJGJD**01**YL (without BLE/WiFi). The MJGJD**02**YL will not work! 27 | - an ESP32 28 | - a nRF24 module (tested with nRF24L01) 29 | 30 | ## Installation 31 | 32 | ### 1. Hardware 33 | 34 | Connect the nRF24 module to the ESP32 as follows. At least these pins work for my combination of ESP32 and nRF24 module. You can change the CE & CSN pins in the `config.h` file if you need to. The SPI pins (SCK, MOSI, MISO) might be different on your ESP32 – check the pinout of your ESP32! 35 | 36 | | nRF24 | ESP32 | 37 | | :---- | -----: | 38 | | VCC | 3V3 | 39 | | GND | GND | 40 | | CE | Pin 4 | 41 | | CSN | Pin 5 | 42 | | SCK | Pin 18 | 43 | | MOSI | Pin 23 | 44 | | MISO | Pin 19 | 45 | 46 | ### 2. Software 47 | 48 | 1. Clone this repository 49 | 2. Copy the `config-example.h` file to `config.h` and adjust the settings to your needs. 50 | 3. Connect your ESP32 to your computer. 51 | 4. Open the Arduino IDE and install the required libraries: 52 | - [Arduino_JSON](https://github.com/arduino-libraries/Arduino_JSON) by Arduino, _Version 0.2.0_ 53 | - [CRC](https://github.com/RobTillaart/CRC) by Rob Tillaart, _Version 1.0.3_ 54 | - [PubSubClient](https://pubsubclient.knolleary.net/) by Nick O'Leary, _Version 2.8_ 55 | - [RF24](https://nrf24.github.io/RF24/) by TMRh20, _Version 1.4.10_ 56 | 5. Select your serial port and board. Upload the sketch to your ESP32. 57 | 6. Inspect the serial monitor (115200 baud). If everything is set up correctly, the ESP32 should connect to your WiFi and MQTT broker. 58 | 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. 59 | 8. Upload the sketch again. 60 | 9. If your Home Assistant has the MQTT integration set up, the light bar should be discovered automatically. 61 | 10. Enjoy controlling your light bar via MQTT! 62 | 63 | ### 3. Pairing Light bar and ESP32 (optional) 64 | 65 | 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. 66 | 67 | ## Usage 68 | 69 | ### MQTT Topics 70 | 71 | All MQTT topics are prefixed with a root topic. You can set this root topic in the `config.h` file. The default root topic is `lightbar2mqtt`. The root topic is followed by the MAC address of your ESP32 in the format `l2m_`, e.g. `l2m_1234567890AB`. Just take a look at the serial monitor of your ESP32 to find out the used root topic and MAC address, it will be printed right after the startup message: 72 | 73 | ```text 74 | ... 75 | [MQTT] Device ID: l2m_1234567890AB 76 | [MQTT] Root Topic: lightbar2mqtt/l2m_12345679890AB 77 | ... 78 | ``` 79 | 80 | 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). 81 | 82 | #### Light Bar 83 | 84 | 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: 85 | 86 | - `state`: `"ON"` or `"OFF"` 87 | - `brightness`: `0` (off) to `15` (full brightness) 88 | - `color_temp`: Mireds – `153` (cold) to `370` (warm) 89 | 90 | Example: 91 | 92 | ```json 93 | { 94 | "state": "ON", 95 | "brightness": 15, 96 | "color_temp": 153 97 | } 98 | ``` 99 | 100 | #### Remote 101 | 102 | 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: 103 | 104 | - `press` 105 | - `turn_clockwise` 106 | - `turn_counterclockwise` 107 | - `hold` 108 | - `press_turn_clockwise` 109 | - `press_turn_counterclockwise` 110 | 111 | #### Pairing 112 | 113 | 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. 114 | 115 | Please note that the light bar needs to be power-cycled within 10 seconds _before_ sending the pairing message. 116 | 117 | #### Availability 118 | 119 | The ESP32 sends its availability to the following topic: `/l2m_/availability` e.g. `lightbar2mqtt/l2m_1234567890AB/availability`. The payload is either `online` or `offline`. 120 | 121 | ### Home Assistant 122 | 123 | If your Home Assistant has the MQTT integration set up, the light bar(s) and remote(s) should be discovered automatically. 124 | 125 | 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`. 126 | 127 | ### Known Issues / Limitations 128 | 129 | - 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. 130 | - 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. 131 | - 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. 132 | 133 | ## Contributing 134 | 135 | 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! 136 | 137 | 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.~~ 138 | 139 | ## License 140 | 141 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 142 | -------------------------------------------------------------------------------- /radio.cpp: -------------------------------------------------------------------------------- 1 | #include "radio.h" 2 | 3 | /* 4 | * Package structure: 5 | * 0 – 7: Preamble (see constant above) 6 | * 8 – 10: Remote ID 7 | * 11 – 11: Separator (0xFF) 8 | * 12 – 12: Sequence counter 9 | * 13 – 14: Command ID + options 10 | * 15 – 16: CRC16 checksum 11 | */ 12 | 13 | Radio::Radio(uint8_t ce, uint8_t csn) 14 | { 15 | this->radio = RF24(ce, csn); 16 | } 17 | 18 | Radio::~Radio() 19 | { 20 | this->radio.stopListening(); 21 | this->radio.powerDown(); 22 | } 23 | 24 | bool Radio::addRemote(Remote *remote) 25 | { 26 | if (this->num_remotes >= constants::MAX_REMOTES) 27 | { 28 | Serial.println("[Radio] Could not add remote, because too many remotes are saved!"); 29 | Serial.println("[Radio] Please check if you actually want to save more than " + String(constants::MAX_REMOTES, DEC) + " remotes."); 30 | Serial.println("[Radio] If you do, increase MAX_REMOTES in constants.h and recompile."); 31 | return false; 32 | } 33 | if (this->num_package_ids >= constants::MAX_SERIALS) 34 | { 35 | Serial.println("[Radio] Could not add remote, because too many serials are saved!"); 36 | Serial.println("[Radio] Please check if you actually want to save more than " + String(constants::MAX_SERIALS, DEC) + " serials."); 37 | Serial.println("[Radio] If you do, increase MAX_SERIALS in constants.h and recompile."); 38 | return false; 39 | } 40 | this->remotes[this->num_remotes] = remote; 41 | this->num_remotes++; 42 | this->package_ids[this->num_package_ids].serial = remote->getSerial(); 43 | this->package_ids[this->num_package_ids].package_id = 0; 44 | this->num_package_ids++; 45 | Serial.print("[Radio] Remote "); 46 | Serial.print(remote->getSerialString()); 47 | Serial.println(" added!"); 48 | return true; 49 | } 50 | 51 | bool Radio::removeRemote(Remote *remote) 52 | { 53 | for (int i = 0; i < this->num_remotes; i++) 54 | { 55 | if (this->remotes[i] == remote) 56 | { 57 | for (int j = 0; j < this->num_package_ids; j++) 58 | { 59 | if (this->package_ids[j].serial == remote->getSerial()) 60 | { 61 | for (int k = j; k < this->num_package_ids - 1; k++) 62 | { 63 | this->package_ids[k] = this->package_ids[k + 1]; 64 | } 65 | this->num_package_ids--; 66 | break; 67 | } 68 | } 69 | 70 | for (int j = i; j < this->num_remotes - 1; j++) 71 | { 72 | this->remotes[j] = this->remotes[j + 1]; 73 | } 74 | this->num_remotes--; 75 | return true; 76 | } 77 | } 78 | return false; 79 | } 80 | 81 | void Radio::sendCommand(uint32_t serial, byte command, byte options) 82 | { 83 | PackageIdForSerial *package_id = nullptr; 84 | for (int i = 0; i < this->num_package_ids; i++) 85 | { 86 | if (this->package_ids[i].serial == serial) 87 | { 88 | package_id = &this->package_ids[i]; 89 | break; 90 | } 91 | } 92 | if (package_id == nullptr) 93 | { 94 | if (this->num_package_ids >= constants::MAX_SERIALS) 95 | { 96 | Serial.println("[Radio] Could not send command, because too many serials are saved!"); 97 | Serial.println("[Radio] Please check if you actually want to save more than " + String(constants::MAX_SERIALS, DEC) + " serials."); 98 | Serial.println("[Radio] If you do, increase MAX_SERIALS in constants.h and recompile."); 99 | return; 100 | } 101 | package_id = &this->package_ids[this->num_package_ids]; 102 | package_id->serial = serial; 103 | package_id->package_id = 0; 104 | this->num_package_ids++; 105 | } 106 | 107 | byte data[17] = {0}; 108 | memcpy(data, Radio::preamble, sizeof(Radio::preamble)); 109 | data[8] = (serial & 0xFF0000) >> 16; 110 | data[9] = (serial & 0x00FF00) >> 8; 111 | data[10] = serial & 0x0000FF; 112 | data[11] = 0xFF; 113 | data[12] = ++package_id->package_id; 114 | data[13] = command; 115 | data[14] = options; 116 | 117 | this->crc.restart(); 118 | this->crc.add(data, sizeof(data) - 2); 119 | uint16_t checksum = this->crc.calc(); 120 | data[15] = (checksum & 0xFF00) >> 8; 121 | data[16] = checksum & 0x00FF; 122 | 123 | Serial.print("[Radio] Sending command: 0x"); 124 | for (int i = 0; i < 17; i++) 125 | { 126 | Serial.print(data[i], HEX); 127 | } 128 | Serial.println(); 129 | 130 | this->radio.stopListening(); 131 | for (int i = 0; i < 20; i++) 132 | { 133 | this->radio.write(&data, sizeof(data), true); 134 | delay(10); 135 | } 136 | this->radio.startListening(); 137 | } 138 | 139 | void Radio::sendCommand(uint32_t serial, byte command) 140 | { 141 | return this->sendCommand(serial, command, 0x0); 142 | } 143 | 144 | void Radio::setup() 145 | { 146 | uint retries = 0; 147 | while (!this->radio.begin()) 148 | { 149 | Serial.println("[Radio] nRF24 not responding! Is it wired correctly?"); 150 | delay(1000); 151 | retries++; 152 | if (retries > 60) 153 | ESP.restart(); 154 | } 155 | 156 | Serial.println("[Radio] Setting up radio..."); 157 | this->radio.failureDetected = false; 158 | 159 | this->radio.openReadingPipe(0, Radio::address); 160 | 161 | this->radio.setChannel(68); 162 | this->radio.setDataRate(RF24_2MBPS); 163 | this->radio.disableCRC(); 164 | this->radio.disableDynamicPayloads(); 165 | this->radio.setPayloadSize(17); 166 | this->radio.setAutoAck(false); 167 | this->radio.setRetries(15, 15); 168 | 169 | this->radio.openWritingPipe(Radio::address); 170 | 171 | this->radio.startListening(); 172 | Serial.println("[Radio] done!"); 173 | } 174 | 175 | void Radio::loop() 176 | { 177 | if (this->radio.failureDetected) 178 | { 179 | Serial.println("[Radio] Failure detected!"); 180 | delay(1000); 181 | this->setup(); 182 | delay(1000); 183 | } 184 | 185 | if (this->radio.available()) 186 | this->handlePackage(); 187 | } 188 | 189 | void Radio::handlePackage() 190 | { 191 | // Read raw data, append a 5 and shift it. See 192 | // https://github.com/lamperez/xiaomi-lightbar-nrf24?tab=readme-ov-file#baseband-packet-format 193 | // on why that is necessary. 194 | byte raw_data[18] = {0}; 195 | this->radio.read(&raw_data, sizeof(raw_data)); 196 | byte data[17] = {0x5}; 197 | for (int i = 0; i < 17; i++) 198 | { 199 | if (i == 0) 200 | data[i] = 0x50 | raw_data[i] >> 5; 201 | else 202 | data[i] = ((raw_data[i - 1] >> 1) & 0x0F) << 4 | ((raw_data[i - 1] & 0x01) << 3) | raw_data[i] >> 5; 203 | } 204 | 205 | // Check if preamble matches. Ignore package otherwise. 206 | if (memcmp(data, Radio::preamble, sizeof(Radio::preamble))) 207 | return; 208 | 209 | // Make sure the checksum of the package is correct. 210 | this->crc.restart(); 211 | this->crc.add(data, sizeof(data) - 2); 212 | uint16_t calculated_checksum = this->crc.calc(); 213 | uint16_t package_checksum = data[15] << 8 | data[16]; 214 | if (calculated_checksum != package_checksum) 215 | { 216 | Serial.println("[Radio] Ignoring pacakge with wrong checksum!"); 217 | return; 218 | } 219 | 220 | // Check if package is coming from a observed remote. 221 | Remote *remote = nullptr; 222 | uint32_t serial = data[8] << 16 | data[9] << 8 | data[10]; 223 | for (int i = 0; i < this->num_remotes; i++) 224 | { 225 | 226 | if (serial == this->remotes[i]->getSerial()) 227 | { 228 | remote = this->remotes[i]; 229 | break; 230 | } 231 | } 232 | 233 | if (remote == nullptr) 234 | { 235 | Serial.print("[Radio] Ignoring package with unknown serial: 0x"); 236 | Serial.print(serial, HEX); 237 | Serial.println(""); 238 | return; 239 | } 240 | 241 | // Make sure the same package was not handled before. 242 | uint8_t package_id = data[12]; 243 | PackageIdForSerial *package_id_for_serial = nullptr; 244 | for (int i = 0; i < this->num_package_ids; i++) 245 | { 246 | if (this->package_ids[i].serial == serial) 247 | { 248 | package_id_for_serial = &this->package_ids[i]; 249 | break; 250 | } 251 | } 252 | if (package_id_for_serial == nullptr) 253 | { 254 | Serial.print("[Radio] Could not find latest package id for serial 0x"); 255 | Serial.print(serial, HEX); 256 | Serial.println("!"); 257 | return; 258 | } 259 | if (package_id <= package_id_for_serial->serial && package_id > package_id_for_serial->serial - 64) 260 | { 261 | Serial.println("[Radio] Ignoring package with too low package number!"); 262 | return; 263 | } 264 | package_id_for_serial->package_id = package_id; 265 | 266 | Serial.println("[Radio] Package received!"); 267 | remote->callback(data[13], data[14]); 268 | } -------------------------------------------------------------------------------- /mqtt.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "mqtt.h" 5 | 6 | MQTT::MQTT(WiFiClient *wifiClient, const char *mqttServer, int mqttPort, const char *mqttUser, const char *mqttPassword, const char *mqttRootTopic, bool homeAssistantAutoDiscovery, const char *homeAssistantAutoDiscoveryPrefix) 7 | { 8 | this->mqttServer = mqttServer; 9 | this->mqttPort = mqttPort; 10 | this->mqttUser = mqttUser; 11 | this->mqttPassword = mqttPassword; 12 | this->mqttRootTopic = String(mqttRootTopic); 13 | this->homeAssistantDiscovery = homeAssistantAutoDiscovery; 14 | this->homeAssistantDiscoveryPrefix = String(homeAssistantAutoDiscoveryPrefix); 15 | 16 | this->remoteCommandHandler = std::bind(&MQTT::sendAction, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); 17 | 18 | this->client = new PubSubClient(*wifiClient); 19 | 20 | String mac = ""; 21 | unsigned char mac_base[6] = {0}; 22 | if (esp_efuse_mac_get_default(mac_base) == ESP_OK) 23 | { 24 | char buffer[13]; // 6*2 characters for hex + 5 characters for colons + 1 character for null terminator 25 | sprintf(buffer, "%02X%02X%02X%02X%02X%02X", mac_base[0], mac_base[1], mac_base[2], mac_base[3], mac_base[4], mac_base[5]); 26 | mac = buffer; 27 | } 28 | this->clientId = "l2m_" + mac; 29 | this->combinedRootTopic = this->mqttRootTopic + "/" + this->clientId; 30 | } 31 | 32 | MQTT::~MQTT() 33 | { 34 | delete this->client; 35 | } 36 | 37 | const String MQTT::getCombinedRootTopic() 38 | { 39 | return this->combinedRootTopic; 40 | } 41 | 42 | const String MQTT::getClientId() 43 | { 44 | return this->clientId; 45 | } 46 | 47 | void MQTT::onMessage(char *topic, byte *payload, unsigned int length) 48 | { 49 | Serial.print("[MQTT] New Message ("); 50 | Serial.print(topic); 51 | Serial.print("): "); 52 | for (int i = 0; i < length; i++) 53 | { 54 | Serial.print((char)payload[i]); 55 | } 56 | Serial.println(); 57 | 58 | JSONVar command = JSON.parse(String(payload, length)); 59 | 60 | Lightbar *lightbar = nullptr; 61 | for (int i = 0; i < this->lightbarCount; i++) 62 | { 63 | lightbar = this->lightbars[i]; 64 | if (!strcmp(topic, String(this->getCombinedRootTopic() + "/" + lightbar->getSerialString() + "/pair").c_str())) 65 | { 66 | lightbar->pair(); 67 | return; 68 | } 69 | 70 | if (strcmp(topic, String(this->getCombinedRootTopic() + "/" + lightbar->getSerialString() + "/command").c_str())) 71 | continue; 72 | 73 | if (JSON.typeof(command) != "object") 74 | continue; 75 | 76 | if (command.hasOwnProperty("state")) 77 | { 78 | const char *state = command["state"]; 79 | lightbar->setOnOff(strcmp(state, "ON")); 80 | } 81 | 82 | if (command.hasOwnProperty("brightness")) 83 | { 84 | lightbar->setBrightness((uint8_t)command["brightness"]); 85 | } 86 | 87 | if (command.hasOwnProperty("color_temp")) 88 | { 89 | lightbar->setMiredTemperature((uint)command["color_temp"]); 90 | } 91 | } 92 | } 93 | 94 | void MQTT::setup() 95 | { 96 | Serial.print("[MQTT] Device ID: "); 97 | Serial.println(this->clientId); 98 | Serial.print("[MQTT] Root Topic: "); 99 | Serial.println(this->getCombinedRootTopic()); 100 | 101 | this->client->setServer(this->mqttServer, this->mqttPort); 102 | this->client->setCallback(std::bind(&MQTT::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); 103 | 104 | uint retries = 0; 105 | Serial.println("[MQTT] Connecting to MQTT broker..."); 106 | while (!this->client->connected()) 107 | { 108 | if (!this->client->connect(this->clientId.c_str(), this->mqttUser, this->mqttPassword, String(this->getCombinedRootTopic() + "/availability").c_str(), 1, true, "offline")) 109 | { 110 | Serial.print("[MQTT] Connection failed! rc="); 111 | Serial.print(this->client->state()); 112 | Serial.println(" trying again in 1 second."); 113 | delay(1000); 114 | retries++; 115 | if (retries > 60) 116 | ESP.restart(); 117 | } 118 | } 119 | 120 | Serial.println("[MQTT] connected!"); 121 | this->client->publish(String(this->getCombinedRootTopic() + "/availability").c_str(), "online", true); 122 | this->client->subscribe(String(this->getCombinedRootTopic() + "/+/command").c_str()); 123 | this->client->subscribe(String(this->getCombinedRootTopic() + "/+/pair").c_str()); 124 | 125 | this->sendAllHomeAssistantDiscoveryMessages(); 126 | } 127 | 128 | bool MQTT::addLightbar(Lightbar *lightbar) 129 | { 130 | if (this->lightbarCount >= constants::MAX_LIGHTBARS) 131 | { 132 | Serial.println("[MQTT] Could not add light bar, because too many light bars are saved!"); 133 | Serial.println("[MQTT] Please check if you actually want to save more than " + String(constants::MAX_LIGHTBARS, DEC) + " light bars."); 134 | Serial.println("[MQTT] If you do, increase MAX_LIGHTBARS in constants.h and recompile."); 135 | return false; 136 | } 137 | this->lightbars[this->lightbarCount] = lightbar; 138 | this->lightbarCount++; 139 | this->sendHomeAssistantLightbarDiscoveryMessages(lightbar); 140 | return true; 141 | } 142 | 143 | bool MQTT::removeLightbar(Lightbar *lightbar) 144 | { 145 | for (int i = 0; i < this->lightbarCount; i++) 146 | { 147 | if (this->lightbars[i] == lightbar) 148 | { 149 | for (int j = i; j < this->lightbarCount - 1; j++) 150 | { 151 | this->lightbars[j] = this->lightbars[j + 1]; 152 | } 153 | this->lightbarCount--; 154 | return true; 155 | } 156 | } 157 | return false; 158 | } 159 | 160 | bool MQTT::addRemote(Remote *remote) 161 | { 162 | if (this->remoteCount >= constants::MAX_REMOTES) 163 | { 164 | Serial.println("[MQTT] Could not add remote, because too many remotes are saved!"); 165 | Serial.println("[MQTT] Please check if you actually want to save more than " + String(constants::MAX_REMOTES, DEC) + " remotes."); 166 | Serial.println("[MQTT] If you do, increase MAX_REMOTES in constants.h and recompile."); 167 | return false; 168 | } 169 | this->remotes[this->remoteCount] = remote; 170 | this->remoteCount++; 171 | remote->registerCommandListener(this->remoteCommandHandler); 172 | this->sendHomeAssistantRemoteDiscoveryMessages(remote); 173 | return true; 174 | } 175 | 176 | bool MQTT::removeRemote(Remote *remote) 177 | { 178 | for (int i = 0; i < this->remoteCount; i++) 179 | { 180 | if (this->remotes[i] == remote) 181 | { 182 | this->remotes[i]->registerCommandListener(this->remoteCommandHandler); 183 | for (int j = i; j < this->remoteCount - 1; j++) 184 | { 185 | this->remotes[j] = this->remotes[j + 1]; 186 | } 187 | this->remoteCount--; 188 | return true; 189 | } 190 | } 191 | return false; 192 | } 193 | 194 | void MQTT::sendAllHomeAssistantDiscoveryMessages() 195 | { 196 | if (!this->homeAssistantDiscovery) 197 | return; 198 | for (int i = 0; i < this->lightbarCount; i++) 199 | { 200 | this->sendHomeAssistantLightbarDiscoveryMessages(this->lightbars[i]); 201 | } 202 | for (int i = 0; i < this->remoteCount; i++) 203 | { 204 | this->sendHomeAssistantRemoteDiscoveryMessages(this->remotes[i]); 205 | } 206 | } 207 | 208 | void MQTT::sendHomeAssistantLightbarDiscoveryMessages(Lightbar *lightbar) 209 | { 210 | if (!this->homeAssistantDiscovery) 211 | return; 212 | 213 | Serial.print("[MQTT] Sending lightbar discovery messages for "); 214 | Serial.println(lightbar->getSerialString()); 215 | 216 | const String topicClient = this->clientId + "_" + lightbar->getSerialString(); 217 | const String baseConfig = R"json( 218 | "schema": "json", 219 | "o": { 220 | "name": "lightbar2mqtt", 221 | "sw_version": ")json" + 222 | constants::VERSION + 223 | R"json(", 224 | "support_url": "https://github.com/ebinf/lightbar2mqtt" 225 | }, 226 | "~": ")json" + this->getCombinedRootTopic() + 227 | "/" + 228 | lightbar->getSerialString() + 229 | R"json(", 230 | "availability_topic": ")json" + 231 | this->getCombinedRootTopic() + R"json(/availability", 232 | "dev": 233 | { 234 | "ids" : ")json" + topicClient + 235 | R"json(", 236 | "name": ")json" + 237 | lightbar->getName() + 238 | R"json(", 239 | "mdl": "Mi Computer Monitor Light Bar (MJGJD01YL)", 240 | "mf": "Xiaomi", 241 | "sw": "lightbar2mqtt )json" + 242 | constants::VERSION + 243 | R"json(", 244 | "sn": ")json" + 245 | lightbar->getSerialString() + 246 | R"json(" 247 | },)json"; 248 | 249 | String rendevous_str = "{" + 250 | baseConfig + 251 | R"json( 252 | "supported_color_modes": [ 253 | "color_temp" 254 | ], 255 | "brightness": true, 256 | "brightness_scale": 15, 257 | "name": "Light bar", 258 | "cmd_t": "~/command", 259 | "uniq_id": ")json" + topicClient + 260 | R"json(_lightbar", 261 | "max_mireds": 370, 262 | "min_mireds":153, 263 | "p": "light", 264 | "icon": "mdi:wall-sconce-flat" 265 | )json" + "}"; 266 | 267 | this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/light/" + topicClient + "/lightbar/config").c_str(), rendevous_str.length(), true); 268 | this->client->print(rendevous_str); 269 | this->client->endPublish(); 270 | 271 | rendevous_str = "{" + 272 | baseConfig + 273 | R"json( 274 | "name": "Pair", 275 | "cmd_t": "~/pair", 276 | "uniq_id": ")json" + 277 | topicClient + R"json(_pair", 278 | "p": "button" 279 | )json" + "}"; 280 | this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/button/" + topicClient + "/pair/config").c_str(), rendevous_str.length(), true); 281 | this->client->print(rendevous_str); 282 | this->client->endPublish(); 283 | } 284 | 285 | void MQTT::sendHomeAssistantRemoteDiscoveryMessages(Remote *remote) 286 | { 287 | if (!this->homeAssistantDiscovery) 288 | return; 289 | 290 | Serial.print("[MQTT] Sending remote discovery messages for "); 291 | Serial.println(remote->getSerialString()); 292 | 293 | const String topicClient = this->clientId + "_" + remote->getSerialString(); 294 | const String baseConfig = R"json( 295 | "schema": "json", 296 | "o": { 297 | "name": "lightbar2mqtt", 298 | "sw_version": ")json" + 299 | constants::VERSION + 300 | R"json(", 301 | "support_url": "https://github.com/ebinf/lightbar2mqtt" 302 | }, 303 | "~": ")json" + this->getCombinedRootTopic() + 304 | "/" + 305 | remote->getSerialString() + 306 | R"json(", 307 | "availability_topic": ")json" + 308 | this->getCombinedRootTopic() + R"json(/availability", 309 | "dev": { 310 | "ids": ")json" + topicClient + 311 | R"json(", 312 | "name": ")json" + remote->getName() + 313 | R"json(", 314 | "mdl": "Mi Computer Monitor Light Bar Remote Control (MJGJD01YL)", 315 | "mf": "Xiaomi", 316 | "sw": "lightbar2mqtt )json" + 317 | constants::VERSION + 318 | R"json(", 319 | "sn": ")json" + remote->getSerialString() + 320 | R"json(" 321 | },)json"; 322 | 323 | String rendevous_str = "{" + 324 | baseConfig + 325 | R"json( 326 | "name": "Remote", 327 | "state_topic": "~/state", 328 | "uniq_id": ")json" + 329 | topicClient + R"json(_remote", 330 | "value_template": "{{ value }}", 331 | "enabled_by_default": true, 332 | "entity_category": "diagnostic", 333 | "icon": "mdi:gesture-double-tap" 334 | )json" + "}"; 335 | 336 | this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/sensor/" + topicClient + "/remote/config").c_str(), rendevous_str.length(), true); 337 | this->client->print(rendevous_str); 338 | this->client->endPublish(); 339 | 340 | const char *commands[] = { 341 | "press", 342 | "turn_clockwise", 343 | "turn_counterclockwise", 344 | "press_turn_clockwise", 345 | "press_turn_counterclockwise", 346 | "hold"}; 347 | String cmd; 348 | for (int i = 0; i < 6; i++) 349 | { 350 | cmd = commands[i]; 351 | rendevous_str = "{" + 352 | baseConfig + 353 | R"json( 354 | "automation_type": "trigger", 355 | "payload": ")json" + cmd + 356 | R"json(", 357 | "subtype": ")json" + cmd + 358 | R"json(", 359 | "type": "action", 360 | "topic": "~/state", 361 | "p": "device_automation" 362 | )json" + "}"; 363 | 364 | this->client->beginPublish(String(homeAssistantDiscoveryPrefix + "/device_automation/" + topicClient + "/action_" + cmd + "/config").c_str(), rendevous_str.length(), true); 365 | this->client->print(rendevous_str); 366 | this->client->endPublish(); 367 | } 368 | } 369 | 370 | void MQTT::loop() 371 | { 372 | if (!this->client->connected()) 373 | { 374 | Serial.println("[MQTT] connection lost!"); 375 | this->setup(); 376 | } 377 | this->client->loop(); 378 | } 379 | 380 | void MQTT::sendAction(Remote *remote, byte command, byte options) 381 | { 382 | String action; 383 | switch ((uint8_t)command) 384 | { 385 | case Lightbar::Command::ON_OFF: 386 | action = "press"; 387 | break; 388 | 389 | case Lightbar::Command::BRIGHTER: 390 | action = "turn_clockwise"; 391 | break; 392 | 393 | case Lightbar::Command::DIMMER: 394 | action = "turn_counterclockwise"; 395 | break; 396 | 397 | case Lightbar::Command::WARMER: 398 | action = "press_turn_counterclockwise"; 399 | break; 400 | 401 | case Lightbar::Command::COOLER: 402 | action = "press_turn_clockwise"; 403 | break; 404 | 405 | case Lightbar::Command::RESET: 406 | action = "hold"; 407 | break; 408 | 409 | default: 410 | return; 411 | } 412 | 413 | String topic = String(this->getCombinedRootTopic() + "/" + remote->getSerialString() + "/state"); 414 | Serial.print("[MQTT] Sending message ("); 415 | Serial.print(topic); 416 | Serial.print("): "); 417 | Serial.println(action); 418 | this->client->publish(topic.c_str(), action.c_str()); 419 | delay(200); 420 | this->client->publish(topic.c_str(), NULL); 421 | } --------------------------------------------------------------------------------