Skip to content

Instantly share code, notes, and snippets.

@taylorfinnell
Created May 7, 2024 03:00
Show Gist options
  • Save taylorfinnell/5349b8085d57836a45be7637055e0692 to your computer and use it in GitHub Desktop.
Save taylorfinnell/5349b8085d57836a45be7637055e0692 to your computer and use it in GitHub Desktop.
A Bluetooth to MQTT for integrating the ChromaComfort smart bathroom fan into Home Assistant.
/*
A Bluetooth to MQTT for integrating the ChromaComfort smart bathroom fan into Home Assistant.
This is an extremley rough sketch of an esp32 firmware for bringing the ChromaComfort Smart Fan into
HomeAssistant. It's meant to be flashed on an esp32.
This firmware allows the esp32 to both recv status updates about the fan and also issue requests to the fan. This means
that Home Assistant will stay in sync even if the fan is controlled by the remote.
Note that you will not be able to use your fan as a BT speaker without first unplugging the ESP32.
This is a best guess at the BT communnications. Lots of information was gained by inspecting the android application.
Things mostly work okay, but it's not perfect.
*/
#include <Arduino.h>
#include <ArduinoHA.h>
#include <BluetoothSerial.h>
#include <WiFi.h>
#include <cppQueue.h>
// ##########
// ### Customize Me
uint8_t address[6] = {0x4c, 0x72, 0x74, 0xf2, 0xec, 0xa};
#define BROKER_ADDR IPAddress(192, 168, 10, 43)
#define WIFI_SSID "CHANGEME"
#define WIFI_PASSWORD "CHANGEME"
#define MQTT_USER "CHANGEME"
#define MQTT_PASS "CHANGEME"
// ##########
#define DEVICE_PIN "1234"
#define CODE_CMD_START 58
#define CODE_CMD_LENGTH 17
#define CODE_VERSION 1
#define CODE_CTRL_CMD_1 0
#define CODE_CTRL_CMD_2 64
#define CODE_TURN_FAN_ON 1
#define CODE_TURN_FAN_OFF 2
#define CODE_TURN_LIGHT_ON 3
#define CODE_TURN_LIGHT_OFF 4
#define CODE_TURN_ON_RGB 5
#define CODE_TURN_OFF_RGB 6
#define CODE_SAVE_FAVORITE_COLOR1 13
#define CODE_ACTIVATE_FAVORITE_COLOR1 11
#define CODE_DEACTIVATE_FAVORITE_COLOR1 12
#define CODE_COUNTDOWN_ON 17
#define CODE_COUNTDOWN_OFF 18
#define CODE_SAVE_CUSTOM_PATTERN 42
#define CODE_ACTIVATE_CUSTOM_PATTERN 32
#define CODE_DEACTIVATE_CUSTOM_PATTERN 33
WiFiClient client;
HADevice device;
HAMqtt mqtt(client, device);
BluetoothSerial SerialBT;
int lastUpdateAt = 0;
int acks = 0;
int txs = 0;
int rxs = 0;
int heartbeats = 0;
int panics = 0;
HASwitch fan("fan");
HASwitch wallRgb("wallrgb");
HALight light("light", HALight::BrightnessFeature | HALight::RGBFeature);
HASensorNumber uptimeSensor("uptime");
HASensorNumber errorSensor("errors");
HASensorNumber txSensor("tx");
HASensorNumber rxSensor("rx");
HASensorNumber acksSensor("acks");
typedef struct Packet {
byte header;
byte len;
uint8_t data[CODE_CMD_LENGTH];
};
typedef struct TxCmd {
byte version = 5;
byte ctrl_cmd_1 = CODE_CTRL_CMD_1;
byte ctrl_cmd_2 = CODE_CTRL_CMD_2;
byte type = 0;
byte r = 0;
byte g = 0;
byte b = 0;
byte dimmer = 0;
byte speed = 30;
byte sweep_color_value_1 = 1;
byte sweep_color_value_2 = 24;
byte duration = 0;
byte timer_1_value = 0;
byte timer_2_value = 0;
byte timer_3_value = 0;
byte timer_4_value = 0;
byte data_end = 0;
};
typedef struct StatusRxCmd {
byte version = 5;
byte control1;
byte control2;
byte status_mask;
byte unk;
byte brightness;
};
typedef struct AckRxCmd {
byte version = 5;
byte control1;
byte control2;
byte end;
};
int applyGammaCorrection(int r, double t) {
return (int)(255 * pow((double)r / 255, t));
}
#define IMPLEMENTATION FIFO
#define NB_ITEMS 25
Packet t_dat[NB_ITEMS];
Packet r_dat[NB_ITEMS];
cppQueue tx(
sizeof(Packet), NB_ITEMS, IMPLEMENTATION, false, t_dat,
sizeof(t_dat)); // Instantiate queue with static queue data arguments
cppQueue rx(
sizeof(Packet), NB_ITEMS, IMPLEMENTATION, false, r_dat,
sizeof(r_dat)); // Instantiate queue with static queue data arguments
Packet createPacket(uint8_t header, const uint8_t* data, size_t dataSize) {
Packet packet;
packet.header = header;
packet.len = (uint8_t)dataSize;
memcpy(packet.data, data, dataSize);
return packet;
}
void turnFanOn() {
TxCmd cmd;
cmd.type = CODE_TURN_FAN_ON;
Packet packet = createPacket(58, (const uint8_t*)&cmd, 17);
tx.push(&packet);
}
void activateFavColor(byte brightness) {
TxCmd cmd1;
cmd1.type = CODE_ACTIVATE_FAVORITE_COLOR1;
cmd1.dimmer = brightness;
Packet packet = createPacket(58, (const uint8_t*)&cmd1, 17);
tx.push(&packet);
}
void setRGB(byte r, byte g, byte b) {
TxCmd cmd0;
cmd0.type = CODE_SAVE_FAVORITE_COLOR1;
cmd0.r = r;
cmd0.g = g;
cmd0.b = b;
Packet packet = createPacket(58, (const uint8_t*)&cmd0, 17);
tx.push(&packet);
}
void deactivateFavColor() {
TxCmd cmd1;
cmd1.type = CODE_DEACTIVATE_FAVORITE_COLOR1;
Packet packet = createPacket(58, (const uint8_t*)&cmd1, 17);
tx.push(&packet);
}
void turnOnWallRGB() {
TxCmd cmd1;
cmd1.type = CODE_TURN_ON_RGB;
Packet packet = createPacket(58, (const uint8_t*)&cmd1, 17);
tx.push(&packet);
}
void turnOffWallRGB() {
TxCmd cmd1;
cmd1.type = CODE_TURN_OFF_RGB;
Packet packet = createPacket(58, (const uint8_t*)&cmd1, 17);
tx.push(&packet);
}
void turnFanOff() {
TxCmd cmd;
cmd.type = CODE_TURN_FAN_OFF;
Packet packet = createPacket(58, (const uint8_t*)&cmd, 17);
tx.push(&packet);
}
int fromHABrightness(uint8_t b) {
// HA is 0->255 we are 0->100
float x = (float)b / 255.0f;
x *= 100.0f;
return (int)x;
}
int toHABrightness(uint8_t b) {
// HA is 0->255 we are 0->100
float x = (float)b / 100.0f;
x *= 255.0f;
return (int)x;
}
void onSwitchCommandFan(bool state, HASwitch* sender) {
if (state == sender->getCurrentState()) {
return;
}
if (state) {
turnFanOn();
} else {
turnFanOff();
}
sender->setState(state);
}
void onSwitchCommandWallRgb(bool state, HASwitch* sender) {
if (state == sender->getCurrentState()) {
return;
}
if (state) {
turnOnWallRGB();
} else {
turnOffWallRGB();
}
sender->setState(state);
}
void onStateCommand(bool state, HALight* sender) {
if (state == sender->getCurrentState()) {
return;
}
if (state) {
activateFavColor(fromHABrightness(sender->getCurrentBrightness()));
} else {
deactivateFavColor();
}
sender->setState(state);
}
void onBrightnessCommand(uint8_t brightness, HALight* sender) {
if ((int)brightness == (int)sender->getCurrentBrightness()) {
return;
}
activateFavColor(fromHABrightness(brightness));
sender->setBrightness(brightness);
}
void onRGBColorCommand(HALight::RGBColor color, HALight* sender) {
setRGB(applyGammaCorrection(color.red, 4),
applyGammaCorrection(color.green, 4),
applyGammaCorrection(color.blue, 4));
sender->setRGBColor(color);
}
void printPacket(Packet packet) {
Serial.printf("Packet (len=%d, header=%d): ", packet.len, packet.header);
Serial.printf("0x%02X ", packet.header);
Serial.printf("0x%02X ", packet.len);
for (int i = 0; i < packet.len; i++) {
Serial.printf("0x%02X ", packet.data[i]);
}
Serial.print("\r\n");
}
void onBTData(const uint8_t* buffer, size_t size) {
// the length does not include the length and header byte
if (buffer[0] != 58) {
Serial.printf("Got invalid packet: %d, %d\r\n", size, buffer[1]);
return;
}
Packet packet = createPacket(buffer[0], buffer + 2, buffer[1]);
printPacket(packet);
// If we have tx to send, let's only queue acks.
rx.push(&packet);
}
void setup() {
Serial.begin(115200);
Serial.println("Starting...");
// Unique ID must be set!
byte mac[6];
WiFi.macAddress(mac);
device.setUniqueId(mac, sizeof(mac));
// connect to wifi
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500); // waiting for the connection
}
Serial.println();
Serial.println("Connected to the network");
// set device's details (optional)
device.setName("ChromaComfort BT to MQTT Bridge");
device.setSoftwareVersion("1.0.0");
fan.onCommand(onSwitchCommandFan);
fan.setName("Fan"); // optional
wallRgb.onCommand(onSwitchCommandWallRgb);
wallRgb.setName("Wall RGB"); // optional
light.onStateCommand(onStateCommand);
light.onBrightnessCommand(onBrightnessCommand); // optional
light.onRGBColorCommand(onRGBColorCommand); // optional
light.setName("Light");
uptimeSensor.setIcon("mdi:home");
uptimeSensor.setName("Uptime");
uptimeSensor.setUnitOfMeasurement("s");
errorSensor.setIcon("mdi:alert-circle");
errorSensor.setName("Errors");
txSensor.setIcon("mdi:transfer-up");
txSensor.setName("TX");
rxSensor.setIcon("mdi:transfer-down");
rxSensor.setName("RX");
acksSensor.setIcon("mdi:call-received");
acksSensor.setName("Acks");
mqtt.begin(BROKER_ADDR, MQTT_USER, MQTT_PASS);
Serial.println("Connected to mqtt");
SerialBT.begin("ChromaComfort Companion", true);
SerialBT.setTimeout(2000);
SerialBT.setPin(DEVICE_PIN);
SerialBT.onData(onBTData);
Serial.println("Attempting to connect");
bool connected = SerialBT.connect(address);
while (!connected) {
Serial.println("Failed to connect...trying again");
connected = SerialBT.connect(address);
}
Serial.println("Connected to BT");
}
int heartbeats_since_ack = 0;
bool firstRun = true;
void loop() {
mqtt.loop();
if (!firstRun) {
acksSensor.setValue(0);
rxSensor.setValue(0);
txSensor.setValue(0);
errorSensor.setValue(0);
uptimeSensor.setValue(0);
firstRun = false;
}
if ((millis() - lastUpdateAt) > 2000) { // update in 2s interval
unsigned long uptimeValue = millis() / 1000;
uptimeSensor.setValue((int)uptimeValue);
heartbeats += 1;
if (tx.getCount() > 0) {
heartbeats_since_ack += 1;
}
lastUpdateAt = millis();
rxSensor.setValue(rxs);
}
if (tx.getCount() > 0) {
Serial.printf("It's been %d heartbeats since last ack",
heartbeats_since_ack);
Packet packet;
tx.peek(&packet);
Serial.println("About to send packet: ");
printPacket(packet);
int written = SerialBT.write((uint8_t*)&packet, sizeof(packet));
if (written != sizeof(packet)) {
Serial.printf("Failed to write: %d\n", written);
} else {
SerialBT.flush();
}
txs += 1;
txSensor.setValue(txs);
}
if (rx.getCount() > 0) {
rxs += 1;
Packet packet;
rx.pop(&packet);
// Device has acked our tx, pop it.
if (packet.len == 4 && tx.getCount() > 0) {
AckRxCmd* ack = (AckRxCmd*)(packet.data);
if (ack->control1 == 160 && ack->control2 == 64) {
acks += 1;
acksSensor.setValue(acks);
tx.pop(&packet);
heartbeats_since_ack = 0;
}
}
// Nothing to be sent or acked so we can update our status
if (packet.len == 17 && tx.getCount() == 0) {
StatusRxCmd* status = (StatusRxCmd*)(packet.data);
if (status->control1 == 160 && status->control2 == 65) {
Serial.println("Status update...");
byte s = status->status_mask;
bool isFanOn = ((s >> 7) & 1) == 1;
bool isLightOn = ((s >> 6) & 1) == 1;
bool rgbButton = ((s >> 5) & 1) == 1;
bool rgbSweep = ((s >> 4) & 1) == 1;
bool isFavoriteColor1Active = ((s >> 3) & 1) == 1;
bool isFavoriteColor2Active = ((s >> 2) & 1) == 1;
bool userPattern = ((s >> 1) & 1) == 1;
bool reservedBit = ((s >> 0) & 1) == 1;
int brightness = status->brightness;
Serial.printf(
"Light: %d, Fan: %d, isFavoriteColor1Active: %d, wallRGBButton: "
"%d, Brightness: %d\r\n",
isLightOn, isFanOn, isFavoriteColor1Active, rgbButton, brightness);
fan.setState(isFanOn);
light.setState(isLightOn || isFavoriteColor1Active);
light.setBrightness(toHABrightness(brightness));
wallRgb.setState(rgbButton);
}
}
}
// about 2 seconds
if (heartbeats_since_ack > 2) {
Serial.println("Flushing the queue from panic");
tx.flush();
heartbeats_since_ack = 0;
panics += 1;
errorSensor.setValue(panics);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment