diff --git a/cmake/installers.cmake b/cmake/installers.cmake index d8f734e1f..c56322a78 100644 --- a/cmake/installers.cmake +++ b/cmake/installers.cmake @@ -344,8 +344,7 @@ macro(DeployUnix TARGET) "libdl" "libexpat" "libfontconfig" - "libgcc_s" - "libgpg-error" + "libgcc_s" "libm" "libpthread" "librt" diff --git a/include/led-drivers/net/DriverNetZigbee2mqtt.h b/include/led-drivers/net/DriverNetZigbee2mqtt.h new file mode 100644 index 000000000..becbb0c2b --- /dev/null +++ b/include/led-drivers/net/DriverNetZigbee2mqtt.h @@ -0,0 +1,62 @@ +#pragma once + +#ifndef PCH_ENABLED + #include + #include + #include + #include + #include +#endif + +#include +#include + +class DriverNetZigbee2mqtt : public LedDevice +{ + Q_OBJECT + + struct Zigbee2mqttLamp; + + struct Zigbee2mqttInstance + { + int transition; + int constantBrightness; + + std::list lamps; + }; + + struct Zigbee2mqttLamp + { + enum Mode { RGB = 0, HSV }; + + QString name; + Mode colorModel; + }; + +public: + explicit DriverNetZigbee2mqtt(const QJsonObject& deviceConfig); + static LedDevice* construct(const QJsonObject& deviceConfig); + + QJsonObject discover(const QJsonObject& params) override; + + void identify(const QJsonObject& params) override; + +public slots: + void handlerSignalMqttReceived(QString topic, QString payload); + +protected: + bool powerOn() override; + bool powerOff() override; + +private: + bool init(const QJsonObject& deviceConfig) override; + int write(const std::vector& ledValues) override; + bool powerOnOff(bool isOn); + + Zigbee2mqttInstance _zigInstance; + std::atomic _discoveryFinished, _colorsFinished; + int _timeLogger; + QString _discoveryMessage; + + static bool isRegistered; +}; diff --git a/include/mqtt/mqtt.h b/include/mqtt/mqtt.h index 68c5aae74..ea441c336 100644 --- a/include/mqtt/mqtt.h +++ b/include/mqtt/mqtt.h @@ -29,6 +29,7 @@ public slots: void handleSettingsUpdate(settings::type type, const QJsonDocument& config); void handleSignalMqttSubscribe(bool subscribe, QString topic); + void handleSignalMqttPublish(QString topic, QString payload); private slots: void connected(); diff --git a/include/utils/GlobalSignals.h b/include/utils/GlobalSignals.h index 17fccdac1..7fd68bbfc 100644 --- a/include/utils/GlobalSignals.h +++ b/include/utils/GlobalSignals.h @@ -103,4 +103,6 @@ class GlobalSignals : public QObject void SignalMqttSubscribe(bool subscribe, QString topic); void SignalMqttReceived(QString topic, QString payload); + + void SignalMqttPublish(QString topic, QString payload); }; diff --git a/sources/led-drivers/LedDeviceSchemas.qrc b/sources/led-drivers/LedDeviceSchemas.qrc index 9b7fd2dc8..bf2a0de3f 100644 --- a/sources/led-drivers/LedDeviceSchemas.qrc +++ b/sources/led-drivers/LedDeviceSchemas.qrc @@ -39,5 +39,6 @@ schemas/schema-cololight.json schemas/schema-hyperspi.json schemas/schema-home_assistant.json + schemas/schema-zigbee2mqtt.json diff --git a/sources/led-drivers/net/DriverNetZigbee2mqtt.cpp b/sources/led-drivers/net/DriverNetZigbee2mqtt.cpp new file mode 100644 index 000000000..89236b3b1 --- /dev/null +++ b/sources/led-drivers/net/DriverNetZigbee2mqtt.cpp @@ -0,0 +1,324 @@ +#include +#include + +namespace +{ + constexpr auto ZIGBEE_DISCOVERY_MESSAGE = "zigbee2mqtt/bridge/devices"; + constexpr int DEFAULT_TIME_MEASURE_MESSAGE = 25; +} + +DriverNetZigbee2mqtt::DriverNetZigbee2mqtt(const QJsonObject& deviceConfig) + : LedDevice(deviceConfig), + _discoveryFinished(false), + _colorsFinished(false), + _timeLogger(0) +{ +} + +LedDevice* DriverNetZigbee2mqtt::construct(const QJsonObject& deviceConfig) +{ + return new DriverNetZigbee2mqtt(deviceConfig); +} + +bool DriverNetZigbee2mqtt::init(const QJsonObject& deviceConfig) +{ + bool isInitOK = false; + + if (LedDevice::init(deviceConfig)) + { + + _zigInstance.transition = deviceConfig["transition"].toInt(0); + _zigInstance.constantBrightness = deviceConfig["constantBrightness"].toInt(0); + + Debug(_log, "Transition (ms) : %s", (_zigInstance.transition > 0) ? QSTRING_CSTR(QString::number(_zigInstance.transition)) : "disabled" ); + Debug(_log, "ConstantBrightness : %s", (_zigInstance.constantBrightness > 0) ? QSTRING_CSTR(QString::number(_zigInstance.constantBrightness)) : "disabled"); + + auto arr = deviceConfig["lamps"].toArray(); + + for (const auto&& lamp : arr) + if (lamp.isObject()) + { + Zigbee2mqttLamp hl; + auto lampObj = lamp.toObject(); + hl.name = lampObj["name"].toString(); + hl.colorModel = static_cast(lampObj["colorModel"].toInt(0)); + Debug(_log, "Configured lamp (%s) : %s", (hl.colorModel == 0) ? "RGB" : "HSV", QSTRING_CSTR(hl.name)); + _zigInstance.lamps.push_back(hl); + } + + if (arr.size() > 0) + { + isInitOK = true; + } + } + return isInitOK; +} + + +bool DriverNetZigbee2mqtt::powerOnOff(bool isOn) +{ + QJsonDocument doc; + + for (const auto& lamp : _zigInstance.lamps) + { + QString topic = QString("zigbee2mqtt/%1/set").arg(lamp.name); + QJsonObject row; + + row["state"] = (isOn) ? "ON" : "OFF"; + + doc.setObject(row); + emit GlobalSignals::getInstance()->SignalMqttPublish(topic, doc.toJson(QJsonDocument::Compact)); + } + + if (_zigInstance.lamps.size() > 0) + { + QString topic = QString("zigbee2mqtt/%1").arg(_zigInstance.lamps.back().name); + if (isOn) + { + emit GlobalSignals::getInstance()->SignalMqttSubscribe(true, topic); + connect(GlobalSignals::getInstance(), &GlobalSignals::SignalMqttReceived, this, &DriverNetZigbee2mqtt::handlerSignalMqttReceived, Qt::DirectConnection); + } + else + { + emit GlobalSignals::getInstance()->SignalMqttSubscribe(false, topic); + disconnect(GlobalSignals::getInstance(), &GlobalSignals::SignalMqttReceived, this, &DriverNetZigbee2mqtt::handlerSignalMqttReceived); + } + } + + _timeLogger = 0; + + return true; +} + +bool DriverNetZigbee2mqtt::powerOn() +{ + return powerOnOff(true); +} + +bool DriverNetZigbee2mqtt::powerOff() +{ + return powerOnOff(false); +} + +int DriverNetZigbee2mqtt::write(const std::vector& ledValues) +{ + QJsonDocument doc; + + _colorsFinished = false; + + auto rgb = ledValues.begin(); + for (const auto& lamp : _zigInstance.lamps) + if (rgb != ledValues.end()) + { + QJsonObject row; + auto& color = *(rgb++); + int brightness = 0; + + QString topic = QString("zigbee2mqtt/%1/set").arg(lamp.name); + + if (_zigInstance.transition > 0) + { + row["transition"] = _zigInstance.transition / 1000.0; + } + + if (lamp.colorModel == Zigbee2mqttLamp::Mode::RGB) + { + QJsonObject rgb; rgb["r"] = color.red; rgb["g"] = color.green; rgb["b"] = color.blue; + row["color"] = rgb; + brightness = std::min(std::max(static_cast(std::roundl(0.2126 * color.red + 0.7152 * color.green + 0.0722 * color.blue)), 0), 255); + } + else + { + uint16_t h; + float s, v; + color.rgb2hsl(color.red, color.green, color.blue, h, s, v); + + QJsonObject hs; hs["hue"] = h; hs["saturation"] = static_cast(std::roundl(s * 100.0)); + row["color"] = hs; + brightness = std::min(std::max(static_cast(std::roundl(v * 255.0)), 0), 255); + } + + if (brightness > 0 && _zigInstance.constantBrightness > 0) + { + brightness = _zigInstance.constantBrightness; + } + + row["brightness"] = brightness; + + doc.setObject(row); + emit GlobalSignals::getInstance()->SignalMqttPublish(topic, doc.toJson(QJsonDocument::Compact)); + } + + int timeout = 0; + for (timeout = 0; timeout < 20 && !_colorsFinished; timeout++) + { + QThread::msleep(10); + } + + if (!_colorsFinished) + { + Warning(_log, "The communication timed out after 200ms (%i)", (++_timeLogger)); + } + else if (_timeLogger >= 0 && _timeLogger < DEFAULT_TIME_MEASURE_MESSAGE) + { + Info(_log, "The communication took: %ims (%i/%i)", timeout * 10, ++_timeLogger, DEFAULT_TIME_MEASURE_MESSAGE); + } + + return 0; +} + +void DriverNetZigbee2mqtt::handlerSignalMqttReceived(QString topic, QString payload) +{ + if (topic == ZIGBEE_DISCOVERY_MESSAGE && !_discoveryFinished) + { + _discoveryMessage = payload; + _discoveryFinished = true; + } + else + { + _colorsFinished = true; + } +} + +QJsonObject DriverNetZigbee2mqtt::discover(const QJsonObject& params) +{ + QJsonObject devicesDiscovered; + QJsonArray deviceList; + devicesDiscovered.insert("ledDeviceType", _activeDeviceType); + + _discoveryFinished = false; + _discoveryMessage = ""; + + connect(GlobalSignals::getInstance(), &GlobalSignals::SignalMqttReceived, this, &DriverNetZigbee2mqtt::handlerSignalMqttReceived, Qt::DirectConnection); + emit GlobalSignals::getInstance()->SignalMqttSubscribe(false, ZIGBEE_DISCOVERY_MESSAGE); + emit GlobalSignals::getInstance()->SignalMqttSubscribe(true, ZIGBEE_DISCOVERY_MESSAGE); + + for (int i = 0; i < 15 && !_discoveryFinished; i++) + { + QThread::msleep(100); + } + + disconnect(GlobalSignals::getInstance(), &GlobalSignals::SignalMqttReceived, this, &DriverNetZigbee2mqtt::handlerSignalMqttReceived); + emit GlobalSignals::getInstance()->SignalMqttSubscribe(false, ZIGBEE_DISCOVERY_MESSAGE); + + if (!_discoveryFinished) + { + Error(_log, "Could not find any Zigbee2mqtt devices. Check HyperHDR MQTT network / MQTT broker / Zigbee2mqtt configuration"); + } + else + { + QJsonDocument doc = QJsonDocument::fromJson(_discoveryMessage.toUtf8()); + + if (!doc.isNull()) + { + if (doc.isArray()) + { + for (const auto&& device : doc.array()) + if (device.isObject()) + { + auto item = device.toObject(); + if (!item["friendly_name"].toString().isEmpty() && + item.contains("definition") && item["definition"].isObject()) + { + auto defItem = item["definition"].toObject(); + if (defItem.contains("exposes") && defItem["exposes"].isArray()) + { + for (const auto&& exposesItem : defItem["exposes"].toArray()) + if (exposesItem.isObject()) + { + auto exposesObj = exposesItem.toObject(); + if (exposesObj.contains("type") && QString::compare(exposesObj["type"].toString(), "light", Qt::CaseInsensitive) == 0 && + exposesObj.contains("features") && exposesObj["features"].isArray()) + { + for (const auto&& featureItem : exposesObj["features"].toArray()) + if (featureItem.isObject()) + { + auto features = featureItem.toObject(); + if (features.contains("name")) + { + auto name = features["name"].toString(); + QString colorMode; + + if (QString::compare(name, "color_xy", Qt::CaseInsensitive) == 0) + { + colorMode = "RGB"; + } + else if (QString::compare(name, "color_hs", Qt::CaseInsensitive) == 0) + { + colorMode = "HSV"; + } + + if (!colorMode.isEmpty()) + { + QJsonObject newIp; + newIp["value"] = colorMode; + newIp["name"] = item["friendly_name"]; + deviceList.push_back(newIp); + break; + } + } + } + } + } + } + + } + } + } + else + { + Error(_log, "Document is not an array"); + } + } + else + { + Error(_log, "Document is not an JSON"); + } + } + + devicesDiscovered.insert("devices", deviceList); + Debug(_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + return devicesDiscovered; +} + +void DriverNetZigbee2mqtt::identify(const QJsonObject& params) +{ + if (params.contains("name") && params.contains("type")) + { + auto name = params["name"].toString(); + auto type = params["type"].toString(); + if (!name.isEmpty() && !type.isEmpty()) + { + Debug(_log, "Testing lamp %s (%s)", QSTRING_CSTR(name), QSTRING_CSTR(type)); + QString topic = QString("zigbee2mqtt/%1/set").arg(name); + QJsonDocument doc; + QJsonObject rowOn; rowOn["state"] = "ON"; + QJsonObject rowOff; rowOff["state"] = "OFF"; + QJsonObject colorRGB, rgb; colorRGB["brightness"] = 255; rgb["r"] = 255; rgb["g"] = 0; rgb["b"] = 0; colorRGB["color"] = rgb; + QJsonObject colorHS, hs; colorHS["brightness"] = 255; hs["hue"] = 360; hs["saturation"] = 100; colorHS["color"] = hs; + + + doc.setObject(rowOn); + emit GlobalSignals::getInstance()->SignalMqttPublish(topic, doc.toJson(QJsonDocument::Compact)); + QThread::msleep(300); + + if (type == "RGB") + { + doc.setObject(colorRGB); + emit GlobalSignals::getInstance()->SignalMqttPublish(topic, doc.toJson(QJsonDocument::Compact)); + } + else + { + doc.setObject(colorHS); + emit GlobalSignals::getInstance()->SignalMqttPublish(topic, doc.toJson(QJsonDocument::Compact)); + } + QThread::msleep(700); + + doc.setObject(rowOff); + emit GlobalSignals::getInstance()->SignalMqttPublish(topic, doc.toJson(QJsonDocument::Compact)); + } + } +} + +bool DriverNetZigbee2mqtt::isRegistered = hyperhdr::leds::REGISTER_LED_DEVICE("zigbee2mqtt", "leds_group_2_network", DriverNetZigbee2mqtt::construct); diff --git a/sources/led-drivers/schemas/schema-zigbee2mqtt.json b/sources/led-drivers/schemas/schema-zigbee2mqtt.json new file mode 100644 index 000000000..9841edfe1 --- /dev/null +++ b/sources/led-drivers/schemas/schema-zigbee2mqtt.json @@ -0,0 +1,66 @@ +{ + "type":"object", + "required":true, + "properties":{ + "transition": { + "type": "integer", + "title": "edt_dev_spec_transistionTime_title", + "default": 0, + "append": "ms", + "minimum": 0, + "maximum": 3000, + "required": true, + "propertyOrder": 3 + }, + "constantBrightness": { + "type": "integer", + "title": "edt_dev_spec_constantBrightness_title", + "default": 0, + "minimum": 0, + "maximum": 255, + "required": true, + "propertyOrder": 4 + }, + "lamps": { + "type": "array", + "title":"edt_dev_spec_lights_title", + "propertyOrder" : 7, + "uniqueItems" : true, + "items" : { + "type" : "object", + "title" : "edt_dev_spec_lights_itemtitle", + "required" : [ + "name", "colorModel" + ], + "properties" : + { + "name" : + { + "type": "string", + "title" : "edt_dev_spec_lights_name", + "propertyOrder" : 1 + }, + "colorModel": { + "type": "integer", + "title":"edt_conf_bb_mode_title", + "enum" : [0, 1], + "default" : 0, + "options" : { + "enum_titles" : ["edt_conf_enum_rgb", "edt_conf_enum_hsv"] + }, + "propertyOrder" : 2 + }, + "defaultPosition" : + { + "type": "string", + "options": { + "hidden": true + }, + "propertyOrder" : 3 + } + } + } + } + }, + "additionalProperties": true +} diff --git a/sources/mqtt/mqtt.cpp b/sources/mqtt/mqtt.cpp index 96e5c35cc..b6f8cc29e 100644 --- a/sources/mqtt/mqtt.cpp +++ b/sources/mqtt/mqtt.cpp @@ -50,7 +50,15 @@ void mqtt::start(QString host, int port, QString username, QString password, boo Debug(_log, "Starting the MQTT connection. Address: %s:%i. Protocol: %s. Authentication: %s, Ignore errors: %s", QSTRING_CSTR(host), port, (is_ssl) ? "SSL" : "NO SSL", (!username.isEmpty() || !password.isEmpty()) ? "YES" : "NO", (ignore_ssl_errors) ? "YES" : "NO"); - Debug(_log, "MQTT topic: %s, MQTT response: %s", QSTRING_CSTR(HYPERHDRAPI), QSTRING_CSTR(HYPERHDRAPI_RESPONSE)); + + if (!_disableApiAccess) + { + Debug(_log, "MQTT topic: %s, MQTT response: %s", QSTRING_CSTR(HYPERHDRAPI), QSTRING_CSTR(HYPERHDRAPI_RESPONSE)); + } + else + { + Debug(_log, "MQTT access to HyperHDR API is disabled by user"); + } QHostAddress address(host); @@ -109,13 +117,17 @@ void mqtt::stop() void mqtt::disconnected() { Debug(_log, "Disconnected"); + + disconnect(GlobalSignals::getInstance(), &GlobalSignals::SignalMqttSubscribe, this, &mqtt::handleSignalMqttSubscribe); + disconnect(GlobalSignals::getInstance(), &GlobalSignals::SignalMqttPublish, this, &mqtt::handleSignalMqttPublish); } void mqtt::connected() { Debug(_log, "Connected"); - connect(GlobalSignals::getInstance(), &GlobalSignals::SignalMqttSubscribe, this, &mqtt::handleSignalMqttSubscribe); + connect(GlobalSignals::getInstance(), &GlobalSignals::SignalMqttSubscribe, this, &mqtt::handleSignalMqttSubscribe, Qt::UniqueConnection); + connect(GlobalSignals::getInstance(), &GlobalSignals::SignalMqttPublish, this, &mqtt::handleSignalMqttPublish, Qt::UniqueConnection); if (_retryTimer != nullptr) { @@ -329,3 +341,16 @@ void mqtt::received(const QMQTT::Message& message) emit GlobalSignals::getInstance()->SignalMqttReceived(topic, payload); } } + +void mqtt::handleSignalMqttPublish(QString topic, QString payload) +{ + if (_clientInstance != nullptr) + { + QMQTT::Message message; + message.setTopic(topic); + message.setQos(0); + message.setPayload(payload.toUtf8()); + + _clientInstance->publish(message); + } +} diff --git a/www/i18n/en.json b/www/i18n/en.json index debf06761..37eadcbc0 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -1271,5 +1271,7 @@ "edt_conf_reorder_displays_title": "Reorder displays", "edt_conf_reorder_displays_expl": "Manipulate the order (permutations) of the displays until you get the correct one in multi-monitor mode", "edt_conf_mqtt_disableApiAccess_title" : "Disable API access", - "edt_conf_mqtt_disableApiAccess_expl" : "Block execution of API commands sent via MQTT" + "edt_conf_mqtt_disableApiAccess_expl" : "Block execution of API commands sent via MQTT", + "wiz_zigbee2mqtt_title": "Zigbee2mqtt lights wizard", + "wiz_mqtt_error": "Could not find devices connected to MQTT server. Check HyperHDR logs and MQTT client configuration in network settings. HyperHDR must have a valid and active connection to MQTT broker." } diff --git a/www/js/light_source.js b/www/js/light_source.js index 92e94a916..99b1e5f64 100644 --- a/www/js/light_source.js +++ b/www/js/light_source.js @@ -868,7 +868,7 @@ $(document).ready(function() }); $("input[name='root[specificOptions][useEntertainmentAPI]']").trigger("change"); } - else if ([ "cololight", "yeelight", "atmoorb", "home_assistant"].includes(ledType)) + else if ([ "cololight", "yeelight", "atmoorb", "home_assistant", "zigbee2mqtt"].includes(ledType)) { const data = { type: ledType diff --git a/www/js/wizard.js b/www/js/wizard.js index 8f985f13c..aac7f1a64 100644 --- a/www/js/wizard.js +++ b/www/js/wizard.js @@ -2496,6 +2496,7 @@ function startWizardHome_assistant(e) haConfig.homeAssistantHost = $('#hostHA').val().trim(); haConfig.longLivedAccessToken= `${$('#tokenHA').val().trim()}`; haConfig.transition = conf_editor.getEditor("root.specificOptions.transition").getValue(); + haConfig.constantBrightness = conf_editor.getEditor("root.specificOptions.constantBrightness").getValue(); haConfig.restoreOriginalState = conf_editor.getEditor("root.specificOptions.restoreOriginalState").getValue(); haConfig.maxRetry = conf_editor.getEditor("root.specificOptions.maxRetry").getValue(); haConfig.lamps = []; @@ -2635,10 +2636,18 @@ function createHaLedConfig(haConfig) options += ``; } let selectLightControl = ``; - let ipVal = encodeURI(haConfig.homeAssistantHost); - let tokenVal = haConfig.longLivedAccessToken; - let buttonLightLink = ``; - $('.ha_lamps_rows').append(createTableRowFlex([lamp.name, selectLightControl, buttonLightLink])); + if (haConfig.type == 'home_assistant') + { + let ipVal = encodeURI(haConfig.homeAssistantHost); + let tokenVal = haConfig.longLivedAccessToken; + let buttonLightLink = ``; + $('.ha_lamps_rows').append(createTableRowFlex([lamp.name, selectLightControl, buttonLightLink])); + } + else + { + let buttonLightLink = ``; + $('.ha_lamps_rows').append(createTableRowFlex([lamp.name, selectLightControl, buttonLightLink])); + } ledHaIndex++; }); @@ -2646,3 +2655,57 @@ function createHaLedConfig(haConfig) $("#wizard_modal").addClass("modal-lg"); $('#wizp2').toggle(true); } + +//**************************** +// Wizard zigbee2mqtt +//**************************** + +function startWizardZigbee2mqtt(e) +{ + + requestLedDeviceDiscovery('zigbee2mqtt').then( (result) => { + + let zigbeeConfig = {}; + + zigbeeConfig.type = 'zigbee2mqtt'; + zigbeeConfig.colorOrder = conf_editor.getEditor("root.generalOptions.colorOrder").getValue(); + zigbeeConfig.transition = conf_editor.getEditor("root.specificOptions.transition").getValue(); + zigbeeConfig.constantBrightness = conf_editor.getEditor("root.specificOptions.constantBrightness").getValue(); + zigbeeConfig.lamps = []; + + let discovered = result; + if (discovered != null && discovered.success === true && discovered.info != null && discovered.info.devices != null && + Array.isArray(discovered.info.devices) && discovered.info.devices.length > 0) + { + discovered.info.devices.forEach(dev => { + let lamp = {}; + lamp.name = dev.name; + lamp.colorModel = dev.value; + zigbeeConfig.lamps.push(lamp); + }); + + if (window.serverConfig.device != null && window.serverConfig.device.type == zigbeeConfig.type && + Array.isArray(window.serverConfig.device.lamps) && window.serverConfig.device.lamps.length == zigbeeConfig.lamps.length) + { + for (var key in zigbeeConfig.lamps) + if (zigbeeConfig.lamps[key].name = window.serverConfig.device.lamps[key].name) + { + zigbeeConfig.lamps[key].defaultPosition = window.serverConfig.device.lamps[key].defaultPosition; + } + } + + createHaLedConfig(zigbeeConfig); + + let zigForm = new bootstrap.Modal($("#wizard_modal"), { + backdrop: "static", + keyboard: false + }); + zigForm.show(); + } + else + { + alert($.i18n('wiz_mqtt_error')); + } + + }); +}