From 41af5c1b9ec79cafa2d137ef3d2b3257a9081dd6 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Fri, 19 Mar 2021 22:52:04 +0100 Subject: [PATCH] LEDDevices - WLED enhancements and minor fixes (#1204) * Yeelight minor updates * Add Timeout to REST API * LEDDevice - Correct storeState * Add WaitTime function * Always show HW-LEDCount for configuration * WLED - New features ("live" support, storing state and identification) * Yeelight - Refactoring * Cololight - Refactoring * Karate - getProperties Support * Atmo - getProperties Support * AtmoOrb - refactoring * Nanoleaf - Refactoring, New "Shapes" considerations * PhilipHue - Minor corrections * Update Changelog --- CHANGELOG.md | 9 + include/utils/WaitTime.h | 27 +++ libsrc/hyperion/schema/schema-device.json | 7 +- libsrc/leddevice/LedDevice.cpp | 11 +- libsrc/leddevice/dev_net/LedDeviceAtmoOrb.cpp | 10 +- .../leddevice/dev_net/LedDeviceCololight.cpp | 87 ++++---- .../leddevice/dev_net/LedDeviceNanoleaf.cpp | 164 +++++++++------ libsrc/leddevice/dev_net/LedDeviceNanoleaf.h | 22 +- .../leddevice/dev_net/LedDevicePhilipsHue.cpp | 27 +-- libsrc/leddevice/dev_net/LedDeviceWled.cpp | 191 ++++++++++++------ libsrc/leddevice/dev_net/LedDeviceWled.h | 29 ++- .../leddevice/dev_net/LedDeviceYeelight.cpp | 64 +++--- libsrc/leddevice/dev_net/LedDeviceYeelight.h | 8 + libsrc/leddevice/dev_net/ProviderRestApi.cpp | 58 ++++-- libsrc/leddevice/dev_net/ProviderRestApi.h | 44 +++- libsrc/leddevice/dev_serial/LedDeviceAtmo.cpp | 21 ++ libsrc/leddevice/dev_serial/LedDeviceAtmo.h | 8 + .../leddevice/dev_serial/LedDeviceKarate.cpp | 57 ++++-- libsrc/leddevice/dev_serial/LedDeviceKarate.h | 8 + libsrc/leddevice/schemas/schema-wled.json | 14 +- 20 files changed, 609 insertions(+), 257 deletions(-) create mode 100644 include/utils/WaitTime.h diff --git a/CHANGELOG.md b/CHANGELOG.md index def25e60..8b9137a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- WLED: Support of ["live" property] (https://github.com/Aircoookie/WLED/issues/1308), addresses #1095 +- WLED: Support storing/restoring state, fixes #1101 +- LED-Devices: Allow to get properties for Atmo and Karatedevices to limit LED numbers configurable +- LED-Devices: Add timeouts for REST-API calls + ### Changed - Updated dependency rpi_ws281x to latest upstream - Fix High CPU load (RPI3B+) (#1013) +- Nanoleaf: Consider Nanoleaf-Shape Controlers +- LED-Devices: Show HW-Ledcount in all setting levels - Documentation: Add link to [Hyperion-py](https://github.com/dermotduffy/hyperion-py) @@ -20,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix issue #1127: LED-Devices: Correct total packet count in tpm2net implementation - LED-Hue: Proper black in Entertainement mode if min brightness is set +- LED-Hue: Minor fix of setColor command +- Nanoleaf: Fix,if external control mode cannot be set ### Removed diff --git a/include/utils/WaitTime.h b/include/utils/WaitTime.h new file mode 100644 index 00000000..0521ed5b --- /dev/null +++ b/include/utils/WaitTime.h @@ -0,0 +1,27 @@ +#ifndef WAITTIME_H +#define WAITTIME_H + +#include +#include + +#include + +inline void wait(std::chrono::milliseconds millisecondsWait) +{ + QEventLoop loop; + QTimer t; + t.connect(&t, &QTimer::timeout, &loop, &QEventLoop::quit); + t.start(millisecondsWait.count()); + loop.exec(); +} + +inline void wait(int millisecondsWait) +{ + QEventLoop loop; + QTimer t; + t.connect(&t, &QTimer::timeout, &loop, &QEventLoop::quit); + t.start(millisecondsWait); + loop.exec(); +} + +#endif // WAITTIME_H diff --git a/libsrc/hyperion/schema/schema-device.json b/libsrc/hyperion/schema/schema-device.json index 23deb34a..4c337200 100644 --- a/libsrc/hyperion/schema/schema-device.json +++ b/libsrc/hyperion/schema/schema-device.json @@ -16,7 +16,6 @@ "title" : "edt_dev_general_hardwareLedCount_title", "minimum" : 1, "default" : 1, - "access" : "expert", "propertyOrder" : 2 }, "colorOrder" : @@ -25,9 +24,11 @@ "title" : "edt_dev_general_colorOrder_title", "enum" : ["rgb", "bgr", "rbg", "brg", "gbr", "grb"], "default" : "rgb", - "options" : { - "enum_titles" : ["edt_conf_enum_rgb", "edt_conf_enum_bgr", "edt_conf_enum_rbg", "edt_conf_enum_brg", "edt_conf_enum_gbr", "edt_conf_enum_grb"] + "required" : true, + "options": { + "enum_titles": [ "edt_conf_enum_rgb", "edt_conf_enum_bgr", "edt_conf_enum_rbg", "edt_conf_enum_brg", "edt_conf_enum_gbr", "edt_conf_enum_grb" ] }, + "access" : "expert", "propertyOrder" : 3 } }, diff --git a/libsrc/leddevice/LedDevice.cpp b/libsrc/leddevice/LedDevice.cpp index 16a1d8da..2ab15bf0 100644 --- a/libsrc/leddevice/LedDevice.cpp +++ b/libsrc/leddevice/LedDevice.cpp @@ -265,12 +265,13 @@ bool LedDevice::switchOn() { if ( _isEnabled &&_isDeviceInitialised ) { - storeState(); - - if ( powerOn() ) + if ( storeState() ) { - _isOn = true; - rc = true; + if ( powerOn() ) + { + _isOn = true; + rc = true; + } } } } diff --git a/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.cpp b/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.cpp index e95f408d..b5bb2140 100644 --- a/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.cpp @@ -13,6 +13,8 @@ // Constants namespace { +const bool verbose = false; +const bool verbose3 = false; const QString MULTICAST_GROUP_DEFAULT_ADDRESS = "239.255.255.250"; const quint16 MULTICAST_GROUP_DEFAULT_PORT = 49692; @@ -272,13 +274,13 @@ void LedDeviceAtmoOrb::setColor(int orbId, const ColorRgb &color, int commandTyp void LedDeviceAtmoOrb::sendCommand(const QByteArray &bytes) { - //Debug ( _log, "command: [%s] -> %s:%u", QSTRING_CSTR( QString(bytes.toHex())), QSTRING_CSTR(_groupAddress.toString()), _multiCastGroupPort ); + DebugIf(verbose3, _log, "command: [%s] -> %s:%u", QSTRING_CSTR( QString(bytes.toHex())), QSTRING_CSTR(_groupAddress.toString()), _multiCastGroupPort ); _udpSocket->writeDatagram(bytes.data(), bytes.size(), _groupAddress, _multiCastGroupPort); } QJsonObject LedDeviceAtmoOrb::discover(const QJsonObject& params) { - //Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); QJsonObject devicesDiscovered; devicesDiscovered.insert("ledDeviceType", _activeDeviceType ); @@ -353,14 +355,14 @@ QJsonObject LedDeviceAtmoOrb::discover(const QJsonObject& params) } devicesDiscovered.insert("devices", deviceList); - Debug(_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + DebugIf(verbose, _log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() ); return devicesDiscovered; } void LedDeviceAtmoOrb::identify(const QJsonObject& params) { - //Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); int orbId = 0; if ( params["id"].isString() ) diff --git a/libsrc/leddevice/dev_net/LedDeviceCololight.cpp b/libsrc/leddevice/dev_net/LedDeviceCololight.cpp index a6bac06f..8905a7a5 100644 --- a/libsrc/leddevice/dev_net/LedDeviceCololight.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceCololight.cpp @@ -1,50 +1,50 @@ #include "LedDeviceCololight.h" #include +#include #include #include #include -#include #include // Constants namespace { -const bool verbose = false; -const bool verbose3 = false; + const bool verbose = false; + const bool verbose3 = false; -// Configuration settings + // Configuration settings -const char CONFIG_HW_LED_COUNT[] = "hardwareLedCount"; + const char CONFIG_HW_LED_COUNT[] = "hardwareLedCount"; -const int COLOLIGHT_BEADS_PER_MODULE = 19; + const int COLOLIGHT_BEADS_PER_MODULE = 19; -// Cololight discovery service + // Cololight discovery service -const int API_DEFAULT_PORT = 8900; + const int API_DEFAULT_PORT = 8900; -const char DISCOVERY_ADDRESS[] = "255.255.255.255"; -const quint16 DISCOVERY_PORT = 12345; -const char DISCOVERY_MESSAGE[] = "Z-SEARCH * \r\n"; -constexpr std::chrono::milliseconds DEFAULT_DISCOVERY_TIMEOUT{ 2000 }; -constexpr std::chrono::milliseconds DEFAULT_READ_TIMEOUT{ 1000 }; -constexpr std::chrono::milliseconds DEFAULT_IDENTIFY_TIME{ 2000 }; + const char DISCOVERY_ADDRESS[] = "255.255.255.255"; + const quint16 DISCOVERY_PORT = 12345; + const char DISCOVERY_MESSAGE[] = "Z-SEARCH * \r\n"; + constexpr std::chrono::milliseconds DEFAULT_DISCOVERY_TIMEOUT{ 2000 }; + constexpr std::chrono::milliseconds DEFAULT_READ_TIMEOUT{ 1000 }; + constexpr std::chrono::milliseconds DEFAULT_IDENTIFY_TIME{ 2000 }; -const char COLOLIGHT_MODEL[] = "mod"; -const char COLOLIGHT_MODEL_TYPE[] = "subkey"; -const char COLOLIGHT_MAC[] = "sn"; -const char COLOLIGHT_NAME[] = "name"; + const char COLOLIGHT_MODEL[] = "mod"; + const char COLOLIGHT_MODEL_TYPE[] = "subkey"; + const char COLOLIGHT_MAC[] = "sn"; + const char COLOLIGHT_NAME[] = "name"; -const char COLOLIGHT_MODEL_IDENTIFIER[] = "OD_WE_QUAN"; + const char COLOLIGHT_MODEL_IDENTIFIER[] = "OD_WE_QUAN"; } //End of constants LedDeviceCololight::LedDeviceCololight(const QJsonObject& deviceConfig) : ProviderUdp(deviceConfig) - , _modelType(-1) - , _ledLayoutType(-1) - , _ledBeadCount(0) - , _distance(0) - , _sequenceNumber(1) + , _modelType(-1) + , _ledLayoutType(-1) + , _ledBeadCount(0) + , _distance(0) + , _sequenceNumber(1) { _packetFixPart.append(reinterpret_cast(PACKET_HEADER), sizeof(PACKET_HEADER)); _packetFixPart.append(reinterpret_cast(PACKET_SECU), sizeof(PACKET_SECU)); @@ -186,7 +186,7 @@ bool LedDeviceCololight::getInfo() QByteArray response; if (readResponse(response)) { - DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); + DebugIf(verbose,_log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); quint16 ledNum = qFromBigEndian(response.data() + 1); @@ -267,7 +267,7 @@ bool LedDeviceCololight::setColor(const uint32_t color) QByteArray response; if (readResponse(response)) { - DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); + DebugIf(verbose,_log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); isCmdOK = true; } } @@ -303,7 +303,7 @@ bool LedDeviceCololight::setState(bool isOn) QByteArray response; if (readResponse(response)) { - DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); + DebugIf(verbose,_log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); isCmdOK = true; } } @@ -327,7 +327,7 @@ bool LedDeviceCololight::setStateDirect(bool isOn) QByteArray response; if (readResponse(response)) { - DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); + DebugIf(verbose,_log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); isCmdOK = true; } } @@ -381,7 +381,7 @@ bool LedDeviceCololight::setTL1CommandMode(bool isOn) QByteArray response; if (readResponse(response)) { - DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); + DebugIf(verbose,_log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); isCmdOK = true; } } @@ -498,7 +498,7 @@ bool LedDeviceCololight::readResponse(QByteArray& response) } else { - DebugIf(verbose, _log, "No additional data returned"); + DebugIf(verbose,_log, "No additional data returned"); } } isRequestOK = true; @@ -605,7 +605,7 @@ QJsonArray LedDeviceCololight::discover() { QJsonObject obj; - QString ipAddress = i.key(); + const QString& ipAddress = i.key(); obj.insert("ip", ipAddress); obj.insert("model", i.value().value(COLOLIGHT_MODEL)); obj.insert("type", i.value().value(COLOLIGHT_MODEL_TYPE)); @@ -661,26 +661,27 @@ QJsonObject LedDeviceCololight::discover(const QJsonObject& /*params*/) devicesDiscovered.insert("discoveryMethod", discoveryMethod); devicesDiscovered.insert("devices", deviceList); - //Debug(_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose,_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); return devicesDiscovered; } QJsonObject LedDeviceCololight::getProperties(const QJsonObject& params) { - DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose,_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); QJsonObject properties; - QString apiHostname = params["host"].toString(""); + QString hostName = params["host"].toString(""); quint16 apiPort = static_cast(params["port"].toInt(API_DEFAULT_PORT)); QJsonObject propertiesDetails; - if (!apiHostname.isEmpty()) + if (!hostName.isEmpty()) { QJsonObject deviceConfig; - deviceConfig.insert("host", apiHostname); + deviceConfig.insert("host", hostName); deviceConfig.insert("port", apiPort); + if (ProviderUdp::init(deviceConfig)) { if (getInfo()) @@ -708,23 +709,23 @@ QJsonObject LedDeviceCololight::getProperties(const QJsonObject& params) properties.insert("properties", propertiesDetails); - DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose,_log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData()); return properties; } void LedDeviceCololight::identify(const QJsonObject& params) { - DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose,_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); - QString apiHostname = params["host"].toString(""); + QString hostName = params["host"].toString(""); quint16 apiPort = static_cast(params["port"].toInt(API_DEFAULT_PORT)); - if (!apiHostname.isEmpty()) + if (!hostName.isEmpty()) { QJsonObject deviceConfig; - deviceConfig.insert("host", apiHostname); + deviceConfig.insert("host", hostName); deviceConfig.insert("port", apiPort); if (ProviderUdp::init(deviceConfig)) { @@ -732,9 +733,7 @@ void LedDeviceCololight::identify(const QJsonObject& params) { setEffect(THE_CIRCUS); - QEventLoop loop; - QTimer::singleShot(DEFAULT_IDENTIFY_TIME.count(), &loop, &QEventLoop::quit); - loop.exec(); + wait(DEFAULT_IDENTIFY_TIME); setColor(ColorRgb::BLACK); } diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp index f76c29b8..fdc50dcb 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp @@ -5,7 +5,6 @@ #include // Qt includes -#include #include #include @@ -78,12 +77,16 @@ const char SSDP_LIGHTPANELS[] = "nanoleaf_aurora:light"; // Nanoleaf Panel Shapetypes enum SHAPETYPES { - TRIANGLE, - RHYTM, - SQUARE, - CONTROL_SQUARE_PRIMARY, - CONTROL_SQUARE_PASSIVE, - POWER_SUPPLY, + TRIANGLE = 0, + RHYTM = 1, + SQUARE = 2, + CONTROL_SQUARE_PRIMARY = 3, + CONTROL_SQUARE_PASSIVE = 4, + POWER_SUPPLY= 5, + HEXAGON_SHAPES = 7, + TRIANGE_SHAPES = 8, + MINI_TRIANGE_SHAPES = 8, + SHAPES_CONTROLLER = 12 }; // Nanoleaf external control versions @@ -100,8 +103,8 @@ LedDeviceNanoleaf::LedDeviceNanoleaf(const QJsonObject& deviceConfig) , _leftRight(true) , _startPos(0) , _endPos(0) - , _extControlVersion(EXTCTRLVER_V2), - _panelLedCount(0) + , _extControlVersion(EXTCTRLVER_V2) + , _panelLedCount(0) { } @@ -127,7 +130,7 @@ bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig) Info(_log, "Device Nanoleaf does not require rewrites. Refresh time is ignored."); } - DebugIf(verbose, _log, "deviceConfig: [%s]", QString(QJsonDocument(_devConfig).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose,_log, "deviceConfig: [%s]", QString(QJsonDocument(_devConfig).toJson(QJsonDocument::Compact)).toUtf8().constData()); bool isInitOK = false; @@ -164,29 +167,29 @@ bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig) // TODO: Allow to handle port dynamically //Set hostname as per configuration and_defaultHost default port - _hostname = deviceConfig[CONFIG_ADDRESS].toString(); + _hostName = deviceConfig[CONFIG_ADDRESS].toString(); _apiPort = API_DEFAULT_PORT; _authToken = deviceConfig[CONFIG_AUTH_TOKEN].toString(); //If host not configured the init failed - if (_hostname.isEmpty()) + if (_hostName.isEmpty()) { this->setInError("No target hostname nor IP defined"); isInitOK = false; } else { - if (initRestAPI(_hostname, _apiPort, _authToken)) + if (initRestAPI(_hostName, _apiPort, _authToken)) { // Read LedDevice configuration and validate against device configuration if (initLedsConfiguration()) { // Set UDP streaming host and port - _devConfig["host"] = _hostname; + _devConfig["host"] = _hostName; _devConfig["port"] = STREAM_CONTROL_DEFAULT_PORT; isInitOK = ProviderUdp::init(_devConfig); - Debug(_log, "Hostname/IP : %s", QSTRING_CSTR(_hostname)); + Debug(_log, "Hostname/IP : %s", QSTRING_CSTR(_hostName)); Debug(_log, "Port : %d", _port); } } @@ -206,7 +209,8 @@ bool LedDeviceNanoleaf::initLedsConfiguration() httpResponse response = _restApi->get(); if (response.error()) { - this->setInError(response.getErrorReason()); + QString errorReason = QString("Getting device details failed with error: '%1'").arg(response.getErrorReason()); + this->setInError ( errorReason ); isInitOK = false; } else @@ -243,16 +247,16 @@ bool LedDeviceNanoleaf::initLedsConfiguration() int panelshapeType = panelObj[PANEL_SHAPE_TYPE].toInt(); //int panelOrientation = panelObj[PANEL_ORIENTATION].toInt(); - DebugIf(verbose, _log, "Panel [%d] (%d,%d) - Type: [%d]", panelId, panelX, panelY, panelshapeType); + DebugIf(verbose,_log, "Panel [%d] (%d,%d) - Type: [%d]", panelId, panelX, panelY, panelshapeType); - // Skip Rhythm panels - if (panelshapeType != RHYTM) + // Skip Rhythm and Shapes controller panels + if (panelshapeType != RHYTM && panelshapeType != SHAPES_CONTROLLER) { panelMap[panelY][panelX] = panelId; } else { // Reset non support/required features - Info(_log, "Rhythm panel skipped."); + Info(_log, "Rhythm/Shape Controller panel skipped."); } } @@ -360,32 +364,31 @@ int LedDeviceNanoleaf::open() int retval = -1; _isDeviceReady = false; - QJsonDocument responseDoc = changeToExternalControlMode(); - // Resolve port for Light Panels - QJsonObject jsonStreamControllInfo = responseDoc.object(); - if (!jsonStreamControllInfo.isEmpty()) + QJsonDocument responseDoc; + if (changeToExternalControlMode(responseDoc)) { - //Set default streaming port - _port = static_cast(jsonStreamControllInfo[STREAM_CONTROL_PORT].toInt()); - } + // Resolve port for Light Panels + QJsonObject jsonStreamControllInfo = responseDoc.object(); + if (!jsonStreamControllInfo.isEmpty()) + { + //Set default streaming port + _port = static_cast(jsonStreamControllInfo[STREAM_CONTROL_PORT].toInt()); + } - if (ProviderUdp::open() == 0) - { - // Everything is OK, device is ready - _isDeviceReady = true; - retval = 0; + if (ProviderUdp::open() == 0) + { + // Everything is OK, device is ready + _isDeviceReady = true; + retval = 0; + } } return retval; } -QJsonObject LedDeviceNanoleaf::discover(const QJsonObject& /*params*/) +QJsonArray LedDeviceNanoleaf::discover() { - QJsonObject devicesDiscovered; - devicesDiscovered.insert("ledDeviceType", _activeDeviceType); - QJsonArray deviceList; - // Discover Nanoleaf Devices SSDPDiscover discover; // Search for Canvas and Light-Panels @@ -399,26 +402,41 @@ QJsonObject LedDeviceNanoleaf::discover(const QJsonObject& /*params*/) deviceList = discover.getServicesDiscoveredJson(); } + return deviceList; +} + +QJsonObject LedDeviceNanoleaf::discover(const QJsonObject& /*params*/) +{ + QJsonObject devicesDiscovered; + devicesDiscovered.insert("ledDeviceType", _activeDeviceType); + + QString discoveryMethod("ssdp"); + QJsonArray deviceList; + + deviceList = discover(); + + devicesDiscovered.insert("discoveryMethod", discoveryMethod); devicesDiscovered.insert("devices", deviceList); - Debug(_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose,_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); return devicesDiscovered; } QJsonObject LedDeviceNanoleaf::getProperties(const QJsonObject& params) { - Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose,_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); QJsonObject properties; // Get Nanoleaf device properties - QString host = params["host"].toString(""); - if (!host.isEmpty()) + QString hostName = params["host"].toString(""); + + if (!hostName.isEmpty()) { QString authToken = params["token"].toString(""); QString filter = params["filter"].toString(""); // Resolve hostname and port (or use default API port) - QStringList addressparts = QStringUtils::split(host, ":", QStringUtils::SplitBehavior::SkipEmptyParts); + QStringList addressparts = QStringUtils::split(hostName, ":", QStringUtils::SplitBehavior::SkipEmptyParts); QString apiHost = addressparts[0]; int apiPort; @@ -443,22 +461,22 @@ QJsonObject LedDeviceNanoleaf::getProperties(const QJsonObject& params) properties.insert("properties", response.getBody().object()); - Debug(_log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose,_log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData()); } return properties; } void LedDeviceNanoleaf::identify(const QJsonObject& params) { - Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose,_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); - QString host = params["host"].toString(""); - if (!host.isEmpty()) + QString hostName = params["host"].toString(""); + if (!hostName.isEmpty()) { QString authToken = params["token"].toString(""); // Resolve hostname and port (or use default API port) - QStringList addressparts = QStringUtils::split(host, ":", QStringUtils::SplitBehavior::SkipEmptyParts); + QStringList addressparts = QStringUtils::split(hostName, ":", QStringUtils::SplitBehavior::SkipEmptyParts); QString apiHost = addressparts[0]; int apiPort; @@ -485,26 +503,41 @@ void LedDeviceNanoleaf::identify(const QJsonObject& params) bool LedDeviceNanoleaf::powerOn() { + bool on = false; if (_isDeviceReady) { - changeToExternalControlMode(); - - //Power-on Nanoleaf device - _restApi->setPath(API_STATE); - _restApi->put(getOnOffRequest(true)); + if (changeToExternalControlMode()) + { + //Power-on Nanoleaf device + _restApi->setPath(API_STATE); + httpResponse response = _restApi->put(getOnOffRequest(true)); + if (response.error()) + { + QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason()); + this->setInError ( errorReason ); + on = false; + } + } } - return true; + return on; } bool LedDeviceNanoleaf::powerOff() { + bool off = true; if (_isDeviceReady) { //Power-off the Nanoleaf device physically _restApi->setPath(API_STATE); - _restApi->put(getOnOffRequest(false)); + httpResponse response = _restApi->put(getOnOffRequest(false)); + if (response.error()) + { + QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason()); + this->setInError ( errorReason ); + off = false; + } } - return true; + return off; } QString LedDeviceNanoleaf::getOnOffRequest(bool isOn) const @@ -513,16 +546,33 @@ QString LedDeviceNanoleaf::getOnOffRequest(bool isOn) const return QString("{\"%1\":{\"%2\":%3}}").arg(STATE_ON, STATE_ONOFF_VALUE, state); } -QJsonDocument LedDeviceNanoleaf::changeToExternalControlMode() +bool LedDeviceNanoleaf::changeToExternalControlMode() { + QJsonDocument resp; + return changeToExternalControlMode(resp); +} + +bool LedDeviceNanoleaf::changeToExternalControlMode(QJsonDocument& resp) +{ + bool success = false; Debug(_log, "Set Nanoleaf to External Control (UDP) streaming mode"); _extControlVersion = EXTCTRLVER_V2; //Enable UDP Mode v2 _restApi->setPath(API_EFFECT); httpResponse response = _restApi->put(API_EXT_MODE_STRING_V2); + if (response.error()) + { + QString errorReason = QString("Change to external control mode failed with error: '%1'").arg(response.getErrorReason()); + this->setInError ( errorReason ); + } + else + { + resp = response.getBody(); + success = true; + } - return response.getBody(); + return success; } int LedDeviceNanoleaf::write(const std::vector& ledValues) diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h index f6d53c08..ab736165 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h @@ -149,9 +149,15 @@ private: /// /// @brief Change Nanoleaf device to External Control (UDP) mode /// - /// @return Response from device - ///@brief - QJsonDocument changeToExternalControlMode(); + /// @return True, if success + bool changeToExternalControlMode(); + /// + /// @brief Change Nanoleaf device to External Control (UDP) mode + /// + /// @param[out] response from device + /// + /// @return True, if success + bool changeToExternalControlMode(QJsonDocument& resp); /// /// @brief Get command to power Nanoleaf device on or off @@ -161,10 +167,18 @@ private: /// QString getOnOffRequest(bool isOn) const; + /// + /// @brief Discover Nanoleaf devices available (for configuration). + /// Nanoleaf specific ssdp discovery + /// + /// @return A JSON structure holding a list of devices found + /// + QJsonArray discover(); + ///REST-API wrapper ProviderRestApi* _restApi; - QString _hostname; + QString _hostName; int _apiPort; QString _authToken; diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp index db67a7f3..46e2c4b7 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp @@ -115,7 +115,7 @@ CiColor CiColor::rgbToCiColor(double red, double green, double blue, const CiCol double cy; double bri; - if(red + green + blue > 0) + if( (red + green + blue) > 0) { // Apply gamma correction. double r = (red > 0.04045) ? pow((red + 0.055) / (1.0 + 0.055), 2.4) : (red / 12.92); @@ -157,7 +157,7 @@ CiColor CiColor::rgbToCiColor(double red, double green, double blue, const CiCol CiColor xy = { cx, cy, bri }; - if(red + green + blue > 0) + if( (red + green + blue) > 0) { // Check if the given XY value is within the color reach of our lamps. if (!isPointInLampsReach(xy, colorSpace)) @@ -387,8 +387,11 @@ void LedDevicePhilipsHueBridge::log(const char* msg, const char* type, ...) cons vsnprintf(val, max_val_length, type, args); va_end(args); std::string s = msg; - int max = 30; - s.append(max - s.length(), ' '); + size_t max = 30; + if (max > s.length()) + { + s.append(max - s.length(), ' '); + } Debug( _log, "%s: %s", s.c_str(), val ); } @@ -859,7 +862,7 @@ bool LedDevicePhilipsHue::init(const QJsonObject &deviceConfig) if( _groupId == 0 ) { - log( "Group-ID is invalid", "%d", _groupId ); + Error(_log, "Disabling Entertainment API as Group-ID is invalid" ); _useHueEntertainmentAPI = false; } } @@ -888,7 +891,7 @@ bool LedDevicePhilipsHue::setLights() if( _useHueEntertainmentAPI ) { _useHueEntertainmentAPI = false; - Debug(_log, "Group-ID [%u] is not usable - Entertainment API usage was disabled!", _groupId ); + Error(_log, "Group-ID [%u] is not usable - Entertainment API usage was disabled!", _groupId ); } lArray = _devConfig[ CONFIG_LIGHTIDS ].toArray(); } @@ -1018,7 +1021,7 @@ bool LedDevicePhilipsHue::updateLights(const QMap &map) if( lightsCount == 0 ) { - Debug(_log, "No usable lights found!" ); + Error(_log, "No usable lights found!" ); isInitOK = false; } @@ -1073,18 +1076,18 @@ bool LedDevicePhilipsHue::openStream() if( isInitOK ) { - Info(_log, "Philips Hue Entertaiment API successful connected! Start Streaming." ); + Info(_log, "Philips Hue Entertainment API successful connected! Start Streaming." ); _allLightsBlack = true; noSignalDetection(); } else { - Error(_log, "Philips Hue Entertaiment API not connected!" ); + Error(_log, "Philips Hue Entertainment API not connected!" ); } } else { - Error(_log, "Philips Hue Entertaiment API could not initialisized!" ); + Error(_log, "Philips Hue Entertainment API could not be initialised!" ); } return isInitOK; @@ -1315,7 +1318,7 @@ bool LedDevicePhilipsHue::switchOff() stop_retry_left = 3; if (_useHueEntertainmentAPI) { - stopStream(); + stopStream(); } return LedDevicePhilipsHueBridge::switchOff(); @@ -1467,7 +1470,7 @@ void LedDevicePhilipsHue::setColor(PhilipsHueLight& light, CiColor& color) if( !_useHueEntertainmentAPI ) { const int bri = qRound(qMin(254.0, _brightnessFactor * qMax(1.0, color.bri * 254.0))); - QString stateCmd = QString("\"%1\":[%2,%3],\"%4\":%5").arg( API_XY_COORDINATES ).arg( color.x, 0, 'd', 4 ).arg( color.y, 0, 'd', 4 ).arg( API_BRIGHTNESS ).arg( bri ); + QString stateCmd = QString("{\"%1\":[%2,%3],\"%4\":%5}").arg( API_XY_COORDINATES ).arg( color.x, 0, 'd', 4 ).arg( color.y, 0, 'd', 4 ).arg( API_BRIGHTNESS ).arg( bri ); setLightState( light.getId(), stateCmd ); } else diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.cpp b/libsrc/leddevice/dev_net/LedDeviceWled.cpp index 80c24680..1f5511cb 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceWled.cpp @@ -1,14 +1,20 @@ // Local-Hyperion includes #include "LedDeviceWled.h" -#include #include +#include +#include + +#include // Constants namespace { +const bool verbose = false; + // Configuration settings const char CONFIG_ADDRESS[] = "host"; +const char CONFIG_RESTORE_STATE[] = "restoreOriginalState"; // UDP elements const quint16 STREAM_DEFAULT_PORT = 19446; @@ -24,12 +30,11 @@ const char API_PATH_STATE[] = "state"; const char STATE_ON[] = "on"; const char STATE_VALUE_TRUE[] = "true"; const char STATE_VALUE_FALSE[] = "false"; +const char STATE_LIVE[] = "live"; -// WLED ssdp services -// TODO: WLED - Update ssdp discovery parameters when available -const char SSDP_ID[] = "ssdp:all"; -const char SSDP_FILTER[] = "(.*)"; -const char SSDP_FILTER_HEADER[] = "ST"; +const int BRI_MAX = 255; + +constexpr std::chrono::milliseconds DEFAULT_IDENTIFY_TIME{ 2000 }; } //End of constants @@ -53,7 +58,6 @@ LedDevice* LedDeviceWled::construct(const QJsonObject &deviceConfig) bool LedDeviceWled::init(const QJsonObject &deviceConfig) { - Debug(_log, ""); bool isInitOK = false; // Initialise LedDevice sub-class, ProviderUdp::init will be executed later, if connectivity is defined @@ -66,18 +70,21 @@ bool LedDeviceWled::init(const QJsonObject &deviceConfig) Debug(_log, "ColorOrder : %s", QSTRING_CSTR( this->getColorOrder() )); Debug(_log, "LatchTime : %d", this->getLatchTime()); + _isRestoreOrigState = _devConfig[CONFIG_RESTORE_STATE].toBool(false); + Debug(_log, "RestoreOrigState : %d", _isRestoreOrigState); + //Set hostname as per configuration - QString address = deviceConfig[ CONFIG_ADDRESS ].toString(); + QString hostName = deviceConfig[ CONFIG_ADDRESS ].toString(); //If host not configured the init fails - if ( address.isEmpty() ) + if ( hostName.isEmpty() ) { this->setInError("No target hostname nor IP defined"); return false; } else { - QStringList addressparts = QStringUtils::split(address,":", QStringUtils::SplitBehavior::SkipEmptyParts); + QStringList addressparts = QStringUtils::split(hostName,":", QStringUtils::SplitBehavior::SkipEmptyParts); _hostname = addressparts[0]; if ( addressparts.size() > 1 ) { @@ -100,13 +107,11 @@ bool LedDeviceWled::init(const QJsonObject &deviceConfig) } } } - Debug(_log, "[%d]", isInitOK); return isInitOK; } bool LedDeviceWled::initRestAPI(const QString &hostname, int port) { - Debug(_log, ""); bool isInitOK = false; if ( _restApi == nullptr ) @@ -116,38 +121,68 @@ bool LedDeviceWled::initRestAPI(const QString &hostname, int port) isInitOK = true; } - - Debug(_log, "[%d]", isInitOK); return isInitOK; } QString LedDeviceWled::getOnOffRequest(bool isOn) const { QString state = isOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE; - return QString( "{\"%1\":%2}" ).arg( STATE_ON, state); + return QString( "\"%1\":%2,\"%3\":%4" ).arg( STATE_ON, state).arg( STATE_LIVE, state); } +QString LedDeviceWled::getBrightnessRequest(int bri) const +{ + return QString( "\"bri\":%1" ).arg(bri); +} + +QString LedDeviceWled::getEffectRequest(int effect, int speed) const +{ + return QString( "\"seg\":{\"fx\":%1,\"sx\":%2}" ).arg(effect).arg(speed); +} + +QString LedDeviceWled::getLorRequest(int lor) const +{ + return QString( "\"lor\":%1" ).arg(lor); +} + +bool LedDeviceWled::sendStateUpdateRequest(const QString &request) +{ + bool rc = true; + + _restApi->setPath(API_PATH_STATE); + + httpResponse response1 = _restApi->put(QString("{%1}").arg(request)); + if ( response1.error() ) + { + rc = false; + } + return rc; +} bool LedDeviceWled::powerOn() { - Debug(_log, ""); - bool on = true; + bool on = false; if ( _isDeviceReady) { //Power-on WLED device _restApi->setPath(API_PATH_STATE); - httpResponse response = _restApi->put(getOnOffRequest(true)); + + httpResponse response = _restApi->put(QString("{%1,%2}").arg(getOnOffRequest(true)).arg(getBrightnessRequest(BRI_MAX))); if ( response.error() ) { - this->setInError ( response.getErrorReason() ); + QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason()); + this->setInError ( errorReason ); on = false; } + else + { + on = true; + } } return on; } bool LedDeviceWled::powerOff() { - Debug(_log, ""); bool off = true; if ( _isDeviceReady) { @@ -156,53 +191,89 @@ bool LedDeviceWled::powerOff() //Power-off the WLED device physically _restApi->setPath(API_PATH_STATE); - httpResponse response = _restApi->put(getOnOffRequest(false)); + httpResponse response = _restApi->put(QString("{%1}").arg(getOnOffRequest(false))); if ( response.error() ) { - this->setInError ( response.getErrorReason() ); + QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason()); + this->setInError ( errorReason ); off = false; } } return off; } +bool LedDeviceWled::storeState() +{ + bool rc = true; + + if ( _isRestoreOrigState ) + { + _restApi->setPath(API_PATH_STATE); + + httpResponse response = _restApi->get(); + if ( response.error() ) + { + QString errorReason = QString("Storing device state failed with error: '%1'").arg(response.getErrorReason()); + setInError(errorReason); + rc = false; + } + else + { + _originalStateProperties = response.getBody().object(); + DebugIf(verbose, _log, "state: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + } + } + + return rc; +} + +bool LedDeviceWled::restoreState() +{ + bool rc = true; + + if ( _isRestoreOrigState ) + { + //powerOff(); + _restApi->setPath(API_PATH_STATE); + + _originalStateProperties[STATE_LIVE] = false; + + httpResponse response = _restApi->put(QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + if ( response.error() ) + { + Warning (_log, "%s restoring state failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); + } + } + + return rc; +} + QJsonObject LedDeviceWled::discover(const QJsonObject& /*params*/) { QJsonObject devicesDiscovered; devicesDiscovered.insert("ledDeviceType", _activeDeviceType ); QJsonArray deviceList; - - // Discover WLED Devices - SSDPDiscover discover; - discover.skipDuplicateKeys(true); - discover.setSearchFilter(SSDP_FILTER, SSDP_FILTER_HEADER); - QString searchTarget = SSDP_ID; - - if ( discover.discoverServices(searchTarget) > 0 ) - { - deviceList = discover.getServicesDiscoveredJson(); - } - devicesDiscovered.insert("devices", deviceList); - Debug(_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + DebugIf(verbose, _log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() ); return devicesDiscovered; } QJsonObject LedDeviceWled::getProperties(const QJsonObject& params) { - Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() ); QJsonObject properties; - // Get Nanoleaf device properties - QString host = params["host"].toString(""); - if ( !host.isEmpty() ) + QString hostName = params["host"].toString(""); + + if ( !hostName.isEmpty() ) { QString filter = params["filter"].toString(""); // Resolve hostname and port (or use default API port) - QStringList addressparts = QStringUtils::split(host,":", QStringUtils::SplitBehavior::SkipEmptyParts); + QStringList addressparts = QStringUtils::split(hostName,":", QStringUtils::SplitBehavior::SkipEmptyParts); QString apiHost = addressparts[0]; int apiPort; @@ -226,49 +297,45 @@ QJsonObject LedDeviceWled::getProperties(const QJsonObject& params) properties.insert("properties", response.getBody().object()); - Debug(_log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); } return properties; } -void LedDeviceWled::identify(const QJsonObject& /*params*/) +void LedDeviceWled::identify(const QJsonObject& params) { -#if 0 - Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); - QString host = params["host"].toString(""); - if ( !host.isEmpty() ) + QString hostName = params["host"].toString(""); + + if ( !hostName.isEmpty() ) { // Resolve hostname and port (or use default API port) - QStringList addressparts = QStringUtils::split(host,":", QStringUtils::SplitBehavior::SkipEmptyParts); + QStringList addressparts = QStringUtils::split(hostName,":", QStringUtils::SplitBehavior::SkipEmptyParts); QString apiHost = addressparts[0]; int apiPort; if ( addressparts.size() > 1) + { apiPort = addressparts[1].toInt(); + } else + { apiPort = API_DEFAULT_PORT; + } - // TODO: WLED::identify - Replace with valid identification code + initRestAPI(apiHost, apiPort); - // initRestAPI(apiHost, apiPort); + _isRestoreOrigState = true; + storeState(); - // QString resource = QString("%1/%2/%3").arg( API_LIGHTS ).arg( lightId ).arg( API_STATE); - // _restApi->setPath(resource); + QString request = getOnOffRequest(true) + "," + getLorRequest(1) + "," + getEffectRequest(25); + sendStateUpdateRequest(request); - // QString stateCmd; - // stateCmd += QString("\"%1\":%2,").arg( API_STATE_ON ).arg( API_STATE_VALUE_TRUE ); - // stateCmd += QString("\"%1\":\"%2\"").arg( "alert" ).arg( "select" ); - // stateCmd = "{" + stateCmd + "}"; + wait(DEFAULT_IDENTIFY_TIME); - // // Perform request - // httpResponse response = _restApi->put(stateCmd); - // if ( response.error() ) - // { - // Warning (_log, "%s identification failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); - // } + restoreState(); } -#endif } int LedDeviceWled::write(const std::vector &ledValues) diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.h b/libsrc/leddevice/dev_net/LedDeviceWled.h index 114519cc..5871a44a 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.h +++ b/libsrc/leddevice/dev_net/LedDeviceWled.h @@ -8,8 +8,6 @@ /// /// Implementation of a WLED-device -/// ... -/// /// class LedDeviceWled : public ProviderUdp { @@ -105,6 +103,25 @@ protected: /// bool powerOff() override; + /// + /// @brief Store the device's original state. + /// + /// Save the device's state before hyperion color streaming starts allowing to restore state during switchOff(). + /// + /// @return True if success + /// + bool storeState() override; + + /// + /// @brief Restore the device's original state. + /// + /// Restore the device's state as before hyperion color streaming started. + /// This includes the on/off state of the device. + /// + /// @return True, if success + /// + bool restoreState() override; + private: /// @@ -123,12 +140,20 @@ private: /// @return Command to switch device on/off /// QString getOnOffRequest (bool isOn ) const; + QString getBrightnessRequest (int bri ) const; + QString getEffectRequest(int effect, int speed=128) const; + QString getLorRequest(int lor) const; + + bool sendStateUpdateRequest(const QString &request); ///REST-API wrapper ProviderRestApi* _restApi; QString _hostname; int _apiPort; + + QJsonObject _originalStateProperties; + }; #endif // LEDDEVICEWLED_H diff --git a/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp b/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp index cca03b9b..3a14cb0b 100644 --- a/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp @@ -234,12 +234,12 @@ int YeelightLight::writeCommand( const QJsonDocument &command, QJsonArray &resul if ( ! _tcpSocket->waitForBytesWritten(WRITE_TIMEOUT.count()) ) { QString errorReason = QString ("(%1) %2").arg(_tcpSocket->error()).arg( _tcpSocket->errorString()); - log ( 2, "Error:", "bytesWritten: [%ll], %s", bytesWritten, QSTRING_CSTR(errorReason)); + log ( 2, "Error:", "bytesWritten: [%d], %s", bytesWritten, QSTRING_CSTR(errorReason)); this->setInError ( errorReason ); } else { - log ( 3, "Success:", "Bytes written [%ll]", bytesWritten ); + log ( 3, "Success:", "Bytes written [%d]", bytesWritten ); // Avoid to overrun the Yeelight Command Quota qint64 elapsedTime = QDateTime::currentMSecsSinceEpoch() - _lastWriteTime; @@ -258,7 +258,7 @@ int YeelightLight::writeCommand( const QJsonDocument &command, QJsonArray &resul { do { - log ( 3, "Reading:", "Bytes available [%ll]", _tcpSocket->bytesAvailable() ); + log ( 3, "Reading:", "Bytes available [%d]", _tcpSocket->bytesAvailable() ); while ( _tcpSocket->canReadLine() ) { QByteArray response = _tcpSocket->readLine(); @@ -338,7 +338,7 @@ bool YeelightLight::streamCommand( const QJsonDocument &command ) { int error = _tcpStreamSocket->error(); QString errorReason = QString ("(%1) %2").arg(error).arg( _tcpStreamSocket->errorString()); - log ( 1, "Error:", "bytesWritten: [%ll], %s", bytesWritten, QSTRING_CSTR(errorReason)); + log ( 1, "Error:", "bytesWritten: [%d], %s", bytesWritten, QSTRING_CSTR(errorReason)); if ( error == QAbstractSocket::RemoteHostClosedError ) { @@ -353,7 +353,7 @@ bool YeelightLight::streamCommand( const QJsonDocument &command ) } else { - log ( 3, "Success:", "Bytes written [%ll]", bytesWritten ); + log ( 3, "Success:", "Bytes written [%d]", bytesWritten ); rc = true; } } @@ -956,7 +956,10 @@ void YeelightLight::log(int logLevel, const char* msg, const char* type, ...) va_end(args); std::string s = msg; uint max = 20; - s.append(max - s.length(), ' '); + if (max > s.length()) + { + s.append(max - s.length(), ' '); + } Debug( _log, "%d|%15.15s| %s: %s", logLevel, QSTRING_CSTR(_name), s.c_str(), val); } @@ -1076,12 +1079,12 @@ bool LedDeviceYeelight::init(const QJsonObject &deviceConfig) int configuredYeelightsCount = 0; for (const QJsonValueRef light : configuredYeelightLights) { - QString host = light.toObject().value("host").toString(); + QString hostName = light.toObject().value("host").toString(); int port = light.toObject().value("port").toInt(API_DEFAULT_PORT); - if ( !host.isEmpty() ) + if ( !hostName.isEmpty() ) { QString name = light.toObject().value("name").toString(); - Debug(_log, "Light [%u] - %s (%s:%d)", configuredYeelightsCount, QSTRING_CSTR(name), QSTRING_CSTR(host), port ); + Debug(_log, "Light [%u] - %s (%s:%d)", configuredYeelightsCount, QSTRING_CSTR(name), QSTRING_CSTR(hostName), port ); ++configuredYeelightsCount; } } @@ -1107,10 +1110,10 @@ bool LedDeviceYeelight::init(const QJsonObject &deviceConfig) _lightsAddressList.clear(); for (int j = 0; j < static_cast( configuredLedCount ); ++j) { - QString address = configuredYeelightLights[j].toObject().value("host").toString(); + QString hostName = configuredYeelightLights[j].toObject().value("host").toString(); int port = configuredYeelightLights[j].toObject().value("port").toInt(API_DEFAULT_PORT); - QStringList addressparts = QStringUtils::split(address,":", QStringUtils::SplitBehavior::SkipEmptyParts); + QStringList addressparts = QStringUtils::split(hostName,":", QStringUtils::SplitBehavior::SkipEmptyParts); QString apiHost = addressparts[0]; int apiPort = port; @@ -1347,14 +1350,10 @@ bool LedDeviceYeelight::restoreState() return rc; } -QJsonObject LedDeviceYeelight::discover(const QJsonObject& /*params*/) +QJsonArray LedDeviceYeelight::discover() { - QJsonObject devicesDiscovered; - devicesDiscovered.insert("ledDeviceType", _activeDeviceType ); - QJsonArray deviceList; - // Discover Yeelight Devices SSDPDiscover discover; discover.setPort(SSDP_PORT); discover.skipDuplicateKeys(true); @@ -1365,25 +1364,36 @@ QJsonObject LedDeviceYeelight::discover(const QJsonObject& /*params*/) { deviceList = discover.getServicesDiscoveredJson(); } + return deviceList; +} + +QJsonObject LedDeviceYeelight::discover(const QJsonObject& /*params*/) +{ + QJsonObject devicesDiscovered; + devicesDiscovered.insert("ledDeviceType", _activeDeviceType ); + + QString discoveryMethod("ssdp"); + QJsonArray deviceList; + deviceList = discover(); devicesDiscovered.insert("devices", deviceList); - Debug(_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + + DebugIf(verbose,_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() ); return devicesDiscovered; } QJsonObject LedDeviceYeelight::getProperties(const QJsonObject& params) { - Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + DebugIf(verbose,_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() ); QJsonObject properties; - QString apiHostname = params["hostname"].toString(""); + QString hostName = params["hostname"].toString(""); quint16 apiPort = static_cast( params["port"].toInt(API_DEFAULT_PORT) ); - Debug (_log, "apiHost [%s], apiPort [%d]", QSTRING_CSTR(apiHostname), apiPort); - if ( !apiHostname.isEmpty() ) + if ( !hostName.isEmpty() ) { - YeelightLight yeelight(_log, apiHostname, apiPort); + YeelightLight yeelight(_log, hostName, apiPort); //yeelight.setDebuglevel(3); if ( yeelight.open() ) @@ -1399,15 +1409,15 @@ QJsonObject LedDeviceYeelight::getProperties(const QJsonObject& params) void LedDeviceYeelight::identify(const QJsonObject& params) { - Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + DebugIf(verbose,_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() ); - QString apiHostname = params["hostname"].toString(""); + QString hostName = params["hostname"].toString(""); quint16 apiPort = static_cast( params["port"].toInt(API_DEFAULT_PORT) ); - Debug (_log, "apiHost [%s], apiPort [%d]", QSTRING_CSTR(apiHostname), apiPort); + Debug (_log, "apiHost [%s], apiPort [%d]", QSTRING_CSTR(hostName), apiPort); - if ( !apiHostname.isEmpty() ) + if ( !hostName.isEmpty() ) { - YeelightLight yeelight(_log, apiHostname, apiPort); + YeelightLight yeelight(_log, hostName, apiPort); //yeelight.setDebuglevel(3); if ( yeelight.open() ) diff --git a/libsrc/leddevice/dev_net/LedDeviceYeelight.h b/libsrc/leddevice/dev_net/LedDeviceYeelight.h index ff37c5ea..481ac91d 100644 --- a/libsrc/leddevice/dev_net/LedDeviceYeelight.h +++ b/libsrc/leddevice/dev_net/LedDeviceYeelight.h @@ -591,6 +591,14 @@ private: /// uint getLightsCount() const { return _lightsCount; } + /// + /// @brief Discover Yeelight devices available (for configuration). + /// Yeelight specific UDP Broadcast discovery + /// + /// @return A JSON structure holding a list of devices found + /// + QJsonArray discover(); + /// Array of the Yeelight addresses handled by the LED-device QVector _lightsAddressList; diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.cpp b/libsrc/leddevice/dev_net/ProviderRestApi.cpp index 29ba212d..d625b667 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.cpp +++ b/libsrc/leddevice/dev_net/ProviderRestApi.cpp @@ -8,12 +8,20 @@ //std includes #include +#include // Constants namespace { const QChar ONE_SLASH = '/'; +const int HTTP_STATUS_NO_CONTENT = 204; +const int HTTP_STATUS_BAD_REQUEST = 400; +const int HTTP_STATUS_UNAUTHORIZED = 401; +const int HTTP_STATUS_NOT_FOUND = 404; + +constexpr std::chrono::milliseconds DEFAULT_REST_TIMEOUT{ 400 }; + } //End of constants ProviderRestApi::ProviderRestApi(const QString &host, int port, const QString &basePath) @@ -59,7 +67,7 @@ void ProviderRestApi::appendPath ( const QString &path ) appendPath (_path, path ); } -void ProviderRestApi::appendPath ( QString& path, const QString &appendPath) const +void ProviderRestApi::appendPath ( QString& path, const QString &appendPath) { if ( !appendPath.isEmpty() && appendPath != ONE_SLASH ) { @@ -118,20 +126,26 @@ httpResponse ProviderRestApi::get() httpResponse ProviderRestApi::get(const QUrl &url) { - Debug(_log, "GET: [%s]", QSTRING_CSTR( url.toString() )); - // Perform request QNetworkRequest request(url); QNetworkReply* reply = _networkManager->get(request); + // Connect requestFinished signal to quit slot of the loop. QEventLoop loop; - loop.connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + QEventLoop::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + + ReplyTimeout::set(reply, DEFAULT_REST_TIMEOUT.count()); + // Go into the loop until the request is finished. loop.exec(); httpResponse response; if(reply->operation() == QNetworkAccessManager::GetOperation) { + if(reply->error() != QNetworkReply::NoError) + { + Debug(_log, "GET: [%s]", QSTRING_CSTR( url.toString() )); + } response = getResponse(reply ); } // Free space. @@ -147,19 +161,25 @@ httpResponse ProviderRestApi::put(const QString &body) httpResponse ProviderRestApi::put(const QUrl &url, const QString &body) { - Debug(_log, "PUT: [%s] [%s]", QSTRING_CSTR( url.toString() ), QSTRING_CSTR( body ) ); // Perform request QNetworkRequest request(url); QNetworkReply* reply = _networkManager->put(request, body.toUtf8()); // Connect requestFinished signal to quit slot of the loop. QEventLoop loop; - loop.connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + QEventLoop::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + + ReplyTimeout::set(reply, DEFAULT_REST_TIMEOUT.count()); + // Go into the loop until the request is finished. loop.exec(); httpResponse response; if(reply->operation() == QNetworkAccessManager::PutOperation) { + if(reply->error() != QNetworkReply::NoError) + { + Debug(_log, "PUT: [%s] [%s]", QSTRING_CSTR( url.toString() ), QSTRING_CSTR( body ) ); + } response = getResponse(reply); } // Free space. @@ -175,14 +195,11 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const &reply) int httpStatusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); response.setHttpStatusCode(httpStatusCode); - - Debug(_log, "Reply.httpStatusCode [%d]", httpStatusCode ); - response.setNetworkReplyError(reply->error()); if(reply->error() == QNetworkReply::NoError) { - if ( httpStatusCode != 204 ){ + if ( httpStatusCode != HTTP_STATUS_NO_CONTENT ){ QByteArray replyData = reply->readAll(); if ( !replyData.isEmpty()) @@ -211,18 +228,19 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const &reply) } else { + Debug(_log, "Reply.httpStatusCode [%d]", httpStatusCode ); QString errorReason; if ( httpStatusCode > 0 ) { QString httpReason = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ).toString(); QString advise; switch ( httpStatusCode ) { - case 400: + case HTTP_STATUS_BAD_REQUEST: advise = "Check Request Body"; break; - case 401: + case HTTP_STATUS_UNAUTHORIZED: advise = "Check Authentication Token (API Key)"; break; - case 404: + case HTTP_STATUS_NOT_FOUND: advise = "Check Resource given"; break; default: @@ -231,10 +249,20 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const &reply) errorReason = QString ("[%3 %4] - %5").arg(QString(httpStatusCode) , httpReason, advise); } else { + errorReason = reply->errorString(); + + if ( reply->error() == QNetworkReply::OperationCanceledError ) + { + //Do not report errors caused by request cancellation because of timeouts + Debug(_log, "Reply: [%s]", QSTRING_CSTR(errorReason) ); + } + else + { + response.setError(true); + response.setErrorReason(errorReason); + } } - response.setError(true); - response.setErrorReason(errorReason); // Create valid body which is empty response.setBody( QJsonDocument() ); diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.h b/libsrc/leddevice/dev_net/ProviderRestApi.h index 5a66bb20..d59709d1 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.h +++ b/libsrc/leddevice/dev_net/ProviderRestApi.h @@ -10,6 +10,48 @@ #include #include +#include +#include + +//Set QNetworkReply timeout without external timer +//https://stackoverflow.com/questions/37444539/how-to-set-qnetworkreply-timeout-without-external-timer + +class ReplyTimeout : public QObject { + Q_OBJECT +public: + enum HandleMethod { Abort, Close }; + ReplyTimeout(QNetworkReply* reply, const int timeout, HandleMethod method = Abort) : + QObject(reply), m_method(method) + { + Q_ASSERT(reply); + if (reply && reply->isRunning()) { + m_timer.start(timeout, this); + connect(reply, &QNetworkReply::finished, this, &QObject::deleteLater); + } + } + static void set(QNetworkReply* reply, const int timeout, HandleMethod method = Abort) + { + new ReplyTimeout(reply, timeout, method); + } + +protected: + QBasicTimer m_timer; + HandleMethod m_method; + void timerEvent(QTimerEvent * ev) override { + if (!m_timer.isActive() || ev->timerId() != m_timer.timerId()) + return; + auto reply = static_cast(parent()); + if (reply->isRunning()) + { + if (m_method == Close) + reply->close(); + else if (m_method == Abort) + reply->abort(); + m_timer.stop(); + } + } +}; + /// /// Response object for REST-API calls and JSON-responses /// @@ -191,7 +233,7 @@ private: /// @param[in/out] path to be updated /// @param[in] path, element to be appended /// - void appendPath (QString &path, const QString &appendPath) const; + static void appendPath (QString &path, const QString &appendPath) ; Logger* _log; diff --git a/libsrc/leddevice/dev_serial/LedDeviceAtmo.cpp b/libsrc/leddevice/dev_serial/LedDeviceAtmo.cpp index 464bbde7..c062baa5 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceAtmo.cpp +++ b/libsrc/leddevice/dev_serial/LedDeviceAtmo.cpp @@ -1,6 +1,10 @@ // hyperion local includes #include "LedDeviceAtmo.h" +namespace { + const bool verbose = false; +} //End of constants + LedDeviceAtmo::LedDeviceAtmo(const QJsonObject &deviceConfig) : ProviderRs232(deviceConfig) { @@ -43,3 +47,20 @@ int LedDeviceAtmo::write(const std::vector &ledValues) memcpy(4 + _ledBuffer.data(), ledValues.data(), _ledCount * sizeof(ColorRgb)); return writeBytes(_ledBuffer.size(), _ledBuffer.data()); } + +QJsonObject LedDeviceAtmo::getProperties(const QJsonObject& params) +{ + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + QJsonObject properties; + + QString serialPort = params["serialPort"].toString(""); + + QJsonObject propertiesDetails; + QJsonArray possibleLedCounts = { 5 }; + propertiesDetails.insert("ledCount", possibleLedCounts); + + properties.insert("properties", propertiesDetails); + + DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData()); + return properties; +} diff --git a/libsrc/leddevice/dev_serial/LedDeviceAtmo.h b/libsrc/leddevice/dev_serial/LedDeviceAtmo.h index 0bb316d9..49665068 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceAtmo.h +++ b/libsrc/leddevice/dev_serial/LedDeviceAtmo.h @@ -23,6 +23,14 @@ public: /// static LedDevice* construct(const QJsonObject &deviceConfig); + /// + /// @brief Get a Atmo device's resource properties + /// + /// @param[in] params Parameters to query device + /// @return A JSON structure holding the device's properties + /// + QJsonObject getProperties(const QJsonObject& params) override; + private: /// diff --git a/libsrc/leddevice/dev_serial/LedDeviceKarate.cpp b/libsrc/leddevice/dev_serial/LedDeviceKarate.cpp index ccbd9598..e99ccce0 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceKarate.cpp +++ b/libsrc/leddevice/dev_serial/LedDeviceKarate.cpp @@ -1,27 +1,31 @@ // hyperion local includes #include "LedDeviceKarate.h" -LedDeviceKarate::LedDeviceKarate(const QJsonObject &deviceConfig) +namespace { + const bool verbose = false; +} //End of constants + +LedDeviceKarate::LedDeviceKarate(const QJsonObject& deviceConfig) : ProviderRs232(deviceConfig) { } -LedDevice* LedDeviceKarate::construct(const QJsonObject &deviceConfig) +LedDevice* LedDeviceKarate::construct(const QJsonObject& deviceConfig) { return new LedDeviceKarate(deviceConfig); } -bool LedDeviceKarate::init(const QJsonObject &deviceConfig) +bool LedDeviceKarate::init(const QJsonObject& deviceConfig) { bool isInitOK = false; // Initialise sub-class - if ( ProviderRs232::init(deviceConfig) ) + if (ProviderRs232::init(deviceConfig)) { if (_ledCount != 8 && _ledCount != 16) { //Error( _log, "%d channels configured. This should always be 16!", _ledCount); - QString errortext = QString ("%1 channels configured. This should always be 8 or 16!").arg(_ledCount); + QString errortext = QString("%1 channels configured. This should always be 8 or 16!").arg(_ledCount); this->setInError(errortext); isInitOK = false; } @@ -33,8 +37,8 @@ bool LedDeviceKarate::init(const QJsonObject &deviceConfig) _ledBuffer[2] = 0x00; // Checksum _ledBuffer[3] = _ledCount * 3; // Number of Databytes send - Debug( _log, "Karatelight header for %d leds: 0x%02x 0x%02x 0x%02x 0x%02x", _ledCount, - _ledBuffer[0], _ledBuffer[1], _ledBuffer[2], _ledBuffer[3] ); + Debug(_log, "Karatelight header for %d leds: 0x%02x 0x%02x 0x%02x 0x%02x", _ledCount, + _ledBuffer[0], _ledBuffer[1], _ledBuffer[2], _ledBuffer[3]); isInitOK = true; } @@ -42,20 +46,37 @@ bool LedDeviceKarate::init(const QJsonObject &deviceConfig) return isInitOK; } -int LedDeviceKarate::write(const std::vector &ledValues) +int LedDeviceKarate::write(const std::vector& ledValues) { - for (signed iLed=0; iLed< static_cast(_ledCount); iLed++) - { - const ColorRgb& rgb = ledValues[iLed]; - _ledBuffer[iLed*3+4] = rgb.green; - _ledBuffer[iLed*3+5] = rgb.blue; - _ledBuffer[iLed*3+6] = rgb.red; - } + for (signed iLed = 0; iLed < static_cast(_ledCount); iLed++) + { + const ColorRgb& rgb = ledValues[iLed]; + _ledBuffer[iLed * 3 + 4] = rgb.green; + _ledBuffer[iLed * 3 + 5] = rgb.blue; + _ledBuffer[iLed * 3 + 6] = rgb.red; + } // Calc Checksum - _ledBuffer[2] = _ledBuffer[0] ^ _ledBuffer[1]; - for (unsigned int i = 3; i < _ledBuffer.size(); i++) - _ledBuffer[2] ^= _ledBuffer[i]; + _ledBuffer[2] = _ledBuffer[0] ^ _ledBuffer[1]; + for (unsigned int i = 3; i < _ledBuffer.size(); i++) + _ledBuffer[2] ^= _ledBuffer[i]; return writeBytes(_ledBuffer.size(), _ledBuffer.data()); } + +QJsonObject LedDeviceKarate::getProperties(const QJsonObject& params) +{ + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + QJsonObject properties; + + QString serialPort = params["serialPort"].toString(""); + + QJsonObject propertiesDetails; + QJsonArray possibleLedCounts = { 16, 8 }; + propertiesDetails.insert("ledCount", possibleLedCounts); + + properties.insert("properties", propertiesDetails); + + DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData()); + return properties; +} diff --git a/libsrc/leddevice/dev_serial/LedDeviceKarate.h b/libsrc/leddevice/dev_serial/LedDeviceKarate.h index f0b4329c..33762823 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceKarate.h +++ b/libsrc/leddevice/dev_serial/LedDeviceKarate.h @@ -26,6 +26,14 @@ public: /// @return LedDevice constructed static LedDevice* construct(const QJsonObject &deviceConfig); + /// + /// @brief Get a Karate device's resource properties + /// + /// @param[in] params Parameters to query device + /// @return A JSON structure holding the device's properties + /// + QJsonObject getProperties(const QJsonObject& params) override; + private: /// diff --git a/libsrc/leddevice/schemas/schema-wled.json b/libsrc/leddevice/schemas/schema-wled.json index bd609223..bb00f3de 100644 --- a/libsrc/leddevice/schemas/schema-wled.json +++ b/libsrc/leddevice/schemas/schema-wled.json @@ -4,8 +4,16 @@ "properties":{ "host" : { "type": "string", - "title":"edt_dev_spec_targetIpHost_title", - "propertyOrder" : 1 + "title": "edt_dev_spec_targetIpHost_title", + "required": true, + "propertyOrder": 1 + }, + "restoreOriginalState": { + "type": "boolean", + "title": "edt_dev_spec_restoreOriginalState_title", + "default": false, + "required": true, + "propertyOrder": 2 }, "latchTime": { "type": "integer", @@ -15,7 +23,7 @@ "minimum": 0, "maximum": 1000, "access" : "expert", - "propertyOrder" : 2 + "propertyOrder" : 3 } }, "additionalProperties": true