Browse Source

lights: more color refactoring

- clamp values via helper functions
- unify channel access as long
  slightly more ram, but we no longer cast values that are 32bit-wide anyway
- remove css option, send unconditionally as /hex
- reimplement hsv conversion functions (based on HA / python's colorsys)
  simplify calculations and allow to return standalone value structs for
  RGB and HSV
- named channel accessors
- implement HA color inputs / outputs
pull/2429/head
Maxim Prokhorov 3 years ago
parent
commit
1a5f95c02e
4 changed files with 763 additions and 232 deletions
  1. +0
    -4
      code/espurna/config/general.h
  2. +88
    -29
      code/espurna/homeassistant.cpp
  3. +559
    -197
      code/espurna/light.cpp
  4. +116
    -2
      code/espurna/light.h

+ 0
- 4
code/espurna/config/general.h View File

@ -1319,10 +1319,6 @@
#define LIGHT_USE_GAMMA 0 // Use gamma correction for color channels
#endif
#ifndef LIGHT_USE_CSS
#define LIGHT_USE_CSS 1 // Use CSS style to report colors (1=> "#FF0000", 0=> "255,0,0")
#endif
#ifndef LIGHT_USE_RGB
#define LIGHT_USE_RGB 0 // Use RGB color selector (1=> RGB, 0=> HSV)
#endif


+ 88
- 29
code/espurna/homeassistant.cpp View File

@ -171,7 +171,7 @@ public:
if (_json) {
return _json->size();
}
return 0;
}
@ -438,21 +438,33 @@ public:
json["pl_avail"] = quote(mqttPayloadStatus(true));
json["pl_not_avail"] = quote(mqttPayloadStatus(false));
// ref. SUPPORT_... flags throughout the light component
// send `true` for every payload we support sending / receiving
// already enabled by default: "state", "transition"
// TODO: handle "rgb", "color_temp" and "white_value"
json["brightness"] = true;
// Note that since we send back the values immediately, HS mode sliders
// *will jump*, as calculations of input do not always match the output.
// (especially, when gamma table is used, as we modify the results)
// In case or RGB, channel values input is expected to match the output exactly.
if (lightHasColor()) {
json["rgb"] = true;
if (lightUseRGB()) {
json["rgb"] = true;
} else {
json["hs"] = true;
}
}
// Mired is only an input, we never send this value back
// (...besides the internally pinned value, ref. MQTT_TOPIC_MIRED. not used here though)
// - in RGB mode, we convert the temperature into a specific color
// - in CCT mode, white channels are used
if (lightHasColor() || lightUseCCT()) {
json["max_mireds"] = LIGHT_WARMWHITE_MIRED;
json["min_mireds"] = LIGHT_COLDWHITE_MIRED;
auto range = lightMiredsRange();
json["min_mirs"] = range.cold();
json["max_mirs"] = range.warm();
json["color_temp"] = true;
}
@ -471,28 +483,55 @@ private:
String _message;
};
void publishLightJson() {
if (!mqttConnected()) {
return;
}
bool heartbeat(heartbeat::Mask mask) {
// TODO: mask json payload specifically?
// or, find a way to detach masking from the system setting / don't use heartbeat timer
if (mask & heartbeat::Report::Light) {
DynamicJsonBuffer buffer(512);
JsonObject& root = buffer.createObject();
auto state = lightState();
root["state"] = state ? "ON" : "OFF";
if (state) {
root["brightness"] = lightBrightness();
if (lightUseCCT()) {
root["white_value"] = lightColdWhite();
}
if (lightColor()) {
auto& color = root.createNestedObject("color");
if (lightUseRGB()) {
auto rgb = lightRgb();
color["r"] = rgb.red();
color["g"] = rgb.green();
color["b"] = rgb.blue();
} else {
auto hsv = lightHsv();
color["h"] = hsv.hue();
color["s"] = hsv.saturation();
}
}
}
DynamicJsonBuffer buffer(512);
JsonObject& root = buffer.createObject();
String message;
root.printTo(message);
root["state"] = lightState() ? "ON" : "OFF";
root["brightness"] = lightBrightness();
String topic = mqttTopic(MQTT_TOPIC_LIGHT_JSON, false);
mqttSendRaw(topic.c_str(), message.c_str(), false);
}
String message;
root.printTo(message);
return true;
}
String topic = mqttTopic(MQTT_TOPIC_LIGHT_JSON, false);
mqttSendRaw(topic.c_str(), message.c_str(), false);
void publishLightJson() {
heartbeat(static_cast<heartbeat::Mask>(heartbeat::Report::Light));
}
void receiveLightJson(char* payload) {
DynamicJsonBuffer buffer(1024);
JsonObject& root = buffer.parseObject(payload);
if (!root.success()) {
return;
}
@ -518,11 +557,31 @@ void receiveLightJson(char* payload) {
}
}
if (root.containsKey("color_temp")) {
lightMireds(root["color_temp"].as<long>());
}
if (root.containsKey("brightness")) {
lightBrightness(root["brightness"].as<long>());
}
// TODO: handle "rgb", "color_temp" and "white_value"
if (lightHasColor() && root.containsKey("color")) {
JsonObject& color = root["color"];
if (lightUseRGB()) {
lightRgb({
color["r"].as<long>(),
color["g"].as<long>(),
color["b"].as<long>()});
} else {
lightHs(
color["h"].as<long>(),
color["s"].as<long>());
}
}
if (lightUseCCT() && root.containsKey("white_value")) {
lightColdWhite(root["white_value"].as<long>());
}
lightUpdate({transition, lightTransitionStep()});
}
@ -705,7 +764,7 @@ public:
return false;
}
return false;
}
@ -781,9 +840,9 @@ void send(TaskPtr ptr, FlagPtr flag_ptr) {
#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT
// - async fails when disconneted and when it's buffers are filled, which should be resolved after $LATENCY
// and the time it takes for the lwip to process it. future versions use queue, but could still fail when low on RAM
// - lwmqtt will fail when disconnected (already checked above) and *will* disconnect in case publish fails. publish funciton will
// wait for the puback all by itself. not tested.
// - pubsub will fail when it can't buffer the payload *or* the underlying wificlient fails. also not tested.
// - lwmqtt will fail when disconnected (already checked above) and *will* disconnect in case publish fails.
// ::publish() will wait for the puback, so we don't have to do it ourselves. not tested.
// - pubsub will fail when it can't buffer the payload *or* the underlying WiFiClient calls fail. also not tested.
if (res) {
flag = false;
@ -859,9 +918,8 @@ void mqttCallback(unsigned int type, const char* topic, char* payload) {
if (MQTT_CONNECT_EVENT == type) {
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
::mqttSubscribe(MQTT_TOPIC_LIGHT_JSON);
schedule_function(publishLightJson);
#endif
schedule_function(publishDiscovery);
::schedule_function(publishDiscovery);
return;
}
@ -899,10 +957,10 @@ bool onKeyCheck(const char* key, JsonVariant& value) {
} // namespace web
} // namespace homeassistant
// This module does not implement .yaml generation, since we can't:
// This module no longer implements .yaml generation, since we can't:
// - use unique_id in the device config
// - have abbreviated keys
// - have mqtt return the correct status & command payloads when it is disabled
// - have mqtt reliably return the correct status & command payloads when it is disabled
// (yet? needs reworked configuration section or making functions read settings directly)
void haSetup() {
@ -915,6 +973,7 @@ void haSetup() {
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
lightSetReportListener(homeassistant::publishLightJson);
mqttHeartbeat(homeassistant::heartbeat);
#endif
mqttRegister(homeassistant::mqttCallback);


+ 559
- 197
code/espurna/light.cpp
File diff suppressed because it is too large
View File


+ 116
- 2
code/espurna/light.h View File

@ -10,6 +10,7 @@
#define MQTT_TOPIC_LIGHT_JSON "light_json"
#define MQTT_TOPIC_CHANNEL "channel"
#define MQTT_TOPIC_COLOR_RGB "rgb"
#define MQTT_TOPIC_COLOR_HEX "hex"
#define MQTT_TOPIC_COLOR_HSV "hsv"
#define MQTT_TOPIC_ANIM_MODE "anim_mode"
#define MQTT_TOPIC_ANIM_SPEED "anim_speed"
@ -68,6 +69,92 @@ constexpr int DefaultReport {
Report::Web | Report::Mqtt | Report::MqttGroup | Report::Broker
};
struct Hsv {
static constexpr long HueMin { 0 };
static constexpr long HueMax { 360 };
static constexpr long SaturationMin { 0 };
static constexpr long SaturationMax { 100 };
static constexpr long ValueMin { 0 };
static constexpr long ValueMax { 100 };
Hsv() = default;
Hsv(long hue, long saturation, long value) :
_hue(std::clamp(hue, HueMin, HueMax)),
_saturation(std::clamp(saturation, SaturationMin, SaturationMax)),
_value(std::clamp(value, ValueMin, ValueMax))
{}
long hue() const {
return _hue;
}
long saturation() const {
return _saturation;
}
long value() const {
return _value;
}
private:
long _hue { HueMin };
long _saturation { SaturationMin };
long _value { ValueMin };
};
struct Rgb {
static constexpr long Min { 0 };
static constexpr long Max { 255 };
Rgb() = default;
Rgb(long red, long green, long blue) :
_red(std::clamp(red, Min, Max)),
_green(std::clamp(green, Min, Max)),
_blue(std::clamp(blue, Min, Max))
{}
long red() const {
return _red;
}
long green() const {
return _green;
}
long blue() const {
return _blue;
}
unsigned long asUlong() const;
private:
long _red { Min };
long _green { Min };
long _blue { Min };
};
struct MiredsRange {
constexpr MiredsRange() = default;
MiredsRange(long cold, long warm) :
_cold(cold),
_warm(warm)
{}
long cold() const {
return _cold;
}
long warm() const {
return _warm;
}
private:
long _cold { MiredsCold };
long _warm { MiredsWarm };
};
} // namespace Light
using LightStateListener = std::function<void(bool)>;
@ -98,16 +185,42 @@ void lightTransition(LightTransition transition);
void lightColor(const char* color, bool rgb);
void lightColor(const String& color, bool rgb);
void lightColor(const String& color);
void lightColor(const char* color);
void lightColor(const String& color);
void lightColor(unsigned long color);
String lightColor(bool rgb);
String lightRgbPayload();
String lightHsvPayload();
String lightColor();
bool lightSave();
void lightSave(bool save);
Light::Rgb lightRgb();
void lightRgb(Light::Rgb);
Light::Hsv lightHsv();
void lightHs(long hue, long saturation);
void lightHsv(Light::Hsv);
void lightMireds(long mireds);
Light::MiredsRange lightMiredsRange();
void lightRed(long value);
long lightRed();
void lightGreen(long value);
long lightGreen();
void lightBlue(long value);
long lightBlue();
void lightColdWhite(long value);
long lightColdWhite();
void lightWarmWhite(long value);
long lightWarmWhite();
void lightState(unsigned char i, bool state);
bool lightState(unsigned char i);
@ -130,6 +243,7 @@ void lightUpdate(bool save);
void lightUpdate();
bool lightHasColor();
bool lightUseRGB();
bool lightUseCCT();
void lightMQTT();


Loading…
Cancel
Save