mirror of
https://github.com/hyperion-project/hyperion.ng.git
synced 2023-10-10 13:36:59 +02:00
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
This commit is contained in:
parent
956edf9e78
commit
41af5c1b9e
@ -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
|
||||
|
||||
|
27
include/utils/WaitTime.h
Normal file
27
include/utils/WaitTime.h
Normal file
@ -0,0 +1,27 @@
|
||||
#ifndef WAITTIME_H
|
||||
#define WAITTIME_H
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
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
|
@ -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",
|
||||
"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
|
||||
}
|
||||
},
|
||||
|
@ -265,8 +265,8 @@ bool LedDevice::switchOn()
|
||||
{
|
||||
if ( _isEnabled &&_isDeviceInitialised )
|
||||
{
|
||||
storeState();
|
||||
|
||||
if ( storeState() )
|
||||
{
|
||||
if ( powerOn() )
|
||||
{
|
||||
_isOn = true;
|
||||
@ -274,6 +274,7 @@ bool LedDevice::switchOn()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
@ -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() )
|
||||
|
@ -1,10 +1,10 @@
|
||||
#include "LedDeviceCololight.h"
|
||||
|
||||
#include <utils/QStringUtils.h>
|
||||
#include <utils/WaitTime.h>
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
#include <QtEndian>
|
||||
#include <QEventLoop>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
@ -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,7 +661,7 @@ 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;
|
||||
}
|
||||
@ -671,16 +671,17 @@ QJsonObject LedDeviceCololight::getProperties(const QJsonObject& params)
|
||||
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<quint16>(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())
|
||||
@ -717,14 +718,14 @@ void LedDeviceCololight::identify(const QJsonObject& params)
|
||||
{
|
||||
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<quint16>(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);
|
||||
}
|
||||
|
@ -5,7 +5,6 @@
|
||||
#include <utils/QStringUtils.h>
|
||||
|
||||
// Qt includes
|
||||
#include <QEventLoop>
|
||||
#include <QNetworkReply>
|
||||
#include <QtEndian>
|
||||
|
||||
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -245,14 +249,14 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
|
||||
|
||||
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,7 +364,9 @@ int LedDeviceNanoleaf::open()
|
||||
int retval = -1;
|
||||
_isDeviceReady = false;
|
||||
|
||||
QJsonDocument responseDoc = changeToExternalControlMode();
|
||||
QJsonDocument responseDoc;
|
||||
if (changeToExternalControlMode(responseDoc))
|
||||
{
|
||||
// Resolve port for Light Panels
|
||||
QJsonObject jsonStreamControllInfo = responseDoc.object();
|
||||
if (!jsonStreamControllInfo.isEmpty())
|
||||
@ -375,17 +381,14 @@ int LedDeviceNanoleaf::open()
|
||||
_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();
|
||||
|
||||
if (changeToExternalControlMode())
|
||||
{
|
||||
//Power-on Nanoleaf device
|
||||
_restApi->setPath(API_STATE);
|
||||
_restApi->put(getOnOffRequest(true));
|
||||
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<ColorRgb>& ledValues)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
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<quint16, QJsonObject> &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;
|
||||
@ -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
|
||||
|
@ -1,14 +1,20 @@
|
||||
// Local-Hyperion includes
|
||||
#include "LedDeviceWled.h"
|
||||
|
||||
#include <ssdp/SSDPDiscover.h>
|
||||
#include <utils/QStringUtils.h>
|
||||
#include <utils/WaitTime.h>
|
||||
#include <QThread>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
// 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);
|
||||
|
||||
// QString resource = QString("%1/%2/%3").arg( API_LIGHTS ).arg( lightId ).arg( API_STATE);
|
||||
// _restApi->setPath(resource);
|
||||
|
||||
// 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 + "}";
|
||||
|
||||
// // 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()));
|
||||
// }
|
||||
}
|
||||
#endif
|
||||
else
|
||||
{
|
||||
apiPort = API_DEFAULT_PORT;
|
||||
}
|
||||
|
||||
initRestAPI(apiHost, apiPort);
|
||||
|
||||
_isRestoreOrigState = true;
|
||||
storeState();
|
||||
|
||||
QString request = getOnOffRequest(true) + "," + getLorRequest(1) + "," + getEffectRequest(25);
|
||||
sendStateUpdateRequest(request);
|
||||
|
||||
wait(DEFAULT_IDENTIFY_TIME);
|
||||
|
||||
restoreState();
|
||||
}
|
||||
}
|
||||
|
||||
int LedDeviceWled::write(const std::vector<ColorRgb> &ledValues)
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
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<int>( 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<quint16>( 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<quint16>( 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() )
|
||||
|
@ -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<yeelightAddress> _lightsAddressList;
|
||||
|
||||
|
@ -8,12 +8,20 @@
|
||||
|
||||
//std includes
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Create valid body which is empty
|
||||
response.setBody( QJsonDocument() );
|
||||
|
@ -10,6 +10,48 @@
|
||||
#include <QUrlQuery>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include <QBasicTimer>
|
||||
#include <QTimerEvent>
|
||||
|
||||
//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<QNetworkReply*>(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;
|
||||
|
||||
|
@ -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<ColorRgb> &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;
|
||||
}
|
||||
|
@ -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:
|
||||
|
||||
///
|
||||
|
@ -1,6 +1,10 @@
|
||||
// hyperion local includes
|
||||
#include "LedDeviceKarate.h"
|
||||
|
||||
namespace {
|
||||
const bool verbose = false;
|
||||
} //End of constants
|
||||
|
||||
LedDeviceKarate::LedDeviceKarate(const QJsonObject& deviceConfig)
|
||||
: ProviderRs232(deviceConfig)
|
||||
{
|
||||
@ -59,3 +63,20 @@ int LedDeviceKarate::write(const std::vector<ColorRgb> &ledValues)
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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:
|
||||
|
||||
///
|
||||
|
@ -5,8 +5,16 @@
|
||||
"host" : {
|
||||
"type": "string",
|
||||
"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",
|
||||
"title":"edt_dev_spec_latchtime_title",
|
||||
@ -15,7 +23,7 @@
|
||||
"minimum": 0,
|
||||
"maximum": 1000,
|
||||
"access" : "expert",
|
||||
"propertyOrder" : 2
|
||||
"propertyOrder" : 3
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
|
Loading…
Reference in New Issue
Block a user