// Local-Hyperion includes #include "LedDeviceWled.h" #include #include #include // mDNS discover #ifdef ENABLE_MDNS #include #include #endif #include #include // Constants namespace { const bool verbose = false; // Configuration settings const char CONFIG_HOST[] = "host"; const char CONFIG_STREAM_PROTOCOL[] = "streamProtocol"; const char CONFIG_RESTORE_STATE[] = "restoreOriginalState"; const char CONFIG_STAY_ON_AFTER_STREAMING[] = "stayOnAfterStreaming"; const char CONFIG_BRIGHTNESS[] = "brightness"; const char CONFIG_BRIGHTNESS_OVERWRITE[] = "overwriteBrightness"; const char CONFIG_SYNC_OVERWRITE[] = "overwriteSync"; const char CONFIG_STREAM_SEGMENTS[] = "segments"; const char CONFIG_STREAM_SEGMENT_ID[] = "streamSegmentId"; const char CONFIG_SWITCH_OFF_OTHER_SEGMENTS[] = "switchOffOtherSegments"; const char DEFAULT_STREAM_PROTOCOL[] = "DDP"; // UDP-RAW const int UDP_STREAM_DEFAULT_PORT = 19446; const int UDP_MAX_LED_NUM = 490; // Version constraints const char WLED_VERSION_DDP[] = "0.11.0"; const char WLED_VERSION_SEGMENT_STREAMING[] = "0.13.3"; // WLED JSON-API elements const int API_DEFAULT_PORT = -1; //Use default port per communication scheme const char API_BASE_PATH[] = "/json/"; const char API_PATH_STATE[] = "state"; const char API_PATH_INFO[] = "info"; // List of State keys const char STATE_ON[] = "on"; const char STATE_BRI[] = "bri"; const char STATE_LIVE[] = "live"; const char STATE_LOR[] = "lor"; const char STATE_SEG[] = "seg"; const char STATE_SEG_ID[] = "id"; const char STATE_SEG_LEN[] = "len"; const char STATE_SEG_FX[] = "fx"; const char STATE_SEG_SX[] = "sx"; const char STATE_MAINSEG[] = "mainseg"; const char STATE_UDPN[] = "udpn"; const char STATE_UDPN_SEND[] = "send"; const char STATE_UDPN_RECV[] = "recv"; const char STATE_TRANSITIONTIME_CURRENTCALL[] = "tt"; // List of Info keys const char INFO_VER[] = "ver"; const char INFO_LIVESEG[] = "liveseg"; //Default state values const bool DEFAULT_IS_RESTORE_STATE = false; const bool DEFAULT_IS_STAY_ON_AFTER_STREAMING = false; const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true; const int BRI_MAX = 255; const bool DEFAULT_IS_SYNC_OVERWRITE = true; const int DEFAULT_SEGMENT_ID = -1; const bool DEFAULT_IS_SWITCH_OFF_OTHER_SEGMENTS = true; constexpr std::chrono::milliseconds DEFAULT_IDENTIFY_TIME{ 2000 }; } //End of constants LedDeviceWled::LedDeviceWled(const QJsonObject &deviceConfig) : ProviderUdp(deviceConfig), LedDeviceUdpDdp(deviceConfig), LedDeviceUdpRaw(deviceConfig) ,_restApi(nullptr) ,_apiPort(API_DEFAULT_PORT) ,_currentVersion("") ,_isBrightnessOverwrite(DEFAULT_IS_BRIGHTNESS_OVERWRITE) ,_brightness (BRI_MAX) ,_isSyncOverwrite(DEFAULT_IS_SYNC_OVERWRITE) ,_originalStateUdpnSend(false) ,_originalStateUdpnRecv(true) ,_isStreamDDP(true) ,_streamSegmentId(DEFAULT_SEGMENT_ID) ,_isSwitchOffOtherSegments(DEFAULT_IS_SWITCH_OFF_OTHER_SEGMENTS) ,_isStreamToSegment(false) { #ifdef ENABLE_MDNS QMetaObject::invokeMethod(&MdnsBrowser::getInstance(), "browseForServiceType", Qt::QueuedConnection, Q_ARG(QByteArray, MdnsServiceRegister::getServiceType(_activeDeviceType))); #endif } LedDeviceWled::~LedDeviceWled() { delete _restApi; _restApi = nullptr; } LedDevice* LedDeviceWled::construct(const QJsonObject &deviceConfig) { return new LedDeviceWled(deviceConfig); } bool LedDeviceWled::init(const QJsonObject &deviceConfig) { bool isInitOK {false}; QString streamProtocol = _devConfig[CONFIG_STREAM_PROTOCOL].toString(DEFAULT_STREAM_PROTOCOL); if (streamProtocol != DEFAULT_STREAM_PROTOCOL) { _isStreamDDP = false; } Debug(_log, "Stream protocol : %s", QSTRING_CSTR(streamProtocol)); Debug(_log, "Stream DDP : %d", _isStreamDDP); if (_isStreamDDP) { LedDeviceUdpDdp::init(deviceConfig); } else { _devConfig["port"] = UDP_STREAM_DEFAULT_PORT; LedDeviceUdpRaw::init(_devConfig); } if (!_isDeviceInError) { _apiPort = API_DEFAULT_PORT; _isRestoreOrigState = _devConfig[CONFIG_RESTORE_STATE].toBool(DEFAULT_IS_RESTORE_STATE); _isStayOnAfterStreaming = _devConfig[CONFIG_STAY_ON_AFTER_STREAMING].toBool(DEFAULT_IS_STAY_ON_AFTER_STREAMING); _isSyncOverwrite = _devConfig[CONFIG_SYNC_OVERWRITE].toBool(DEFAULT_IS_SYNC_OVERWRITE); _isBrightnessOverwrite = _devConfig[CONFIG_BRIGHTNESS_OVERWRITE].toBool(DEFAULT_IS_BRIGHTNESS_OVERWRITE); _brightness = _devConfig[CONFIG_BRIGHTNESS].toInt(BRI_MAX); Debug(_log, "RestoreOrigState : %d", _isRestoreOrigState); Debug(_log, "Overwrite Sync. : %d", _isSyncOverwrite); Debug(_log, "Overwrite Brightn.: %d", _isBrightnessOverwrite); Debug(_log, "Set Brightness to : %d", _brightness); QJsonObject segments = _devConfig[CONFIG_STREAM_SEGMENTS].toObject(); _streamSegmentId = segments[CONFIG_STREAM_SEGMENT_ID].toInt(DEFAULT_SEGMENT_ID); if (_streamSegmentId > DEFAULT_SEGMENT_ID) { _isStreamToSegment = true; } _isSwitchOffOtherSegments = segments[CONFIG_SWITCH_OFF_OTHER_SEGMENTS].toBool(DEFAULT_IS_SWITCH_OFF_OTHER_SEGMENTS); Debug(_log, "Stream to one segment: %s", _isStreamToSegment ? "Yes" : "No"); if (_isStreamToSegment ) { Debug(_log, "Stream to segment [%d]", _streamSegmentId); Debug(_log, "Switch-off other segments: %s", _isSwitchOffOtherSegments ? "Yes" : "No"); } isInitOK = true; } return isInitOK; } bool LedDeviceWled::openRestAPI() { bool isInitOK {true}; if ( _restApi == nullptr ) { _restApi = new ProviderRestApi(_address.toString(), _apiPort); _restApi->setLogger(_log); _restApi->setBasePath( API_BASE_PATH ); } else { _restApi->setHost(_address.toString()); _restApi->setPort(_apiPort); } return isInitOK; } int LedDeviceWled::open() { int retval = -1; _isDeviceReady = false; if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) { if ( openRestAPI() ) { if (_isStreamDDP) { if (LedDeviceUdpDdp::open() == 0) { // Everything is OK, device is ready _isDeviceReady = true; retval = 0; } } else { if (LedDeviceUdpRaw::open() == 0) { // Everything is OK, device is ready _isDeviceReady = true; retval = 0; } } } } return retval; } int LedDeviceWled::close() { int retval = -1; if (_isStreamDDP) { retval = LedDeviceUdpDdp::close(); } else { retval = LedDeviceUdpRaw::close(); } return retval; } QJsonObject LedDeviceWled::getUdpnObject(bool isSendOn, bool isRecvOn) const { QJsonObject udpnObj { {STATE_UDPN_SEND, isSendOn}, {STATE_UDPN_RECV, isRecvOn} }; return udpnObj; } QJsonObject LedDeviceWled::getSegmentObject(int segmentId, bool isOn, int brightness) const { QJsonObject segmentObj { {STATE_SEG_ID, segmentId}, {STATE_ON, isOn} }; if ( brightness > -1) { segmentObj.insert(STATE_BRI, brightness); } return segmentObj; } bool LedDeviceWled::sendStateUpdateRequest(const QJsonObject &request, const QString requestType) { bool rc = true; _restApi->setPath(API_PATH_STATE); httpResponse response = _restApi->put(request); if ( response.error() ) { QString errorReason = QString("%1 request failed with error: '%2'").arg(requestType, response.getErrorReason()); this->setInError ( errorReason ); rc = false; } return rc; } bool LedDeviceWled::isReadyForSegmentStreaming(semver::version& version) const { bool isReady{false}; if (version.isValid()) { semver::version segmentStreamingVersion{WLED_VERSION_SEGMENT_STREAMING}; if (version < segmentStreamingVersion) { Warning(_log, "Segment streaming not supported by your WLED device version [%s], minimum version expected [%s].", _currentVersion.getVersion().c_str(), segmentStreamingVersion.getVersion().c_str()); } else { Debug(_log, "Segment streaming is supported by your WLED device version [%s].", _currentVersion.getVersion().c_str()); isReady = true; } } else { Error(_log, "Version provided to test for streaming readiness is not valid "); } return isReady; } bool LedDeviceWled::isReadyForDDPStreaming(semver::version& version) const { bool isReady{false}; if (version.isValid()) { semver::version ddpVersion{WLED_VERSION_DDP}; if (version < ddpVersion) { Warning(_log, "DDP streaming not supported by your WLED device version [%s], minimum version expected [%s]. Fall back to UDP-Streaming (%d LEDs max)", _currentVersion.getVersion().c_str(), ddpVersion.getVersion().c_str(), UDP_MAX_LED_NUM); } else { Debug(_log, "DDP streaming is supported by your WLED device version [%s]. No limitation in number of LEDs.", _currentVersion.getVersion().c_str()); isReady = true; } } else { Error(_log, "Version provided to test for streaming readiness is not valid "); } return isReady; } bool LedDeviceWled::powerOn() { bool on = false; if ( _isDeviceReady) { //Power-on WLED device QJsonObject cmd; if (_isStreamToSegment) { if (!isReadyForSegmentStreaming(_currentVersion)) { return false; } if (_wledInfo[INFO_LIVESEG].toInt() == -1) { stopEnableAttemptsTimer(); this->setInError( "Segment streaming configured, but \"Use main segment only\" in WLED Sync Interface configuration is not enabled!", false); return false; } else { QJsonArray propertiesSegments = _originalStateProperties[STATE_SEG].toArray(); bool isStreamSegmentIdFound { false }; QJsonArray segments; for (const auto& segmentItem : qAsConst(propertiesSegments)) { QJsonObject segmentObj = segmentItem.toObject(); int segmentID = segmentObj.value(STATE_SEG_ID).toInt(); if (segmentID == _streamSegmentId) { isStreamSegmentIdFound = true; int len = segmentObj.value(STATE_SEG_LEN).toInt(); if (getLedCount() > len) { QString errorReason = QString("Too many LEDs [%1] configured for segment [%2], which supports maximum [%3] LEDs. Check your WLED setup!").arg(getLedCount()).arg(_streamSegmentId).arg(len); this->setInError(errorReason, false); return false; } else { int brightness{ -1 }; if (_isBrightnessOverwrite) { brightness = _brightness; } segments.append(getSegmentObject(segmentID, true, brightness)); } } else { if (_isSwitchOffOtherSegments) { segments.append(getSegmentObject(segmentID, false)); } } } if (!isStreamSegmentIdFound) { QString errorReason = QString("Segment streaming to segment [%1] configured, but segment does not exist on WLED. Check your WLED setup!").arg(_streamSegmentId); this->setInError(errorReason, false); return false; } cmd.insert(STATE_SEG, segments); //Set segment to be streamed to cmd.insert(STATE_MAINSEG, _streamSegmentId); } } else { if (_isBrightnessOverwrite) { cmd.insert(STATE_BRI, _brightness); } } cmd.insert(STATE_LIVE, true); cmd.insert(STATE_ON, true); if (_isSyncOverwrite) { Debug( _log, "Disable synchronisation with other WLED devices"); cmd.insert(STATE_UDPN, getUdpnObject(false, false)); } on = sendStateUpdateRequest(cmd,"Power-on"); } return on; } bool LedDeviceWled::powerOff() { bool off = true; if ( _isDeviceReady) { // Write a final "Black" to have a defined outcome writeBlack(); //Power-off the WLED device physically QJsonObject cmd; if (_isStreamToSegment) { QJsonArray segments; segments.append(getSegmentObject(_streamSegmentId, _isStayOnAfterStreaming)); cmd.insert(STATE_SEG, segments); } cmd.insert(STATE_LIVE, false); cmd.insert(STATE_TRANSITIONTIME_CURRENTCALL, 0); cmd.insert(STATE_ON, _isStayOnAfterStreaming); if (_isSyncOverwrite) { Debug( _log, "Restore synchronisation with other WLED devices"); cmd.insert(STATE_UDPN, getUdpnObject(_originalStateUdpnSend, _originalStateUdpnRecv)); } off = sendStateUpdateRequest(cmd,"Power-off"); } return off; } bool LedDeviceWled::storeState() { bool rc = true; if ( _isRestoreOrigState || _isSyncOverwrite || _isStreamToSegment) { _restApi->setPath(""); httpResponse response = _restApi->get(); if ( response.error() ) { QString errorReason = QString("Retrieving device properties failed with error: '%1'").arg(response.getErrorReason()); setInError(errorReason); rc = false; } else { _originalStateProperties = response.getBody().object().value(API_PATH_STATE).toObject(); DebugIf(verbose, _log, "state: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); QJsonObject udpn = _originalStateProperties.value(STATE_UDPN).toObject(); if (!udpn.isEmpty()) { _originalStateUdpnSend = udpn[STATE_UDPN_SEND].toBool(false); _originalStateUdpnRecv = udpn[STATE_UDPN_RECV].toBool(true); } _wledInfo = response.getBody().object().value(API_PATH_INFO).toObject(); DebugIf(verbose, _log, "info: [%s]", QString(QJsonDocument(_wledInfo).toJson(QJsonDocument::Compact)).toUtf8().constData() ); _currentVersion.setVersion(_wledInfo.value(INFO_VER).toString().toStdString()); } } return rc; } bool LedDeviceWled::restoreState() { bool rc = true; if ( _isRestoreOrigState ) { _restApi->setPath(API_PATH_STATE); if (_isStreamToSegment) { QJsonArray propertiesSegments = _originalStateProperties[STATE_SEG].toArray(); QJsonArray segments; for (const auto& segmentItem : qAsConst(propertiesSegments)) { QJsonObject segmentObj = segmentItem.toObject(); int segmentID = segmentObj.value(STATE_SEG_ID).toInt(); if (segmentID == _streamSegmentId) { segmentObj[STATE_ON] = _isStayOnAfterStreaming; } segments.append(segmentObj); } _originalStateProperties[STATE_SEG] = segments; } _originalStateProperties[STATE_LIVE] = false; _originalStateProperties[STATE_TRANSITIONTIME_CURRENTCALL] = 0; if (_isStayOnAfterStreaming) { _originalStateProperties[STATE_ON] = true; } httpResponse response = _restApi->put(_originalStateProperties); 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; #ifdef ENABLE_MDNS QString discoveryMethod("mDNS"); deviceList = MdnsBrowser::getInstance().getServicesDiscoveredJson( MdnsServiceRegister::getServiceType(_activeDeviceType), MdnsServiceRegister::getServiceNameFilter(_activeDeviceType), DEFAULT_DISCOVER_TIMEOUT ); devicesDiscovered.insert("discoveryMethod", discoveryMethod); #endif devicesDiscovered.insert("devices", deviceList); DebugIf(verbose, _log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() ); return devicesDiscovered; } QJsonObject LedDeviceWled::getProperties(const QJsonObject& params) { DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() ); QJsonObject properties; _hostName = params[CONFIG_HOST].toString(""); _apiPort = API_DEFAULT_PORT; Info(_log, "Get properties for %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName) ); if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) { if ( openRestAPI() ) { QString filter = params["filter"].toString(""); _restApi->setPath(filter); httpResponse response = _restApi->get(); if ( response.error() ) { Warning (_log, "%s get properties failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); } else { QJsonObject propertiesDetails = response.getBody().object(); _wledInfo = propertiesDetails.value(API_PATH_INFO).toObject(); _currentVersion.setVersion(_wledInfo.value(INFO_VER).toString().toStdString()); if (!isReadyForDDPStreaming(_currentVersion)) { if (!propertiesDetails.isEmpty()) { propertiesDetails.insert("maxLedCount", UDP_MAX_LED_NUM); } } properties.insert("properties", propertiesDetails); } } DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); } return properties; } void LedDeviceWled::identify(const QJsonObject& params) { DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); _hostName = params[CONFIG_HOST].toString(""); _apiPort = API_DEFAULT_PORT; Info(_log, "Identify %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName) ); if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) { if ( openRestAPI() ) { _isRestoreOrigState = true; storeState(); QJsonObject cmd; cmd.insert(STATE_ON, true); cmd.insert(STATE_LOR, 1); _streamSegmentId = params[CONFIG_STREAM_SEGMENT_ID].toInt(0); QJsonObject segment; segment = getSegmentObject(_streamSegmentId, true, BRI_MAX); segment.insert(STATE_SEG_FX, 25); segment.insert(STATE_SEG_SX, 128); QJsonArray segments; segments.append(segment); cmd.insert(STATE_SEG, segments); sendStateUpdateRequest(cmd,"Identify"); wait(DEFAULT_IDENTIFY_TIME); restoreState(); } } } int LedDeviceWled::write(const std::vector &ledValues) { int rc {0}; if (_isStreamDDP) { rc = LedDeviceUdpDdp::write(ledValues); } else { rc = LedDeviceUdpRaw::write(ledValues); } return rc; }