Files
Frank 4b47714f68 Philips hue robustness improvements
* reduce JSON buffer from 1024 to 640
* ESP32: define buffer globally (reduces likelyhood of crashes due to low stack)
* added some sanity checks after string ops
* delete hueClient object after closing the connection

* make functions static if not used outside of hue.cpp, re-shuffle order where needed
* rename JSON buffer object: root -> jsonHUEroot
2026-01-28 22:12:24 +01:00

224 lines
5.7 KiB
C++

#include "wled.h"
/*
* Sync to Philips hue lights
*/
#ifndef WLED_DISABLE_HUESYNC
// WLEDMM moved out of wled.h
static AsyncClient* hueClient = nullptr;
#ifdef ARDUINO_ARCH_ESP32
StaticJsonDocument<640> jsonHUEroot; // (was 1024) - 640 bytes should be more than enough
#endif
void handleHue()
{
if (hueReceived)
{
colorUpdated(CALL_MODE_HUE); hueReceived = false;
if (hueStoreAllowed && hueNewKey)
{
serializeConfigSec(); //save api key
hueStoreAllowed = false;
hueNewKey = false;
}
}
if (!WLED_CONNECTED || hueClient == nullptr || millis() - hueLastRequestSent < huePollIntervalMs) return;
hueLastRequestSent = millis();
if (huePollingEnabled)
{
reconnectHue();
} else {
hueClient->close();
delete hueClient; hueClient = nullptr;
if (hueError == HUE_ERROR_ACTIVE) hueError = HUE_ERROR_INACTIVE;
}
}
static void onHueError(void* arg, AsyncClient* client, int8_t error)
{
USER_PRINTLN("Hue err " + String(error));
hueError = HUE_ERROR_TIMEOUT;
}
static void sendHuePoll()
{
if (hueClient == nullptr || !hueClient->connected()) return;
String req = "";
if (hueAuthRequired)
{
req += F("POST /api HTTP/1.1\r\nHost: ");
req += hueIP.toString();
req += F("\r\nContent-Length: 25\r\n\r\n{\"devicetype\":\"wled#esp\"}");
} else
{
req += F("GET /api/");
req += hueApiKey;
req += F("/lights/");
req += String(huePollLightId);
req += F(" HTTP/1.1\r\nHost: ");
req += hueIP.toString();
req += "\r\n\r\n";
}
hueClient->add(req.c_str(), req.length());
hueClient->send();
hueLastRequestSent = millis();
}
static void onHueConnect(void* arg, AsyncClient* client)
{
DEBUG_PRINTLN(F("Hue connect"));
sendHuePoll();
}
static void onHueData(void* arg, AsyncClient* client, void *data, size_t len)
{
if (!len) return;
char* str = (char*)data;
DEBUG_PRINTLN(hueApiKey);
DEBUG_PRINTLN(str);
//only get response body
str = strstr(str,"\r\n\r\n");
if ((str == nullptr) || strlen(str) < 4) return;
str += 4;
#if !defined(ARDUINO_ARCH_ESP32)
StaticJsonDocument<640> jsonHUEroot; // was 1024 - completely ovrersized.
#endif
if (str[0] == '[') //is JSON array
{
auto error = deserializeJson(jsonHUEroot, str);
if (error)
{
USER_PRINTF("Hue error: failed to parse (len =%d) \t: %s\n", strlen(str), str);
hueError = HUE_ERROR_JSON_PARSING; return;
}
int hueErrorCode = jsonHUEroot[0][F("error")]["type"];
if (hueErrorCode)//hue bridge returned error
{
hueError = hueErrorCode;
switch (hueErrorCode)
{
case 1: hueAuthRequired = true; break; //Unauthorized user
case 3: huePollingEnabled = false; break; //Invalid light ID
case 101: hueAuthRequired = true; break; //link button not presset
}
return;
}
if (hueAuthRequired)
{
const char* apikey = jsonHUEroot[0][F("success")][F("username")];
if (apikey != nullptr && strlen(apikey) < sizeof(hueApiKey))
{
strlcpy(hueApiKey, apikey, sizeof(hueApiKey));
hueAuthRequired = false;
hueNewKey = true;
}
}
return;
}
//else, assume it is JSON object, look for state and only parse that
str = strstr(str,"state");
if (str == nullptr) return;
str = strstr(str,"{");
if (str == nullptr) return;
auto error = deserializeJson(jsonHUEroot, str);
if (error)
{
USER_PRINTF("Hue error: failed to parse (len =%d) \t: %s\n", strlen(str), str);
hueError = HUE_ERROR_JSON_PARSING; return;
}
float hueX=0, hueY=0;
uint16_t hueHue=0, hueCt=0;
byte hueBri=0, hueSat=0, hueColormode=0;
if (jsonHUEroot["on"]) {
if (jsonHUEroot.containsKey("bri")) //Dimmable device
{
hueBri = jsonHUEroot["bri"];
hueBri++;
const char* cm =jsonHUEroot[F("colormode")];
if (cm != nullptr) //Color device
{
if (strstr(cm,("ct")) != nullptr) //ct mode
{
hueCt = jsonHUEroot["ct"];
hueColormode = 3;
} else if (strstr(cm,"xy") != nullptr) //xy mode
{
hueX = jsonHUEroot["xy"][0]; // 0.5051
hueY = jsonHUEroot["xy"][1]; // 0.4151
hueColormode = 1;
} else //hs mode
{
hueHue = jsonHUEroot["hue"];
hueSat = jsonHUEroot[F("sat")];
hueColormode = 2;
}
}
} else //On/Off device
{
hueBri = briLast;
}
} else
{
hueBri = 0;
}
hueError = HUE_ERROR_ACTIVE;
//apply vals
if (hueBri != hueBriLast)
{
if (hueApplyOnOff)
{
if (hueBri==0) {bri = 0;}
else if (bri==0 && hueBri>0) bri = briLast;
}
if (hueApplyBri)
{
if (hueBri>0) bri = hueBri;
}
hueBriLast = hueBri;
}
if (hueApplyColor)
{
switch(hueColormode)
{
case 1: if (hueX != hueXLast || hueY != hueYLast) colorXYtoRGB(hueX,hueY,col); hueXLast = hueX; hueYLast = hueY; break;
case 2: if (hueHue != hueHueLast || hueSat != hueSatLast) colorHStoRGB(hueHue,hueSat,col); hueHueLast = hueHue; hueSatLast = hueSat; break;
case 3: if (hueCt != hueCtLast) colorCTtoRGB(hueCt,col); hueCtLast = hueCt; break;
}
}
hueReceived = true;
}
void reconnectHue() // this is also called from async_tcp task handleSettingsSet()
{
if (!WLED_CONNECTED || !huePollingEnabled) return;
DEBUG_PRINTLN(F("Hue reconnect"));
if (hueClient == nullptr) {
hueClient = new AsyncClient();
hueClient->onConnect(&onHueConnect, hueClient);
hueClient->onData(&onHueData, hueClient);
hueClient->onError(&onHueError, hueClient);
hueAuthRequired = (strlen(hueApiKey)<20);
}
hueClient->connect(hueIP, 80);
}
#else
void handleHue(){}
void reconnectHue(){}
#pragma message "Philips HUE bridge interface disabled"
#endif