diff --git a/EleksTubeHAX_pio/data/clockfaces.txt b/EleksTubeHAX_pio/data/clockfaces.txt new file mode 100644 index 0000000..8e821ff --- /dev/null +++ b/EleksTubeHAX_pio/data/clockfaces.txt @@ -0,0 +1,7 @@ +Nixie Tube +Blue Ribbon +Colors +Modern Neon +Yellow Dots +Cyan and Pink +Thin Blue \ No newline at end of file diff --git a/EleksTubeHAX_pio/src/Backlights.cpp b/EleksTubeHAX_pio/src/Backlights.cpp index e2749ac..863a817 100644 --- a/EleksTubeHAX_pio/src/Backlights.cpp +++ b/EleksTubeHAX_pio/src/Backlights.cpp @@ -10,11 +10,11 @@ void Backlights::begin(StoredConfig::Config::Backlights *config_) { setPattern(rainbow); setColorPhase(0); setIntensity(max_intensity-1); - setPulseRate(72); - setBreathRate(10); + setPulseRate(60); + setBreathRate(20); + setRainbowDuration(2); config->is_valid = StoredConfig::valid; } - off = false; } @@ -87,26 +87,23 @@ void Backlights::loop() { } void Backlights::pulsePattern() { - if (pattern_needs_init) { - fill(phaseToColor(config->color_phase)); - } + fill(phaseToColor(config->color_phase)); - float pulse_length_millis = (60.0f * 1000) / config->breath_per_min; + float pulse_length_millis = (60.0f * 1000) / config->pulse_bpm; float val = 1 + abs(sin(2 * M_PI * millis() / pulse_length_millis)) * 254; if (dimming) { val = val * BACKLIGHT_DIMMED_INTENSITY / 7; - } + } else { + val = val * config->intensity / 7; + } setBrightness((uint8_t)val); show(); - } void Backlights::breathPattern() { - if (pattern_needs_init) { - fill(phaseToColor(config->color_phase)); - } - + fill(phaseToColor(config->color_phase)); + // https://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/ // Avoid a 0 value as it shuts off the LEDs and we have to re-initialize. float pulse_length_millis = (60.0f * 1000) / config->breath_per_min; @@ -114,7 +111,9 @@ void Backlights::breathPattern() { if (dimming) { val = val * BACKLIGHT_DIMMED_INTENSITY / 7; - } + } else { + val = val * config->intensity / 7; + } uint8_t brightness = (uint8_t)val; if (brightness < 1) { brightness = 1; } @@ -133,8 +132,14 @@ void Backlights::testPattern() { clear(); setPixelColor(digit, color); - show(); + if (dimming) { + setBrightness(0xFF >> max_intensity - (uint8_t) BACKLIGHT_DIMMED_INTENSITY - 1); + } else { + setBrightness(0xFF >> max_intensity - config->intensity - 1); + } + + show(); } uint8_t Backlights::phaseToIntensity(uint16_t phase) { @@ -164,15 +169,34 @@ uint32_t Backlights::phaseToColor(uint16_t phase) { return(uint32_t(red) << 16 | uint32_t(green) << 8 | uint32_t(blue)); } +uint32_t Backlights::hueToPhase(float hue) { + hue = hue - 120.f; + if (hue < 0) { + hue = hue + 360.f; + } + uint32_t phase = uint32_t(round(768.f * (1.f - hue/360.f))); + phase = phase % max_phase; + return(phase); +} + +float Backlights::phaseToHue(uint32_t phase) { + float hue = 120.f + ((768.f - float(phase))/768.f)*360.f; + //h = 120 + (1 - p/768)*360 + if( hue >= 360.f ) { + hue = hue - 360.f; + } + return(round(hue)); +} + void Backlights::rainbowPattern() { // Divide by 3 to spread it out some, so the whole rainbow isn't displayed at once. // TODO Make this /3 a parameter const uint16_t phase_per_digit = (max_phase/NUM_DIGITS)/3; - // Divide by 10 to slow down the rainbow rotation rate. - // TODO Make this /10 a parameter - uint16_t phase = millis()/16 % max_phase; - + // Rainbow roatation speed now configurable + uint16_t duration = uint16_t(round(getRainbowDuration() * 1000)); + uint16_t phase = uint16_t(round(float(millis() % duration) / duration * max_phase)); + for (uint8_t digit=0; digit < NUM_DIGITS; digit++) { // Shift the phase for this LED. uint16_t my_phase = (phase + digit*phase_per_digit) % max_phase; diff --git a/EleksTubeHAX_pio/src/Backlights.h b/EleksTubeHAX_pio/src/Backlights.h index 241d882..df63dc1 100644 --- a/EleksTubeHAX_pio/src/Backlights.h +++ b/EleksTubeHAX_pio/src/Backlights.h @@ -33,6 +33,7 @@ class Backlights: public Adafruit_NeoPixel { void togglePower() { off = !off; pattern_needs_init = true; } void PowerOn() { off = false; pattern_needs_init = true; } void PowerOff() { off = true; pattern_needs_init = true; } + bool getPower() { return !off; } void setPattern(patterns p) { config->pattern = uint8_t(p); pattern_needs_init = true; } patterns getPattern() { return patterns(config->pattern); } @@ -43,8 +44,12 @@ class Backlights: public Adafruit_NeoPixel { // Configure the patterns void setPulseRate(uint8_t bpm) { config->pulse_bpm = bpm; } + uint8_t getPulseRate() { return config->pulse_bpm; } void setBreathRate(uint8_t per_min) { config->breath_per_min = per_min; } - + uint8_t getBreathRate() { return config->breath_per_min; } + void setRainbowDuration(float seconds) { config->rainbow_sec = seconds; } + float getRainbowDuration() { return config->rainbow_sec; } + // Used by all constant color patterns. void setColorPhase(uint16_t phase) { config->color_phase = phase % max_phase; pattern_needs_init = true; } void adjustColorPhase(int16_t adj); @@ -56,7 +61,16 @@ class Backlights: public Adafruit_NeoPixel { uint8_t getIntensity() { return config->intensity; } bool dimming = false; - + + // Helper methods + uint32_t phaseToColor(uint16_t phase); + uint32_t hueToPhase(float hue); + float phaseToHue(uint32_t phase); + uint8_t phaseToIntensity(uint16_t phase); + + const uint16_t max_phase = 768; // 256 up, 256 down, 256 off + const uint8_t max_intensity = 8; // 0 to 7 + private: bool pattern_needs_init; bool off; @@ -69,15 +83,10 @@ class Backlights: public Adafruit_NeoPixel { void rainbowPattern(); void pulsePattern(); void breathPattern(); - - // Helper methods - uint8_t phaseToIntensity(uint16_t phase); - uint32_t phaseToColor(uint16_t phase); - - const uint16_t max_phase = 768; // 256 up, 256 down, 256 off - const uint8_t max_intensity = 8; // 0 to 7 + const uint32_t test_ms_delay = 250; - }; +extern Backlights backlights; + #endif // BACKLIGHTS_H diff --git a/EleksTubeHAX_pio/src/Clock.h b/EleksTubeHAX_pio/src/Clock.h index 63a2597..81e3106 100644 --- a/EleksTubeHAX_pio/src/Clock.h +++ b/EleksTubeHAX_pio/src/Clock.h @@ -81,8 +81,9 @@ class Clock { uint8_t getSecondsTens() { return getSecond()/10; } uint8_t getSecondsOnes() { return getSecond()%10; } -private: time_t loop_time, local_time; + +private: bool time_valid; StoredConfig::Config::Clock *config; @@ -93,6 +94,6 @@ class Clock { const static uint32_t refresh_ntp_every_ms = 3600000; // Get new NTP every hour, use RTC in between. }; - +extern Clock uclock; #endif // CLOCK_H diff --git a/EleksTubeHAX_pio/src/GLOBAL_DEFINES.h b/EleksTubeHAX_pio/src/GLOBAL_DEFINES.h index 47aa10d..b6994da 100644 --- a/EleksTubeHAX_pio/src/GLOBAL_DEFINES.h +++ b/EleksTubeHAX_pio/src/GLOBAL_DEFINES.h @@ -21,7 +21,7 @@ // ************* Version Infomation ************* #define DEVICE_NAME "IPS-clock" -#define FIRMWARE_VERSION "SmittyHalibut & aly-fly IPS clock v1.0" +#define FIRMWARE_VERSION "SmittyHalibut & aly-fly IPS clock v1.0 HA Edition victorvuelma & gamba69" #define SAVED_CONFIG_NAMESPACE "configs" @@ -34,7 +34,7 @@ // ************ MQTT config ********************* #define MQTT_RECONNECT_WAIT_SEC 30 // how long to wait between retries to connect to broker -#define MQTT_REPORT_STATUS_EVERY_SEC 71 // How often report status to MQTT Broker +#define MQTT_REPORT_STATUS_EVERY_SEC 15 // How often report status to MQTT Broker // ************ Temperature config ********************* diff --git a/EleksTubeHAX_pio/src/Mqtt_client_ips.cpp b/EleksTubeHAX_pio/src/Mqtt_client_ips.cpp index 4bace06..a44d414 100644 --- a/EleksTubeHAX_pio/src/Mqtt_client_ips.cpp +++ b/EleksTubeHAX_pio/src/Mqtt_client_ips.cpp @@ -13,13 +13,30 @@ #include "Mqtt_client_ips.h" #include "WiFi.h" // for ESP32 #include // Download and install this library first from: https://www.arduinolibraries.info/libraries/pub-sub-client +#include #include "TempSensor.h" +#include "TFTs.h" +#include "Backlights.h" +#include "Clock.h" + +#define concat2(first, second) first second +#define concat3(first, second, third) first second third +#define concat4(first, second, third, fourth) first second third fourth WiFiClient espClient; PubSubClient MQTTclient(espClient); +#define MQTT_STATE_ON "ON" +#define MQTT_STATE_OFF "OFF" + +#define MQTT_BRIGHTNESS_MIN 0 +#define MQTT_BRIGHTNESS_MAX 255 + +#define MQTT_ITENSITY_MIN 0 +#define MQTT_ITENSITY_MAX 7 + // private: -int splitTopic(char* topic, char* tokens[], int tokensNumber); +int splitCommand(char* topic, char* tokens[], int tokensNumber); void callback(char* topic, byte* payload, unsigned int length); void MqttProcessCommand(); @@ -29,8 +46,9 @@ void MqttReportPowerState(); void MqttReportWiFiSignal(); void MqttReportTemperature(); void MqttReportNotification(String message); +void MqttReportGraphic(bool force); void MqttReportBackOnChange(); -void MqttReportBackEverything(); +void MqttReportBackEverything(bool force); void MqttPeriodicReportBack(); char topic[100]; @@ -42,25 +60,97 @@ uint32_t LastTimeTriedToConnect = 0; bool MqttConnected = true; // skip error meggase if disabled // commands from server // = "directive/status" bool MqttCommandPower = true; -int MqttCommandState = 1; +bool MqttCommandMainPower = true; +bool MqttCommandBackPower = true; bool MqttCommandPowerReceived = false; +bool MqttCommandMainPowerReceived = false; +bool MqttCommandBackPowerReceived = false; + +bool MqttCommandUseTwelveHours = false; +bool MqttCommandUseTwelveHoursReceived = false; +bool MqttCommandBlankZeroHours = false; +bool MqttCommandBlankZeroHoursReceived = false; + +int MqttCommandState = 1; bool MqttCommandStateReceived = false; +uint8_t MqttCommandBrightness = -1; +uint8_t MqttCommandMainBrightness = -1; +uint8_t MqttCommandBackBrightness = -1; +bool MqttCommandBrightnessReceived = false; +bool MqttCommandMainBrightnessReceived = false; +bool MqttCommandBackBrightnessReceived = false; + +char MqttCommandPattern[24] = ""; +bool MqttCommandPatternReceived = false; +char MqttCommandBackPattern[24] = ""; +bool MqttCommandBackPatternReceived = false; + +uint16_t MqttCommandBackColorPhase = -1; +bool MqttCommandBackColorPhaseReceived = false; + +uint8_t MqttCommandGraphic = -1; +bool MqttCommandGraphicReceived = false; +uint8_t MqttCommandMainGraphic = -1; +bool MqttCommandMainGraphicReceived = false; + +uint8_t MqttCommandPulseBpm = -1; +bool MqttCommandPulseBpmReceived = false; + +uint8_t MqttCommandBreathBpm = -1; +bool MqttCommandBreathBpmReceived = false; + +float MqttCommandRainbowSec = -1; +bool MqttCommandRainbowSecReceived = false; + // status to server bool MqttStatusPower = true; +bool MqttStatusMainPower = true; +bool MqttStatusBackPower = true; +bool MqttStatusUseTwelveHours = true; +bool MqttStatusBlankZeroHours = true; int MqttStatusState = 0; int MqttStatusBattery = 7; +uint8_t MqttStatusBrightness = 0; +uint8_t MqttStatusMainBrightness = 0; +uint8_t MqttStatusBackBrightness = 0; +char MqttStatusPattern[24] = ""; +char MqttStatusBackPattern[24] = ""; +uint16_t MqttStatusBackColorPhase = 0; +uint8_t MqttStatusGraphic = 0; +uint8_t MqttStatusMainGraphic = 0; +uint8_t MqttStatusPulseBpm = 0; +uint8_t MqttStatusBreathBpm = 0; +float MqttStatusRainbowSec = 0; int LastSentSignalLevel = 999; int LastSentPowerState = -1; +int LastSentMainPowerState = -1; +int LastSentBackPowerState = -1; int LastSentStatus = -1; +int LastSentBrightness = -1; +int LastSentMainBrightness = -1; +int LastSentBackBrightness = -1; +char LastSentPattern[24] = ""; +char LastSentBackPattern[24] = ""; +int LastSentBackColorPhase = -1; +int LastSentGraphic = -1; +int LastSentMainGraphic = -1; +bool LastSentUseTwelveHours = false; +bool LastSentBlankZeroHours = false; +uint8_t LastSentPulseBpm = -1; +uint8_t LastSentBreathBpm = -1; +float LastSentRainbowSec = -1; +double round1(double value) { + return (int)(value * 10 + 0.5) / 10.0; +} void sendToBroker(const char* topic, const char* message) { if (MQTTclient.connected()) { char topicArr[100]; sprintf(topicArr, "%s/%s", MQTT_CLIENT, topic); - MQTTclient.publish(topicArr, message); + MQTTclient.publish(topicArr, message, true); #ifdef DEBUG_OUTPUT // long output Serial.print("Sending to MQTT: "); Serial.print(topicArr); @@ -68,39 +158,205 @@ void sendToBroker(const char* topic, const char* message) { Serial.println(message); #else Serial.print("TX MQTT: "); - Serial.print(topic); - Serial.print("/"); + Serial.print(topicArr); + Serial.print(" "); Serial.println(message); #endif delay (120); } } +void MqttReportState(bool force) { +#ifdef MQTT_HOME_ASSISTANT + if(MQTTclient.connected()) { + // time_t rawtime; + // struct tm * timeinfo; + // char last_seen [80]; + // rawtime = uclock.local_time; + // timeinfo = localtime (&rawtime); + // strftime (last_seen, 80, "%Y-%m-%dT%H:%M:%SZ", timeinfo); + + if(force + || MqttStatusMainPower != LastSentMainPowerState + || MqttStatusMainBrightness != LastSentMainBrightness + || MqttStatusMainGraphic != LastSentMainGraphic) { + + JsonDocument state; + state["state"] = MqttStatusMainPower == 0 ? MQTT_STATE_OFF : MQTT_STATE_ON; + state["brightness"] = MqttStatusMainBrightness; + state["effect"] = tfts.clockFaceToName(MqttStatusMainGraphic); + // state["Last seen"] = last_seen; + + char buffer[256]; + size_t n = serializeJson(state, buffer); + const char* topic = concat2(MQTT_CLIENT, "/main"); + MQTTclient.publish(topic, buffer, true); + LastSentMainPowerState = MqttStatusMainPower; + LastSentMainBrightness = MqttStatusMainBrightness; + LastSentMainGraphic = MqttStatusMainGraphic; + + Serial.print("TX MQTT: "); + Serial.print(topic); + Serial.print(" "); + Serial.println(buffer); + } + + if(force + || MqttStatusBackPower != LastSentBackPowerState + || MqttStatusBackBrightness != LastSentBackBrightness + || strcmp(MqttStatusBackPattern, LastSentBackPattern) != 0 + || MqttStatusBackColorPhase != LastSentBackColorPhase ) { + + JsonDocument state; + state["state"] = MqttStatusBackPower == 0 ? MQTT_STATE_OFF : MQTT_STATE_ON; + state["brightness"] = MqttStatusBackBrightness; //map(MqttStatusBackBrightness, MQTT_ITENSITY_MIN, MQTT_ITENSITY_MAX, MQTT_BRIGHTNESS_MIN, MQTT_BRIGHTNESS_MAX); + state["effect"] = MqttStatusBackPattern; + state["color_mode"] = "hs"; + state["color"]["h"] = backlights.phaseToHue(MqttStatusBackColorPhase); + state["color"]["s"] = 100.f; + // state["color"]["b"] = backlights.phaseToIntensity((MqttStatusBackColorPhase + 512) % backlights.max_phase); + // state["Last seen"] = last_seen; + + char buffer[256]; + size_t n = serializeJson(state, buffer); + const char* topic = concat2(MQTT_CLIENT, "/back"); + MQTTclient.publish(topic, buffer, true); + LastSentBackPowerState = MqttStatusBackPower; + LastSentBackBrightness = MqttStatusBackBrightness; + strcpy(LastSentBackPattern, MqttStatusBackPattern); + LastSentBackColorPhase = MqttStatusBackColorPhase; + + Serial.print("TX MQTT: "); + Serial.print(topic); + Serial.print(" "); + Serial.println(buffer); + } + + if(force + || MqttStatusUseTwelveHours != LastSentUseTwelveHours) { + + JsonDocument state; + state["state"] = MqttStatusUseTwelveHours ? MQTT_STATE_ON : MQTT_STATE_OFF; + // state["Last seen"] = last_seen; + + char buffer[256]; + size_t n = serializeJson(state, buffer); + const char* topic = concat2(MQTT_CLIENT, "/use_twelve_hours"); + MQTTclient.publish(topic, buffer, true); + LastSentUseTwelveHours = MqttStatusUseTwelveHours; + + Serial.print("TX MQTT: "); + Serial.print(topic); + Serial.print(" "); + Serial.println(buffer); + } + + if(force + || MqttStatusBlankZeroHours != LastSentBlankZeroHours) { + + JsonDocument state; + state["state"] = MqttStatusBlankZeroHours ? MQTT_STATE_ON : MQTT_STATE_OFF; + // state["Last seen"] = last_seen; + + char buffer[256]; + size_t n = serializeJson(state, buffer); + const char* topic = concat2(MQTT_CLIENT, "/blank_zero_hours"); + MQTTclient.publish(topic, buffer, true); + LastSentBlankZeroHours = MqttStatusBlankZeroHours; + + Serial.print("TX MQTT: "); + Serial.print(topic); + Serial.print(" "); + Serial.println(buffer); + } + + if(force + || MqttStatusPulseBpm != LastSentPulseBpm) { + + JsonDocument state; + state["state"] = MqttStatusPulseBpm; + // state["Last seen"] = last_seen; + + char buffer[256]; + size_t n = serializeJson(state, buffer); + const char* topic = concat2(MQTT_CLIENT, "/pulse_bpm"); + MQTTclient.publish(topic, buffer, true); + LastSentPulseBpm = MqttStatusPulseBpm; + + Serial.print("TX MQTT: "); + Serial.print(topic); + Serial.print(" "); + Serial.println(buffer); + } + + if(force + || MqttStatusBreathBpm != LastSentBreathBpm) { + + JsonDocument state; + state["state"] = MqttStatusBreathBpm; + // state["Last seen"] = last_seen; + + char buffer[256]; + size_t n = serializeJson(state, buffer); + const char* topic = concat2(MQTT_CLIENT, "/breath_bpm"); + MQTTclient.publish(topic, buffer, true); + LastSentBreathBpm = MqttStatusBreathBpm; + + Serial.print("TX MQTT: "); + Serial.print(topic); + Serial.print(" "); + Serial.println(buffer); + } + + if(force + || MqttStatusRainbowSec != LastSentRainbowSec) { + + JsonDocument state; + state["state"] = round1(MqttStatusRainbowSec); + // state["Last seen"] = last_seen; + + char buffer[256]; + serializeJson(state, buffer); + const char* topic = concat2(MQTT_CLIENT, "/rainbow_duration"); + MQTTclient.publish(topic, buffer, true); + LastSentRainbowSec = MqttStatusRainbowSec; + + Serial.print("TX MQTT: "); + Serial.print(topic); + Serial.print(" "); + Serial.println(buffer); + } + } +#endif +} + void MqttStart() { #ifdef MQTT_ENABLED MqttConnected = false; - if (((millis() - LastTimeTriedToConnect) > (MQTT_RECONNECT_WAIT_SEC * 1000)) || (LastTimeTriedToConnect == 0)) { + if (((millis() - LastTimeTriedToConnect) > (MQTT_RECONNECT_WAIT_SEC * 100)) || (LastTimeTriedToConnect == 0)) { LastTimeTriedToConnect = millis(); MQTTclient.setServer(MQTT_BROKER, MQTT_PORT); MQTTclient.setCallback(callback); + MQTTclient.setBufferSize(2048); Serial.println("Connecting to MQTT..."); if (MQTTclient.connect(MQTT_CLIENT, MQTT_USERNAME, MQTT_PASSWORD)) { Serial.println("MQTT connected"); MqttConnected = true; } else { - if (MQTTclient.state() == 5) { - Serial.println("Connection not allowed by broker, possible reasons:"); - Serial.println("- Device is already online. Wait some seconds until it appears offline"); - Serial.println("- Wrong Username or password. Check credentials"); - Serial.println("- Client Id does not belong to this username, verify ClientId"); - } else { - Serial.print("Not possible to connect to Broker Error code:"); - Serial.println(MQTTclient.state()); - } - return; // do not continue if not connected + if (MQTTclient.state() == 5) { + Serial.println("Connection not allowed by broker, possible reasons:"); + Serial.println("- Device is already online. Wait some seconds until it appears offline"); + Serial.println("- Wrong Username or password. Check credentials"); + Serial.println("- Client Id does not belong to this username, verify ClientId"); + } else { + Serial.print("Not possible to connect to Broker Error code:"); + Serial.println(MQTTclient.state()); } + return; // do not continue if not connected + } + #ifndef MQTT_HOME_ASSISTANT char subscibeTopic[100]; sprintf(subscibeTopic, "%s/#", MQTT_CLIENT); MQTTclient.subscribe(subscibeTopic); //Subscribes to all messages send to the device @@ -110,78 +366,216 @@ void MqttStart() { sendToBroker("report/ip", (char*)WiFi.localIP().toString().c_str()); // Reports the ip sendToBroker("report/network", (char*)WiFi.SSID().c_str()); // Reports the network name MqttReportWiFiSignal(); + #endif + + #ifdef MQTT_HOME_ASSISTANT + char subscibeTopic[100]; + sprintf(subscibeTopic, "%s/main/set", MQTT_CLIENT); + MQTTclient.subscribe(subscibeTopic); + + sprintf(subscibeTopic, "%s/back/set", MQTT_CLIENT); + MQTTclient.subscribe(subscibeTopic); + + sprintf(subscibeTopic, "%s/use_twelve_hours/set", MQTT_CLIENT); + MQTTclient.subscribe(subscibeTopic); + + sprintf(subscibeTopic, "%s/blank_zero_hours/set", MQTT_CLIENT); + MQTTclient.subscribe(subscibeTopic); + + sprintf(subscibeTopic, "%s/pulse_bpm/set", MQTT_CLIENT); + MQTTclient.subscribe(subscibeTopic); + + sprintf(subscibeTopic, "%s/breath_bpm/set", MQTT_CLIENT); + MQTTclient.subscribe(subscibeTopic); + + sprintf(subscibeTopic, "%s/rainbow_duration/set", MQTT_CLIENT); + MQTTclient.subscribe(subscibeTopic); + #endif } #endif } -int splitTopic(char* topic, char* tokens[], int tokensNumber) { - const char s[2] = "/"; - int pos = 0; - tokens[0] = strtok(topic, s); - while (pos < tokensNumber - 1 && tokens[pos] != NULL) { - pos++; - tokens[pos] = strtok(NULL, s); - } - return pos; +int splitCommand(char* topic, char* tokens[], int tokensNumber) { + int mqttClientLength = strlen(MQTT_CLIENT); + int topicLength = strlen(topic); + int finalLength = topicLength - mqttClientLength + 2; + char* command = (char*) malloc(finalLength); + + strncpy(command, topic + (mqttClientLength + 1), finalLength - 2); + + const char s[2] = "/"; + int pos = 0; + tokens[0] = strtok(command, s); + while (pos < tokensNumber - 1 && tokens[pos] != NULL) { + pos++; + tokens[pos] = strtok(NULL, s); + } + + free(command); + + return pos; } void checkMqtt() { MqttConnected = MQTTclient.connected(); if (!MqttConnected) { - MqttStart(); - } + MqttStart(); + } } void callback(char* topic, byte* payload, unsigned int length) { //A new message has been received -#ifdef DEBUG_OUTPUT - Serial.print("Received MQTT topic: "); - Serial.print(topic); // long output -#endif - int tokensNumber = 10; - char* tokens[tokensNumber]; - char message[length + 1]; - tokensNumber = splitTopic(topic, tokens, tokensNumber); - sprintf(message, "%c", (char)payload[0]); - for (int i = 1; i < length; i++) { - sprintf(message, "%s%c", message, (char)payload[i]); - } -#ifdef DEBUG_OUTPUT - Serial.print("\t Message: "); - Serial.println(message); -#else - Serial.print("MQTT RX: "); - Serial.print(tokens[1]); - Serial.print("/"); - Serial.print(tokens[2]); - Serial.print("/"); - Serial.println(message); -#endif + #ifdef DEBUG_OUTPUT + Serial.print("Received MQTT topic: "); + Serial.print(topic); // long output + #endif + int commandNumber = 10; + char* command[commandNumber]; + commandNumber = splitCommand(topic, command, commandNumber); - if (tokensNumber < 3) { - // otherwise code below crashes on the strmp on non-initialized pointers in tokens[] array - Serial.println("Number of tokens in MQTT message < 3!"); - return; - } + #ifndef MQTT_HOME_ASSISTANT + char message[length + 1]; + sprintf(message, "%c", (char)payload[0]); + for (int i = 1; i < length; i++) { + sprintf(message, "%s%c", message, (char)payload[i]); + } + #ifdef DEBUG_OUTPUT + Serial.print("\t Message: "); + Serial.println(message); + #else + Serial.print("MQTT RX: "); + Serial.print(command[0]); + Serial.print("/"); + Serial.print(command[1]); + Serial.print("/"); + Serial.println(message); + #endif + + if (commandNumber < 2) { + // otherwise code below crashes on the strmp on non-initialized pointers in command[] array + Serial.println("Number of command in MQTT message < 2!"); + return; + } //------------------Decide what to do depending on the topic and message--------------------------------- - if (strcmp(tokens[1], "directive") == 0 && strcmp(tokens[2], "powerState") == 0) { // Turn On or OFF - if (strcmp(message, "ON") == 0) { - MqttCommandPower = true; - MqttCommandPowerReceived = true; - MqttReportBackEverything(); - } else if (strcmp(message, "OFF") == 0) { - MqttCommandPower = false; - MqttCommandPowerReceived = true; - MqttReportBackEverything(); - } // SmartNest: // SmartThings - } else if (strcmp(tokens[1], "directive") == 0 && (strcmp(tokens[2], "setpoint") == 0) || (strcmp(tokens[2], "percentage") == 0)) { - double valueD = atof(message); - if (!isnan(valueD)) { - MqttCommandState = (int) valueD; - MqttCommandStateReceived = true; - MqttReportBackEverything(); - } + if (strcmp(command[0], "directive") == 0 && strcmp(command[1], "powerState") == 0) { // Turn On or OFF + if (strcmp(message, "ON") == 0) { + MqttCommandPower = true; + MqttCommandPowerReceived = true; + } else if (strcmp(message, "OFF") == 0) { + MqttCommandPower = false; + MqttCommandPowerReceived = true; + } // SmartNest: // SmartThings + } else if (strcmp(command[0], "directive") == 0 && (strcmp(command[1], "setpoint") == 0) || (strcmp(command[1], "percentage") == 0)) { + double valueD = atof(message); + if (!isnan(valueD)) { + MqttCommandState = (int) valueD; + MqttCommandStateReceived = true; } + } + #endif + + #ifdef MQTT_HOME_ASSISTANT + char message[length + 1]; + sprintf(message, "%c", (char)payload[0]); + for (int i = 1; i < length; i++) { + sprintf(message, "%s%c", message, (char)payload[i]); + } + Serial.print("RX MQTT: "); + Serial.print(topic); + Serial.print(" "); + Serial.println(message); + if (strcmp(command[0], "main") == 0 && strcmp(command[1], "set") == 0) { + JsonDocument doc; + deserializeJson(doc, payload, length); + + if(doc.containsKey("state")) { + MqttCommandMainPower = doc["state"] == MQTT_STATE_ON; + MqttCommandMainPowerReceived = true; + } + if(doc.containsKey("brightness")) { + MqttCommandMainBrightness = doc["brightness"]; + MqttCommandMainBrightnessReceived = true; + } + if(doc.containsKey("effect")) { + MqttCommandMainGraphic = tfts.nameToClockFace(doc["effect"]); + MqttCommandMainGraphicReceived = true; + } + + doc.clear(); + } + if (strcmp(command[0], "back") == 0 && strcmp(command[1], "set") == 0) { + JsonDocument doc; + deserializeJson(doc, payload, length); + + if(doc.containsKey("state")) { + MqttCommandBackPower = doc["state"] == MQTT_STATE_ON; + MqttCommandBackPowerReceived = true; + } + if(doc.containsKey("brightness")) { + MqttCommandBackBrightness = doc["brightness"]; //map(doc["brightness"], MQTT_BRIGHTNESS_MIN, MQTT_BRIGHTNESS_MAX, MQTT_ITENSITY_MIN, MQTT_ITENSITY_MAX); + MqttCommandBackBrightnessReceived = true; + } + if(doc.containsKey("effect")) { + strcpy(MqttCommandBackPattern, doc["effect"]); + MqttCommandBackPatternReceived = true; + } + if(doc.containsKey("color")) { + MqttCommandBackColorPhase = backlights.hueToPhase(doc["color"]["h"]); + MqttCommandBackColorPhaseReceived = true; + } + doc.clear(); + } + if (strcmp(command[0], "use_twelve_hours") == 0 && strcmp(command[1], "set") == 0) { + JsonDocument doc; + deserializeJson(doc, payload, length); + + if(doc.containsKey("state")) { + MqttCommandUseTwelveHours = doc["state"] == MQTT_STATE_ON; + MqttCommandUseTwelveHoursReceived = true; + } + doc.clear(); + } + if (strcmp(command[0], "blank_zero_hours") == 0 && strcmp(command[1], "set") == 0) { + JsonDocument doc; + deserializeJson(doc, payload, length); + + if(doc.containsKey("state")) { + MqttCommandBlankZeroHours = doc["state"] == MQTT_STATE_ON; + MqttCommandBlankZeroHoursReceived = true; + } + doc.clear(); + } + if (strcmp(command[0], "pulse_bpm") == 0 && strcmp(command[1], "set") == 0) { + JsonDocument doc; + deserializeJson(doc, payload, length); + + if(doc.containsKey("state")) { + MqttCommandPulseBpm = uint8_t(doc["state"]); + MqttCommandPulseBpmReceived = true; + } + doc.clear(); + } + if (strcmp(command[0], "breath_bpm") == 0 && strcmp(command[1], "set") == 0) { + JsonDocument doc; + deserializeJson(doc, payload, length); + + if(doc.containsKey("state")) { + MqttCommandBreathBpm = uint8_t(doc["state"]); + MqttCommandBreathBpmReceived = true; + } + doc.clear(); + } + if (strcmp(command[0], "rainbow_duration") == 0 && strcmp(command[1], "set") == 0) { + JsonDocument doc; + deserializeJson(doc, payload, length); + + if(doc.containsKey("state")) { + MqttCommandRainbowSec = float(doc["state"]); + MqttCommandRainbowSecReceived = true; + } + doc.clear(); + } + #endif } void MqttLoopFrequently(){ @@ -201,7 +595,7 @@ void MqttLoopInFreeTime(){ void MqttReportBattery() { char message2[5]; sprintf(message2, "%d", MqttStatusBattery); - sendToBroker("report/battery", message2); + sendToBroker("report/battery", message2); } void MqttReportStatus() { @@ -222,12 +616,9 @@ void MqttReportTemperature() { } void MqttReportPowerState() { - if (MqttStatusPower != LastSentPowerState) { - if (MqttStatusPower != 0) { - sendToBroker("report/powerState", "ON"); - } else { - sendToBroker("report/powerState", "OFF"); - } + if (MqttStatusPower != LastSentPowerState) { + sendToBroker("report/powerState", MqttStatusPower == 0 ? MQTT_STATE_OFF : MQTT_STATE_ON); + LastSentPowerState = MqttStatusPower; } } @@ -258,22 +649,295 @@ void MqttReportNotification(String message) { } } -void MqttReportBackEverything() { +void MqttReportGraphic(bool force) { + if (force || MqttStatusGraphic != LastSentGraphic) { + char graphic[2] = ""; + sprintf(graphic, "%i", MqttStatusGraphic); + sendToBroker("graphic", graphic); // Reports the signal strength + + LastSentGraphic = MqttStatusGraphic; + } +} + +void MqttReportBackEverything(bool force) { + if(MQTTclient.connected()) { + #ifndef MQTT_HOME_ASSISTANT MqttReportPowerState(); MqttReportStatus(); -// MqttReportBattery(); + // MqttReportBattery(); MqttReportWiFiSignal(); MqttReportTemperature(); + #endif + + #ifdef MQTT_HOME_ASSISTANT + MqttReportState(force); + #endif + lastTimeSent = millis(); + } +} + +bool discoveryReported = false; + +void MqttReportDiscovery() { + #ifdef MQTT_HOME_ASSISTANT_DISCOVERY + char json_buffer[1024]; + JsonDocument discovery; + + // Main Light + discovery["device"]["identifiers"][0] = MQTT_CLIENT; + discovery["device"]["manufacturer"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MANUFACTURER; + discovery["device"]["model"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["name"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["sw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_SW_VERSION; + discovery["device"]["hw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_HW_VERSION; + discovery["device"]["connections"][0][0] = "mac"; + discovery["device"]["connections"][0][1] = WiFi.macAddress(); + discovery["unique_id"] = concat2(MQTT_CLIENT, "_main"); + discovery["object_id"] = concat2(MQTT_CLIENT, "_main"); + discovery["name"] = "Main"; + discovery["schema"] = "json"; + discovery["state_topic"] = concat2(MQTT_CLIENT, "/main"); + discovery["json_attributes_topic"] = concat2(MQTT_CLIENT, "/main"); + discovery["command_topic"] = concat2(MQTT_CLIENT, "/main/set"); + discovery["brightness"] = true; + discovery["brightness_scale"] = 255; + discovery["effect"] = true; + for (size_t i = 1; i <= tfts.NumberOfClockFaces; i++) + { + discovery["effect_list"][i-1] = tfts.clockFaceToName(i); + } + size_t main_n = serializeJson(discovery, json_buffer); + const char* main_topic = concat3("homeassistant/light/", MQTT_CLIENT, "_main/light/config"); + MQTTclient.publish(main_topic, json_buffer, true); + delay(120); + Serial.print("TX MQTT: "); + Serial.print(main_topic); + Serial.print(" "); + Serial.println(json_buffer); + discovery.clear(); + + // Back Light + discovery["device"]["identifiers"][0] = MQTT_CLIENT; + discovery["device"]["manufacturer"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MANUFACTURER; + discovery["device"]["model"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["name"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["sw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_SW_VERSION; + discovery["device"]["hw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_HW_VERSION; + discovery["device"]["connections"][0][0] = "mac"; + discovery["device"]["connections"][0][1] = WiFi.macAddress(); + discovery["unique_id"] = concat2(MQTT_CLIENT, "_back"); + discovery["object_id"] = concat2(MQTT_CLIENT, "_back"); + discovery["name"] = "Back"; + discovery["schema"] = "json"; + discovery["state_topic"] = concat2(MQTT_CLIENT, "/back"); + discovery["json_attributes_topic"] = concat2(MQTT_CLIENT, "/back"); + discovery["command_topic"] = concat2(MQTT_CLIENT, "/back/set"); + discovery["brightness"] = true; + discovery["brightness_scale"] = 7; + discovery["effect"] = true; + for (size_t i = 0; i < backlights.num_patterns; i++) + { + discovery["effect_list"][i] = backlights.patterns_str[i]; + } + discovery["supported_color_modes"][0] = "hs"; + size_t back_n = serializeJson(discovery, json_buffer); + const char* back_topic = concat3("homeassistant/light/", MQTT_CLIENT, "_back/light/config"); + MQTTclient.publish(back_topic, json_buffer, true); + delay(120); + Serial.print("TX MQTT: "); + Serial.print(back_topic); + Serial.print(" "); + Serial.println(json_buffer); + discovery.clear(); + + // Use Twelwe Hours + discovery["device"]["identifiers"][0] = MQTT_CLIENT; + discovery["device"]["manufacturer"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MANUFACTURER; + discovery["device"]["model"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["name"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["sw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_SW_VERSION; + discovery["device"]["hw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_HW_VERSION; + discovery["device"]["connections"][0][0] = "mac"; + discovery["device"]["connections"][0][1] = WiFi.macAddress(); + discovery["unique_id"] = concat2(MQTT_CLIENT, "_use_twelve_hours"); + discovery["object_id"] = concat2(MQTT_CLIENT, "_use_twelve_hours"); + discovery["entity_category"] = "config"; + discovery["name"] = "Use Twelve Hours"; + discovery["state_topic"] = concat2(MQTT_CLIENT, "/use_twelve_hours"); + discovery["json_attributes_topic"] = concat2(MQTT_CLIENT, "/use_twelve_hours"); + discovery["command_topic"] = concat2(MQTT_CLIENT, "/use_twelve_hours/set"); + discovery["value_template"] = "{{ value_json.state }}"; + discovery["state_on"] = "ON"; + discovery["state_off"] = "OFF"; + discovery["payload_on"] = "{\"state\":\"ON\"}"; + discovery["payload_off"] = "{\"state\":\"OFF\"}"; + size_t useTwelveHours_n = serializeJson(discovery, json_buffer); + const char* useTwelveHours_topic = concat3("homeassistant/switch/", MQTT_CLIENT, "_use_twelve_hours/switch/config"); + MQTTclient.publish(useTwelveHours_topic, json_buffer, true); + delay(120); + Serial.print("TX MQTT: "); + Serial.print(useTwelveHours_topic); + Serial.print(" "); + Serial.println(json_buffer); + discovery.clear(); + + // Blank Zero Hours + discovery["device"]["identifiers"][0] = MQTT_CLIENT; + discovery["device"]["manufacturer"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MANUFACTURER; + discovery["device"]["model"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["name"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["sw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_SW_VERSION; + discovery["device"]["hw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_HW_VERSION; + discovery["device"]["connections"][0][0] = "mac"; + discovery["device"]["connections"][0][1] = WiFi.macAddress(); + discovery["unique_id"] = concat2(MQTT_CLIENT, "_blank_zero_hours"); + discovery["object_id"] = concat2(MQTT_CLIENT, "_blank_zero_hours"); + discovery["entity_category"] = "config"; + discovery["name"] = "Blank Zero Hours"; + discovery["state_topic"] = concat2(MQTT_CLIENT, "/blank_zero_hours"); + discovery["json_attributes_topic"] = concat2(MQTT_CLIENT, "/blank_zero_hours"); + discovery["command_topic"] = concat2(MQTT_CLIENT, "/blank_zero_hours/set"); + discovery["value_template"] = "{{ value_json.state }}"; + discovery["state_on"] = "ON"; + discovery["state_off"] = "OFF"; + discovery["payload_on"] = "{\"state\":\"ON\"}"; + discovery["payload_off"] = "{\"state\":\"OFF\"}"; + size_t blankZeroHours_n = serializeJson(discovery, json_buffer); + const char* blankZeroHours_topic = concat3("homeassistant/switch/", MQTT_CLIENT, "_blank_zero_hours/switch/config"); + MQTTclient.publish(blankZeroHours_topic, json_buffer, true); + delay(120); + Serial.print("TX MQTT: "); + Serial.print(blankZeroHours_topic); + Serial.print(" "); + Serial.println(json_buffer); + discovery.clear(); + + // Pulses per minute + discovery["device"]["identifiers"][0] = MQTT_CLIENT; + discovery["device"]["manufacturer"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MANUFACTURER; + discovery["device"]["model"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["name"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["sw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_SW_VERSION; + discovery["device"]["hw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_HW_VERSION; + discovery["device"]["connections"][0][0] = "mac"; + discovery["device"]["connections"][0][1] = WiFi.macAddress(); + discovery["unique_id"] = concat2(MQTT_CLIENT, "_pulse_bpm"); + discovery["object_id"] = concat2(MQTT_CLIENT, "_pulse_bpm"); + discovery["entity_category"] = "config"; + discovery["name"] = "Pulse, bpm"; + discovery["state_topic"] = concat2(MQTT_CLIENT, "/pulse_bpm"); + discovery["json_attributes_topic"] = concat2(MQTT_CLIENT, "/pulse_bpm"); + discovery["command_topic"] = concat2(MQTT_CLIENT, "/pulse_bpm/set"); + discovery["command_template"] = "{\"state\":{{value}}}"; + discovery["step"] = 1; + discovery["min"] = 20; + discovery["max"] = 120; + discovery["mode"] = "slider"; + discovery["value_template"] = "{{ value_json.state }}"; + size_t pulseBpm_n = serializeJson(discovery, json_buffer); + const char* pulseBpm_topic = concat3("homeassistant/number/", MQTT_CLIENT, "_pulse_bpm/number/config"); + MQTTclient.publish(pulseBpm_topic, json_buffer, true); + delay(120); + Serial.print("TX MQTT: "); + Serial.print(pulseBpm_topic); + Serial.print(" "); + Serial.println(json_buffer); + discovery.clear(); + + // Breathes per minute + discovery["device"]["identifiers"][0] = MQTT_CLIENT; + discovery["device"]["manufacturer"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MANUFACTURER; + discovery["device"]["model"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["name"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["sw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_SW_VERSION; + discovery["device"]["hw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_HW_VERSION; + discovery["device"]["connections"][0][0] = "mac"; + discovery["device"]["connections"][0][1] = WiFi.macAddress(); + discovery["unique_id"] = concat2(MQTT_CLIENT, "_breath_bpm"); + discovery["object_id"] = concat2(MQTT_CLIENT, "_breath_bpm"); + discovery["entity_category"] = "config"; + discovery["name"] = "Breath, bpm"; + discovery["state_topic"] = concat2(MQTT_CLIENT, "/breath_bpm"); + discovery["json_attributes_topic"] = concat2(MQTT_CLIENT, "/breath_bpm"); + discovery["command_topic"] = concat2(MQTT_CLIENT, "/breath_bpm/set"); + discovery["command_template"] = "{\"state\":{{value}}}"; + discovery["step"] = 1; + discovery["min"] = 5; + discovery["max"] = 60; + discovery["mode"] = "slider"; + discovery["value_template"] = "{{ value_json.state }}"; + size_t breathBpm_n = serializeJson(discovery, json_buffer); + const char* breathBpm_topic = concat3("homeassistant/number/", MQTT_CLIENT, "_breath_bpm/number/config"); + MQTTclient.publish(breathBpm_topic, json_buffer, true); + delay(120); + Serial.print("TX MQTT: "); + Serial.print(breathBpm_topic); + Serial.print(" "); + Serial.println(json_buffer); + discovery.clear(); + + // Rainbow duration + discovery["device"]["identifiers"][0] = MQTT_CLIENT; + discovery["device"]["manufacturer"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MANUFACTURER; + discovery["device"]["model"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["name"] = MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL; + discovery["device"]["sw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_SW_VERSION; + discovery["device"]["hw_version"] = MQTT_HOME_ASSISTANT_DISCOVERY_HW_VERSION; + discovery["device"]["connections"][0][0] = "mac"; + discovery["device"]["connections"][0][1] = WiFi.macAddress(); + discovery["unique_id"] = concat2(MQTT_CLIENT, "_rainbow_duration"); + discovery["object_id"] = concat2(MQTT_CLIENT, "_rainbow_duration"); + discovery["entity_category"] = "config"; + discovery["name"] = "Rainbow, sec"; + discovery["state_topic"] = concat2(MQTT_CLIENT, "/rainbow_duration"); + discovery["json_attributes_topic"] = concat2(MQTT_CLIENT, "/rainbow_duration"); + discovery["command_topic"] = concat2(MQTT_CLIENT, "/rainbow_duration/set"); + discovery["command_template"] = "{\"state\":{{value}}}"; + discovery["step"] = 0.1; + discovery["min"] = 0.2; + discovery["max"] = 10; + discovery["mode"] = "slider"; + discovery["value_template"] = "{{ value_json.state }}"; + size_t rainbowSec_n = serializeJson(discovery, json_buffer); + const char* rainbowSec_topic = concat3("homeassistant/number/", MQTT_CLIENT, "_rainbow_duration/number/config"); + MQTTclient.publish(rainbowSec_topic, json_buffer, true); + delay(120); + Serial.print("TX MQTT: "); + Serial.print(rainbowSec_topic); + Serial.print(" "); + Serial.println(json_buffer); + discovery.clear(); + + #endif } void MqttReportBackOnChange() { + if(MQTTclient.connected()) { + #ifndef MQTT_HOME_ASSISTANT MqttReportPowerState(); MqttReportStatus(); + #endif + #ifdef MQTT_HOME_ASSISTANT_DISCOVERY + if(!discoveryReported) { + MqttReportDiscovery(); + discoveryReported = true; + } + #endif + #ifdef MQTT_HOME_ASSISTANT + MqttReportState(false); + #endif + } } void MqttPeriodicReportBack() { if (((millis() - lastTimeSent) > (MQTT_REPORT_STATUS_EVERY_SEC * 1000)) && MQTTclient.connected()) { - MqttReportBackEverything(); + #ifdef MQTT_HOME_ASSISTANT_DISCOVERY + if(!discoveryReported) { + MqttReportDiscovery(); + discoveryReported = true; } + #endif + MqttReportBackEverything(true); + } } diff --git a/EleksTubeHAX_pio/src/Mqtt_client_ips.h b/EleksTubeHAX_pio/src/Mqtt_client_ips.h index c1af90e..d448f48 100644 --- a/EleksTubeHAX_pio/src/Mqtt_client_ips.h +++ b/EleksTubeHAX_pio/src/Mqtt_client_ips.h @@ -7,19 +7,65 @@ extern bool MqttConnected; // commands from server extern bool MqttCommandPower; -extern int MqttCommandState; +extern bool MqttCommandMainPower; +extern bool MqttCommandBackPower; extern bool MqttCommandPowerReceived; +extern bool MqttCommandMainPowerReceived; +extern bool MqttCommandBackPowerReceived; +extern int MqttCommandState; extern bool MqttCommandStateReceived; +extern uint8_t MqttCommandBrightness; +extern uint8_t MqttCommandMainBrightness; +extern uint8_t MqttCommandBackBrightness; +extern bool MqttCommandBrightnessReceived; +extern bool MqttCommandMainBrightnessReceived; +extern bool MqttCommandBackBrightnessReceived; +extern char MqttCommandPattern[]; +extern char MqttCommandBackPattern[]; +extern bool MqttCommandPatternReceived; +extern bool MqttCommandBackPatternReceived; +extern uint16_t MqttCommandBackColorPhase; +extern bool MqttCommandBackColorPhaseReceived; +extern uint8_t MqttCommandGraphic; +extern uint8_t MqttCommandMainGraphic; +extern bool MqttCommandGraphicReceived; +extern bool MqttCommandMainGraphicReceived; +extern bool MqttCommandUseTwelveHours; +extern bool MqttCommandUseTwelveHoursReceived; +extern bool MqttCommandBlankZeroHours; +extern bool MqttCommandBlankZeroHoursReceived; +extern uint8_t MqttCommandPulseBpm; +extern bool MqttCommandPulseBpmReceived; +extern uint8_t MqttCommandBreathBpm; +extern bool MqttCommandBreathBpmReceived; +extern float MqttCommandRainbowSec; +extern bool MqttCommandRainbowSecReceived; // status to server extern bool MqttStatusPower; +extern bool MqttStatusMainPower; +extern bool MqttStatusBackPower; extern int MqttStatusState; extern int MqttStatusBattery; +extern uint8_t MqttStatusBrightness; +extern uint8_t MqttStatusMainBrightness; +extern uint8_t MqttStatusBackBrightness; +extern char MqttStatusPattern[]; +extern char MqttStatusBackPattern[]; +extern uint16_t MqttStatusBackColorPhase; +extern uint8_t MqttStatusGraphic; +extern uint8_t MqttStatusMainGraphic; +extern bool MqttStatusUseTwelveHours; +extern bool MqttStatusBlankZeroHours; +extern uint8_t MqttStatusPulseBpm; +extern uint8_t MqttStatusBreathBpm; +extern float MqttStatusRainbowSec; // functions void MqttStart(); void MqttLoopFrequently(); void MqttLoopInFreeTime(); +void MqttReportBackEverything(bool force); #endif /* mqtt_client_H_ */ diff --git a/EleksTubeHAX_pio/src/StoredConfig.h b/EleksTubeHAX_pio/src/StoredConfig.h index 2f35f10..8cc73f2 100644 --- a/EleksTubeHAX_pio/src/StoredConfig.h +++ b/EleksTubeHAX_pio/src/StoredConfig.h @@ -33,6 +33,7 @@ class StoredConfig { uint8_t intensity; uint8_t pulse_bpm; uint8_t breath_per_min; + float rainbow_sec; uint8_t is_valid; // Write StoredConfig::valid here when valid data is loaded. } backlights; diff --git a/EleksTubeHAX_pio/src/TFTs.cpp b/EleksTubeHAX_pio/src/TFTs.cpp index 639eb06..d8a2a87 100644 --- a/EleksTubeHAX_pio/src/TFTs.cpp +++ b/EleksTubeHAX_pio/src/TFTs.cpp @@ -24,6 +24,7 @@ void TFTs::begin() { } NumberOfClockFaces = CountNumberOfClockFaces(); + loadClockFacesNames(); } void TFTs::reinit() { @@ -45,6 +46,23 @@ void TFTs::clear() { enableAllDisplays(); } +void TFTs::loadClockFacesNames() { + int8_t i = 0; + const char* filename = "/clockfaces.txt"; + Serial.println("Load clock face's names"); + fs::File f = SPIFFS.open(filename); + if(!f) { + Serial.println("SPIFFS clockfaces.txt not found."); + return; + } + while(f.available() && i<9) { + patterns_str[i] = f.readStringUntil('\n'); + Serial.println(patterns_str[i]); + i++; + } + f.close(); +} + void TFTs::showNoWifiStatus() { chip_select.setSecondsOnes(); setTextColor(TFT_RED, TFT_BLACK); @@ -480,4 +498,17 @@ uint32_t TFTs::read32(fs::File &f) { ((uint8_t *)&result)[3] = f.read(); // MSB return result; } + +String TFTs::clockFaceToName(uint8_t clockFace) { + return patterns_str[clockFace -1]; +} + +uint8_t TFTs::nameToClockFace(String name) { + for(int i=0; i<9; i++) { + if(patterns_str[i] == name) { + return i+1; + } + } + return 1; +} //// END STOLEN CODE diff --git a/EleksTubeHAX_pio/src/TFTs.h b/EleksTubeHAX_pio/src/TFTs.h index 59799f4..b7fdd55 100644 --- a/EleksTubeHAX_pio/src/TFTs.h +++ b/EleksTubeHAX_pio/src/TFTs.h @@ -52,6 +52,9 @@ class TFTs : public TFT_eSPI { void LoadNextImage(); void InvalidateImageInBuffer(); // force reload from Flash with new dimming settings + String clockFaceToName(uint8_t clockFace); + uint8_t nameToClockFace(String name); + private: uint8_t digits[NUM_DIGITS]; bool enabled; @@ -65,7 +68,10 @@ class TFTs : public TFT_eSPI { static uint16_t UnpackedImageBuffer[TFT_HEIGHT][TFT_WIDTH]; uint8_t FileInBuffer=255; // invalid, always load first image - uint8_t NextFileRequired = 0; + uint8_t NextFileRequired = 0; + + String patterns_str[9] = {"1", "2", "3", "4", "5", "6", "7", "8", "9"}; + void loadClockFacesNames(); }; extern TFTs tfts; diff --git a/EleksTubeHAX_pio/src/_USER_DEFINES - empty.h b/EleksTubeHAX_pio/src/_USER_DEFINES - empty.h index 7890c38..7145f9a 100644 --- a/EleksTubeHAX_pio/src/_USER_DEFINES - empty.h +++ b/EleksTubeHAX_pio/src/_USER_DEFINES - empty.h @@ -26,6 +26,7 @@ // ************* Display Dimming / Night time operation ************* +#define DIMMING // uncomment to enable hardware dimming #define NIGHT_TIME 22 // dim displays at 10 pm #define DAY_TIME 7 // full brightness after 7 am #define BACKLIGHT_DIMMED_INTENSITY 1 // 0..7 @@ -48,12 +49,18 @@ // ************* MQTT config ************* //#define MQTT_ENABLED // enable after creating an account, setting up the Thermostat device on www.smartnest.cz and filling in all the data below: +//#define MQTT_HOME_ASSISTANT // enable if you want Home Assistant support +//#define MQTT_HOME_ASSISTANT_DISCOVERY +//#define MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MANUFACTURER "EleksMaker" +//#define MQTT_HOME_ASSISTANT_DISCOVERY_DEVICE_MODEL "Elekstube IPS PR2" +//#define MQTT_HOME_ASSISTANT_DISCOVERY_SW_VERSION "0.8 Home Assistant Edition" +//#define MQTT_HOME_ASSISTANT_DISCOVERY_HW_VERSION "2.3.04" #define MQTT_BROKER "smartnest.cz" // Broker host #define MQTT_PORT 1883 // Broker port #define MQTT_USERNAME "__enter_your_username_here__" // Username from Smartnest #define MQTT_PASSWORD "__enter_your_api_key_here__" // Password from Smartnest or API key (under MY Account) #define MQTT_CLIENT "__enter_your_device_id_here__" // Device Id from Smartnest - +#define MQTT_SAVE_PREFERENCES_AFTER_SEC 60 // ************* Optional temperature sensor ************* //#define ONE_WIRE_BUS_PIN 4 // DS18B20 connected to GPIO4; comment this line if sensor is not connected diff --git a/EleksTubeHAX_pio/src/main.cpp b/EleksTubeHAX_pio/src/main.cpp index 40c594c..d942c1e 100644 --- a/EleksTubeHAX_pio/src/main.cpp +++ b/EleksTubeHAX_pio/src/main.cpp @@ -46,6 +46,8 @@ uint8_t hour_old = 255; bool DstNeedsUpdate = false; uint8_t yesterday = 0; +uint32_t lastMqttCommandExecuted = (uint32_t) -1; + // Helper function, defined below. void updateClockDisplay(TFTs::show_t show=TFTs::yes); void setupMenu(void); @@ -146,10 +148,28 @@ void loop() { uint32_t millis_at_top = millis(); // Do all the maintenance work WifiReconnect(); // if not connected attempt to reconnect - - MqttStatusPower = tfts.isEnabled(); - MqttStatusState = (uclock.getActiveGraphicIdx()+1) * 5; // 10 + MqttLoopFrequently(); + + bool MqttCommandReceived = + MqttCommandPowerReceived || + MqttCommandMainPowerReceived || + MqttCommandBackPowerReceived || + MqttCommandStateReceived || + MqttCommandBrightnessReceived || + MqttCommandMainBrightnessReceived || + MqttCommandBackBrightnessReceived || + MqttCommandPatternReceived || + MqttCommandBackPatternReceived || + MqttCommandBackColorPhaseReceived || + MqttCommandGraphicReceived || + MqttCommandMainGraphicReceived || + MqttCommandUseTwelveHoursReceived || + MqttCommandBlankZeroHoursReceived || + MqttCommandPulseBpmReceived || + MqttCommandBreathBpmReceived || + MqttCommandRainbowSecReceived; + if (MqttCommandPowerReceived) { MqttCommandPowerReceived = false; if (MqttCommandPower) { @@ -167,6 +187,30 @@ void loop() { } } + if (MqttCommandMainPowerReceived) { + MqttCommandMainPowerReceived = false; + if (MqttCommandMainPower) { + #ifndef HARDWARE_SI_HAI_CLOCK + if (!tfts.isEnabled()) { + tfts.reinit(); // reinit (original EleksTube HW: after a few hours in OFF state the displays do not wake up properly) + updateClockDisplay(TFTs::force); + } + #endif + tfts.enableAllDisplays(); + } else { + tfts.disableAllDisplays(); + } + } + + if (MqttCommandBackPowerReceived) { + MqttCommandBackPowerReceived = false; + if (MqttCommandBackPower) { + backlights.PowerOn(); + } else { + backlights.PowerOff(); + } + } + if (MqttCommandStateReceived) { MqttCommandStateReceived = false; randomSeed(millis()); @@ -181,11 +225,128 @@ void loop() { uclock.setClockGraphicsIdx(idx); tfts.current_graphic = uclock.getActiveGraphicIdx(); updateClockDisplay(TFTs::force); // redraw everything - /* do not save to flash everytime mqtt changes; can be frequent - Serial.print("Saving config..."); - stored_config.save(); - Serial.println(" Done."); - */ + } + + if(MqttCommandMainBrightnessReceived) { + MqttCommandMainBrightnessReceived = false; + tfts.dimming = MqttCommandMainBrightness; + tfts.InvalidateImageInBuffer(); + updateClockDisplay(TFTs::force); + } + + if(MqttCommandBackBrightnessReceived) { + MqttCommandBackBrightnessReceived = false; + backlights.setIntensity(uint8_t(MqttCommandBackBrightness)); + } + + if(MqttCommandPatternReceived) { + MqttCommandPatternReceived = false; + + for(int8_t i = 0; i < Backlights::num_patterns; i++){ + Serial.print("New pattern "); + Serial.print(MqttCommandPattern); + Serial.print(", check pattern "); + Serial.println(Backlights::patterns_str[i]); + if(strcmp(MqttCommandPattern, (Backlights::patterns_str[i]).c_str()) == 0) { + backlights.setPattern(Backlights::patterns(i)); + break; + } + } + } + + if(MqttCommandBackPatternReceived) { + MqttCommandBackPatternReceived = false; + for(int8_t i = 0; i < Backlights::num_patterns; i++){ + Serial.print("new pattern "); + Serial.print(MqttCommandBackPattern); + Serial.print(", check pattern "); + Serial.println(Backlights::patterns_str[i]); + if(strcmp(MqttCommandBackPattern, (Backlights::patterns_str[i]).c_str()) == 0) { + backlights.setPattern(Backlights::patterns(i)); + break; + } + } + } + + if(MqttCommandBackColorPhaseReceived) { + MqttCommandBackColorPhaseReceived = false; + + backlights.setColorPhase(MqttCommandBackColorPhase); + } + + if(MqttCommandGraphicReceived) { + MqttCommandGraphicReceived = false; + + uclock.setClockGraphicsIdx(MqttCommandGraphic); + tfts.current_graphic = uclock.getActiveGraphicIdx(); + updateClockDisplay(TFTs::force); // redraw everything + } + + if(MqttCommandMainGraphicReceived) { + MqttCommandMainGraphicReceived = false; + uclock.setClockGraphicsIdx(MqttCommandMainGraphic); + tfts.current_graphic = uclock.getActiveGraphicIdx(); + updateClockDisplay(TFTs::force); // redraw everything + } + + if(MqttCommandUseTwelveHoursReceived) { + MqttCommandUseTwelveHoursReceived = false; + uclock.setTwelveHour(MqttCommandUseTwelveHours); + } + + if(MqttCommandBlankZeroHoursReceived) { + MqttCommandBlankZeroHoursReceived = false; + uclock.setBlankHoursZero(MqttCommandBlankZeroHours); + } + + if(MqttCommandPulseBpmReceived) { + MqttCommandPulseBpmReceived = false; + backlights.setPulseRate(MqttCommandPulseBpm); + } + + if(MqttCommandBreathBpmReceived) { + MqttCommandBreathBpmReceived = false; + backlights.setBreathRate(MqttCommandBreathBpm); + } + + if(MqttCommandRainbowSecReceived) { + MqttCommandRainbowSecReceived = false; + backlights.setRainbowDuration(MqttCommandRainbowSec); + } + + MqttStatusPower = tfts.isEnabled(); + MqttStatusMainPower = tfts.isEnabled(); + MqttStatusBackPower = backlights.getPower(); + MqttStatusState = (uclock.getActiveGraphicIdx()+1) * 5; // 10 + MqttStatusBrightness = backlights.getIntensity(); + MqttStatusMainBrightness = tfts.dimming; + MqttStatusBackBrightness = backlights.getIntensity(); + strcpy(MqttStatusPattern, backlights.getPatternStr().c_str()); + strcpy(MqttStatusBackPattern, backlights.getPatternStr().c_str()); + backlights.getPatternStr().toCharArray(MqttStatusBackPattern, backlights.getPatternStr().length() + 1); + MqttStatusBackColorPhase = backlights.getColorPhase(); + MqttStatusGraphic = uclock.getActiveGraphicIdx(); + MqttStatusMainGraphic = uclock.getActiveGraphicIdx(); + MqttStatusUseTwelveHours = uclock.getTwelveHour(); + MqttStatusBlankZeroHours = uclock.getBlankHoursZero(); + MqttStatusPulseBpm = backlights.getPulseRate(); + MqttStatusBreathBpm = backlights.getBreathRate(); + MqttStatusRainbowSec = backlights.getRainbowDuration(); + + if(MqttCommandReceived) { + lastMqttCommandExecuted = millis(); + + MqttReportBackEverything(true); + } + + if(lastMqttCommandExecuted != -1) { + if (((millis() - lastMqttCommandExecuted) > (MQTT_SAVE_PREFERENCES_AFTER_SEC * 1000)) && menu.getState() == Menu::idle) { + lastMqttCommandExecuted = -1; + + Serial.print("Saving config..."); + stored_config.save(); + Serial.println(" Done."); + } } buttons.loop(); @@ -512,11 +673,12 @@ bool isNightTime(uint8_t current_hour) { void EveryFullHour(bool loopUpdate) { // dim the clock at night + #ifdef DIMMING uint8_t current_hour = uclock.getHour24(); FullHour = current_hour != hour_old; if (FullHour) { - Serial.print("current hour = "); - Serial.println(current_hour); + Serial.print("current hour = "); + Serial.println(current_hour); if (isNightTime(current_hour)) { Serial.println("Setting night mode (dimmed)"); tfts.dimming = TFT_DIMMED_INTENSITY; @@ -535,7 +697,8 @@ void EveryFullHour(bool loopUpdate) { } } hour_old = current_hour; - } + } + #endif } void UpdateDstEveryNight() { diff --git a/README.md b/README.md index 5cffecf..c0cac7d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # EleksTubeHAX - An aftermarket custom firmware for the desk clock ![EleksTube IPS clock](/Photos/Clock1.jpg) +### This is Home Assistant Edition with extensive MQTT support, see below + Supported hardware models: ### "EleksTube IPS clock", "SI HAI IPS clock", "NovelLife SE clock", "PunkCyber clock", "RGB Glow Tube DIY clock" @@ -34,6 +36,15 @@ Firmware supports and was tested on different clock versions. Note that "EleksTu - Supported hardware: original "EleksTube IPS clock"; "SI HAI clock" (chinese cknockoff); NovelLife SE clock (without gesture sensor); "PunkCyber" or "RGB Glow Tube DIY" clock (from pcbway). NOTE: EleksTube IPS Gen 2 was not tested. If someone owns it, please test this firmware and contact us (best to open Issue on GitHub and report back) - (in works: ) Integrated web server to input configuration (or maybe load new clock faces) +# Home Assistant Edition with extensive MQTT features + +- Device detected as two different lights, main and back +- Supported on/off, brightness, effects (main and back), color (back) +- Main light clock faces may be named, see clockfaces.txt on SPIFFS +- Supported 12/24 hours and blank zeroes settings switches +- Supported effect's speed change for backlight +- All options are discoverable via Home Assistant MQTT + # How to use this firmware If you just want to use new firmware without setting up all the tools and libraries and everything, navigate to folder `\pre-built-firmware\` and modify `_ESP32 write flash.cmd` to upload selected version to your clock. If you want more features, continue reading below.