// Local-Hyperion includes #include "LedDevicePhilipsHue.h" #include #include #include // Constants namespace { bool verbose = false; // Configuration settings const char CONFIG_ADDRESS[] = "host"; //const char CONFIG_PORT[] = "port"; const char CONFIG_USERNAME[] = "username"; const char CONFIG_CLIENTKEY[] = "clientkey"; const char CONFIG_BRIGHTNESSFACTOR[] = "brightnessFactor"; const char CONFIG_TRANSITIONTIME[] = "transitiontime"; const char CONFIG_BLACK_LIGHTS_TIMEOUT[] = "blackLightsTimeout"; const char CONFIG_ON_OFF_BLACK[] = "switchOffOnBlack"; const char CONFIG_RESTORE_STATE[] = "restoreOriginalState"; const char CONFIG_LIGHTIDS[] = "lightIds"; const char CONFIG_USE_HUE_ENTERTAINMENT_API[] = "useEntertainmentAPI"; const char CONFIG_GROUPID[] = "groupId"; const char CONFIG_VERBOSE[] = "verbose"; const char CONFIG_BRIGHTNESS_MIN[] = "brightnessMin"; const char CONFIG_BRIGHTNESS_MAX[] = "brightnessMax"; const char CONFIG_BRIGHTNESS_THRESHOLD[] = "brightnessThreshold"; const char CONFIG_SSL_HANDSHAKE_TIMEOUT_MIN[] = "sslHSTimeoutMin"; const char CONFIG_SSL_HANDSHAKE_TIMEOUT_MAX[] = "sslHSTimeoutMax"; const char CONFIG_SSL_READ_TIMEOUT[] = "sslReadTimeout"; // Device Data elements const char DEV_DATA_BRIDGEID[] = "bridgeid"; const char DEV_DATA_MODEL[] = "modelid"; const char DEV_DATA_NAME[] = "name"; //const char DEV_DATA_MANUFACTURER[] = "manufacturer"; const char DEV_DATA_FIRMWAREVERSION[] = "swversion"; const char DEV_DATA_APIVERSION[] = "apiversion"; // Philips Hue OpenAPI URLs const int API_DEFAULT_PORT = -1; //Use default port per communication scheme const char API_BASE_PATH[] = "/api/%1/"; const char API_ROOT[] = ""; const char API_STATE[] = "state"; const char API_CONFIG[] = "config"; const char API_LIGHTS[] = "lights"; const char API_GROUPS[] = "groups"; // List of Group / Stream Information const char API_GROUP_NAME[] = "name"; const char API_GROUP_TYPE[] = "type"; const char API_GROUP_TYPE_ENTERTAINMENT[] = "Entertainment"; const char API_STREAM[] = "stream"; const char API_STREAM_ACTIVE[] = "active"; const char API_STREAM_ACTIVE_VALUE_TRUE[] = "true"; const char API_STREAM_ACTIVE_VALUE_FALSE[] = "false"; const char API_STREAM_OWNER[] = "owner"; const char API_STREAM_RESPONSE_FORMAT[] = "/%1/%2/%3/%4"; // List of resources const char API_XY_COORDINATES[] = "xy"; const char API_BRIGHTNESS[] = "bri"; //const char API_SATURATION[] = "sat"; const char API_TRANSITIONTIME[] = "transitiontime"; const char API_MODEID[] = "modelid"; // List of State Information const char API_STATE_ON[] = "on"; const char API_STATE_VALUE_TRUE[] = "true"; const char API_STATE_VALUE_FALSE[] = "false"; // List of Error Information const char API_ERROR[] = "error"; const char API_ERROR_ADDRESS[] = "address"; const char API_ERROR_DESCRIPTION[] = "description"; const char API_ERROR_TYPE[] = "type"; // List of Success Information const char API_SUCCESS[] = "success"; // Phlips Hue ssdp services const char SSDP_ID[] = "upnp:rootdevice"; const char SSDP_FILTER[] = "(.*)IpBridge(.*)"; const char SSDP_FILTER_HEADER[] = "SERVER"; // DTLS Connection / SSL / Cipher Suite const char API_SSL_SERVER_NAME[] = "Hue"; const char API_SSL_SEED_CUSTOM[] = "dtls_client"; const int API_SSL_SERVER_PORT = 2100; const int STREAM_CONNECTION_RETRYS = 5; const int STREAM_SSL_HANDSHAKE_ATTEMPTS = 5; constexpr std::chrono::milliseconds STREAM_REWRITE_TIME{20}; const int SSL_CIPHERSUITES[2] = { MBEDTLS_TLS_PSK_WITH_AES_128_GCM_SHA256, 0 }; } //End of constants bool operator ==(const CiColor& p1, const CiColor& p2) { return ((p1.x == p2.x) && (p1.y == p2.y) && (p1.bri == p2.bri)); } bool operator != (const CiColor& p1, const CiColor& p2) { return !(p1 == p2); } CiColor CiColor::rgbToCiColor(double red, double green, double blue, const CiColorTriangle &colorSpace) { double cx; double cy; double bri; 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); double g = (green > 0.04045) ? pow((green + 0.055) / (1.0 + 0.055), 2.4) : (green / 12.92); double b = (blue > 0.04045) ? pow((blue + 0.055) / (1.0 + 0.055), 2.4) : (blue / 12.92); // Convert to XYZ space. double X = r * 0.664511 + g * 0.154324 + b * 0.162028; double Y = r * 0.283881 + g * 0.668433 + b * 0.047685; double Z = r * 0.000088 + g * 0.072310 + b * 0.986039; cx = X / (X + Y + Z); cy = Y / (X + Y + Z); // RGB to HSV/B Conversion before gamma correction V/B for brightness, not Y from XYZ Space. // bri = std::max(std::max(red, green), blue); // RGB to HSV/B Conversion after gamma correction V/B for brightness, not Y from XYZ Space. bri = std::max(r, std::max(g, b)); } else { cx = 0.0; cy = 0.0; bri = 0.0; } if (std::isnan(cx)) { cx = 0.0; } if (std::isnan(cy)) { cy = 0.0; } if (std::isnan(bri)) { bri = 0.0; } CiColor xy = { cx, cy, bri }; if( (red + green + blue) > 0) { // Check if the given XY value is within the color reach of our lamps. if (!isPointInLampsReach(xy, colorSpace)) { // It seems the color is out of reach let's find the closes color we can produce with our lamp and send this XY value out. XYColor pAB = getClosestPointToPoint(colorSpace.red, colorSpace.green, xy); XYColor pAC = getClosestPointToPoint(colorSpace.blue, colorSpace.red, xy); XYColor pBC = getClosestPointToPoint(colorSpace.green, colorSpace.blue, xy); // Get the distances per point and see which point is closer to our Point. double dAB = getDistanceBetweenTwoPoints(xy, pAB); double dAC = getDistanceBetweenTwoPoints(xy, pAC); double dBC = getDistanceBetweenTwoPoints(xy, pBC); double lowest = dAB; XYColor closestPoint = pAB; if (dAC < lowest) { lowest = dAC; closestPoint = pAC; } if (dBC < lowest) { //lowest = dBC; closestPoint = pBC; } // Change the xy value to a value which is within the reach of the lamp. xy.x = closestPoint.x; xy.y = closestPoint.y; } } return xy; } double CiColor::crossProduct(XYColor p1, XYColor p2) { return p1.x * p2.y - p1.y * p2.x; } bool CiColor::isPointInLampsReach(CiColor p, const CiColorTriangle &colorSpace) { bool rc = false; XYColor v1 = { colorSpace.green.x - colorSpace.red.x, colorSpace.green.y - colorSpace.red.y }; XYColor v2 = { colorSpace.blue.x - colorSpace.red.x, colorSpace.blue.y - colorSpace.red.y }; XYColor q = { p.x - colorSpace.red.x, p.y - colorSpace.red.y }; double s = crossProduct(q, v2) / crossProduct(v1, v2); double t = crossProduct(v1, q) / crossProduct(v1, v2); if ( ( s >= 0.0 ) && ( t >= 0.0 ) && ( s + t <= 1.0 ) ) { rc = true; } return rc; } XYColor CiColor::getClosestPointToPoint(XYColor a, XYColor b, CiColor p) { XYColor AP = { p.x - a.x, p.y - a.y }; XYColor AB = { b.x - a.x, b.y - a.y }; double ab2 = AB.x * AB.x + AB.y * AB.y; double ap_ab = AP.x * AB.x + AP.y * AB.y; double t = ap_ab / ab2; if ( t < 0.0 ) { t = 0.0; } else if ( t > 1.0 ) { t = 1.0; } return { a.x + AB.x * t, a.y + AB.y * t }; } double CiColor::getDistanceBetweenTwoPoints(CiColor p1, XYColor p2) { // Horizontal difference. double dx = p1.x - p2.x; // Vertical difference. double dy = p1.y - p2.y; // Absolute value. return sqrt(dx * dx + dy * dy); } LedDevicePhilipsHueBridge::LedDevicePhilipsHueBridge(const QJsonObject &deviceConfig) : ProviderUdpSSL(deviceConfig) , _restApi(nullptr) , _apiPort(API_DEFAULT_PORT) , _useHueEntertainmentAPI(false) , _api_major(0) , _api_minor(0) , _api_patch(0) , _isHueEntertainmentReady(false) { } LedDevicePhilipsHueBridge::~LedDevicePhilipsHueBridge() { delete _restApi; _restApi = nullptr; } bool LedDevicePhilipsHueBridge::init(const QJsonObject &deviceConfig) { _useHueEntertainmentAPI = deviceConfig[CONFIG_USE_HUE_ENTERTAINMENT_API].toBool(false); // Overwrite non supported/required features _devConfig["latchTime"] = 0; if ( deviceConfig["rewriteTime"].toInt(0) > 0 ) { InfoIf ( ( !_useHueEntertainmentAPI ), _log, "Device Philips Hue does not require rewrites. Refresh time is ignored." ); _devConfig["rewriteTime"] = 0; } DebugIf( verbose, _log, "deviceConfig: [%s]", QString(QJsonDocument(_devConfig).toJson(QJsonDocument::Compact)).toUtf8().constData() ); bool isInitOK = false; if ( LedDevice::init(deviceConfig) ) { log( "DeviceType", "%s", QSTRING_CSTR( this->getActiveDeviceType() ) ); log( "LedCount", "%d", this->getLedCount() ); log( "ColorOrder", "%s", QSTRING_CSTR( this->getColorOrder() ) ); log( "RefreshTime", "%d", _refreshTimerInterval_ms ); log( "LatchTime", "%d", this->getLatchTime() ); //Set hostname as per configuration and_defaultHost default port QString address = deviceConfig[ CONFIG_ADDRESS ].toString(); //If host not configured the init failed if ( address.isEmpty() ) { this->setInError("No target hostname nor IP defined"); isInitOK = false; } else { QStringList addressparts = QStringUtils::split(address,":", QStringUtils::SplitBehavior::SkipEmptyParts); _hostname = addressparts[0]; log( "Hostname/IP", "%s", QSTRING_CSTR( _hostname ) ); if ( addressparts.size() > 1 ) { _apiPort = addressparts[1].toInt(); log( "Port", "%u", _apiPort ); } _username = deviceConfig[ CONFIG_USERNAME ].toString(); if ( initRestAPI( _hostname, _apiPort, _username ) ) { if ( initMaps() ) { isInitOK = ProviderUdpSSL::init(_devConfig); } } } } return isInitOK; } bool LedDevicePhilipsHueBridge::initRestAPI(const QString &hostname, int port, const QString &token) { bool isInitOK = false; if ( _restApi == nullptr ) { _restApi = new ProviderRestApi(hostname, port); //Base-path is api-path + authentication token (here username) _restApi->setBasePath( QString(API_BASE_PATH).arg(token) ); isInitOK = true; } return isInitOK; } int LedDevicePhilipsHueBridge::open() { int retval = -1; _isDeviceReady = false; if( _useHueEntertainmentAPI ) { // Open bridge for streaming if ( ProviderUdpSSL::open() == 0 ) { // Everything is OK, device is ready _isDeviceReady = true; retval = 0; } } else { // Everything is OK, device is ready _isDeviceReady = true; retval = 0; } return retval; } int LedDevicePhilipsHueBridge::close() { _isDeviceReady = false; int retval = 0; if( _useHueEntertainmentAPI ) { retval = ProviderUdpSSL::close(); } return retval; } const int *LedDevicePhilipsHueBridge::getCiphersuites() const { return SSL_CIPHERSUITES; } void LedDevicePhilipsHueBridge::log(const char* msg, const char* type, ...) const { const size_t max_val_length = 1024; char val[max_val_length]; va_list args; va_start(args, type); vsnprintf(val, max_val_length, type, args); va_end(args); std::string s = msg; size_t max = 30; if (max > s.length()) { s.append(max - s.length(), ' '); } Debug( _log, "%s: %s", s.c_str(), val ); } QJsonDocument LedDevicePhilipsHueBridge::getAllBridgeInfos() { return get(API_ROOT); } bool LedDevicePhilipsHueBridge::initMaps() { bool isInitOK = true; QJsonDocument doc = getAllBridgeInfos(); DebugIf( verbose, _log, "doc: [%s]", QString(QJsonDocument(doc).toJson(QJsonDocument::Compact)).toUtf8().constData() ); if ( this->isInError() ) { isInitOK = false; } else { setBridgeConfig( doc ); if( _useHueEntertainmentAPI ) { setGroupMap( doc ); } setLightsMap( doc ); } return isInitOK; } void LedDevicePhilipsHueBridge::setBridgeConfig(const QJsonDocument &doc) { QJsonObject jsonConfigInfo = doc.object()[ API_CONFIG ].toObject(); if ( verbose ) { std::cout << "jsonConfigInfo: [" << QString(QJsonDocument(jsonConfigInfo).toJson(QJsonDocument::Compact)).toUtf8().constData() << "]" << std::endl; } QString deviceName = jsonConfigInfo[DEV_DATA_NAME].toString(); _deviceModel = jsonConfigInfo[DEV_DATA_MODEL].toString(); QString deviceBridgeID = jsonConfigInfo[DEV_DATA_BRIDGEID].toString(); _deviceFirmwareVersion = jsonConfigInfo[DEV_DATA_FIRMWAREVERSION].toString(); _deviceAPIVersion = jsonConfigInfo[DEV_DATA_APIVERSION].toString(); QStringList apiVersionParts = QStringUtils::split(_deviceAPIVersion,".", QStringUtils::SplitBehavior::SkipEmptyParts); if ( !apiVersionParts.isEmpty() ) { _api_major = apiVersionParts[0].toUInt(); _api_minor = apiVersionParts[1].toUInt(); _api_patch = apiVersionParts[2].toUInt(); if ( _api_major > 1 || (_api_major == 1 && _api_minor >= 22) ) { _isHueEntertainmentReady = true; } } if( _useHueEntertainmentAPI ) { DebugIf( !_isHueEntertainmentReady, _log, "Bridge is not Entertainment API Ready - Entertainment API usage was disabled!" ); _useHueEntertainmentAPI = _isHueEntertainmentReady; } log( "Bridge Name", "%s", QSTRING_CSTR( deviceName )); log( "Model", "%s", QSTRING_CSTR( _deviceModel )); log( "Bridge-ID", "%s", QSTRING_CSTR( deviceBridgeID )); log( "SoftwareVersion", "%s", QSTRING_CSTR( _deviceFirmwareVersion )); log( "API-Version", "%u.%u.%u", _api_major, _api_minor, _api_patch ); log( "EntertainmentReady", "%d", static_cast(_isHueEntertainmentReady) ); } void LedDevicePhilipsHueBridge::setLightsMap(const QJsonDocument &doc) { QJsonObject jsonLightsInfo = doc.object()[ API_LIGHTS ].toObject(); DebugIf(verbose, _log, "jsonLightsInfo: [%s]", QString(QJsonDocument(jsonLightsInfo).toJson(QJsonDocument::Compact)).toUtf8().constData() ); // Get all available light ids and their values QStringList keys = jsonLightsInfo.keys(); _ledCount = static_cast(keys.size()); _lightsMap.clear(); for ( int i = 0; i < static_cast(_ledCount); ++i ) { _lightsMap.insert(keys.at(i).toUShort(), jsonLightsInfo.take(keys.at(i)).toObject()); } if ( getLedCount() == 0 ) { this->setInError( "No light-IDs found at the Philips Hue Bridge" ); } else { log( "Lights in Bridge found", "%d", getLedCount() ); } } void LedDevicePhilipsHueBridge::setGroupMap(const QJsonDocument &doc) { QJsonObject jsonGroupsInfo = doc.object()[ API_GROUPS ].toObject(); DebugIf(verbose, _log, "jsonGroupsInfo: [%s]", QString(QJsonDocument(jsonGroupsInfo).toJson(QJsonDocument::Compact)).toUtf8().constData() ); // Get all available group ids and their values QStringList keys = jsonGroupsInfo.keys(); int _groupsCount = keys.size(); _groupsMap.clear(); for ( int i = 0; i < _groupsCount; ++i ) { _groupsMap.insert( keys.at(i).toUShort(), jsonGroupsInfo.take(keys.at(i)).toObject() ); } } QMap LedDevicePhilipsHueBridge::getLightMap() const { return _lightsMap; } QMap LedDevicePhilipsHueBridge::getGroupMap() const { return _groupsMap; } QString LedDevicePhilipsHueBridge::getGroupName(quint16 groupId) const { QString groupName; if( _groupsMap.contains( groupId ) ) { QJsonObject group = _groupsMap.value( groupId ); groupName = group.value( API_GROUP_NAME ).toString().trimmed().replace("\"", ""); } else { Error(_log, "Group ID %u doesn't exists on this bridge", groupId ); } return groupName; } QJsonArray LedDevicePhilipsHueBridge::getGroupLights(quint16 groupId) const { QJsonArray groupLights; // search user groupid inside _groupsMap and create light if found if( _groupsMap.contains( groupId ) ) { QJsonObject group = _groupsMap.value( groupId ); if( group.value( API_GROUP_TYPE ) == API_GROUP_TYPE_ENTERTAINMENT ) { QString groupName = getGroupName( groupId ); groupLights = group.value( API_LIGHTS ).toArray(); log( "Entertainment Group found", "[%d] %s", groupId, QSTRING_CSTR(groupName) ); log( "Lights in Group", "%d", groupLights.size() ); Info(_log, "Entertainment Group [%d] \"%s\" with %d Lights found", groupId, QSTRING_CSTR(groupName), groupLights.size() ); } else { Error(_log, "Group ID %d is not an entertainment group", groupId ); } } else { Error(_log, "Group ID %d doesn't exists on this bridge", groupId ); } return groupLights; } bool LedDevicePhilipsHueBridge::checkApiError(const QJsonDocument &response) { bool apiError = false; QString errorReason; QString strJson(response.toJson(QJsonDocument::Compact)); DebugIf(verbose, _log, "Reply: [%s]", strJson.toUtf8().constData()); QVariantList rspList = response.toVariant().toList(); if ( !rspList.isEmpty() ) { QVariantMap map = rspList.first().toMap(); if ( map.contains( API_ERROR ) ) { // API call failed to execute an error message was returned QString errorAddress = map.value(API_ERROR).toMap().value(API_ERROR_ADDRESS).toString(); QString errorDesc = map.value(API_ERROR).toMap().value(API_ERROR_DESCRIPTION).toString(); QString errorType = map.value(API_ERROR).toMap().value(API_ERROR_TYPE).toString(); log( "Error Type", "%s", QSTRING_CSTR( errorType ) ); log( "Error Address", "%s", QSTRING_CSTR( errorAddress ) ); log( "Error Address Description", "%s", QSTRING_CSTR( errorDesc ) ); if( errorType != "901" ) { errorReason = QString ("(%1) %2, Resource:%3").arg(errorType, errorDesc, errorAddress); this->setInError( errorReason ); apiError = true; } } } return apiError; } QJsonDocument LedDevicePhilipsHueBridge::get(const QString& route) { _restApi->setPath(route); httpResponse response = _restApi->get(); checkApiError(response.getBody()); return response.getBody(); } QJsonDocument LedDevicePhilipsHueBridge::post(const QString& route, const QString& content) { _restApi->setPath(route); httpResponse response = _restApi->put(content); checkApiError(response.getBody()); return response.getBody(); } QJsonDocument LedDevicePhilipsHueBridge::getLightState(unsigned int lightId) { DebugIf( verbose, _log, "GetLightState [%u]", lightId ); return get( QString("%1/%2").arg( API_LIGHTS ).arg( lightId ) ); } void LedDevicePhilipsHueBridge::setLightState(unsigned int lightId, const QString &state) { DebugIf( verbose, _log, "SetLightState [%u]: %s", lightId, QSTRING_CSTR(state) ); post( QString("%1/%2/%3").arg( API_LIGHTS ).arg( lightId ).arg( API_STATE ), state ); } QJsonDocument LedDevicePhilipsHueBridge::getGroupState(unsigned int groupId) { DebugIf( verbose, _log, "GetGroupState [%u]", groupId ); return get( QString("%1/%2").arg( API_GROUPS ).arg( groupId ) ); } QJsonDocument LedDevicePhilipsHueBridge::setGroupState(unsigned int groupId, bool state) { QString active = state ? API_STREAM_ACTIVE_VALUE_TRUE : API_STREAM_ACTIVE_VALUE_FALSE; return post( QString("%1/%2").arg( API_GROUPS ).arg( groupId ), QString("{\"%1\":{\"%2\":%3}}").arg( API_STREAM, API_STREAM_ACTIVE, active ) ); } bool LedDevicePhilipsHueBridge::isStreamOwner(const QString &streamOwner) const { return ( streamOwner != "" && streamOwner == _username ); } const std::set PhilipsHueLight::GAMUT_A_MODEL_IDS = { "LLC001", "LLC005", "LLC006", "LLC007", "LLC010", "LLC011", "LLC012", "LLC013", "LLC014", "LST001" }; const std::set PhilipsHueLight::GAMUT_B_MODEL_IDS = { "LCT001", "LCT002", "LCT003", "LCT007", "LLM001" }; const std::set PhilipsHueLight::GAMUT_C_MODEL_IDS = { "LCA001", "LCA002", "LCA003", "LCG002", "LCP001", "LCP002", "LCT010", "LCT011", "LCT012", "LCT014", "LCT015", "LCT016", "LCT024", "LCX001", "LLC020", "LST002" }; PhilipsHueLight::PhilipsHueLight(Logger* log, unsigned int id, QJsonObject values, unsigned int ledidx) : _log(log) , _id(id) , _ledidx(ledidx) , _on(false) , _transitionTime(0) , _colorBlack({0.0, 0.0, 0.0}) , _modelId(values[API_MODEID].toString().trimmed().replace("\"", "")) { // Find id in the sets and set the appropriate color space. if (GAMUT_A_MODEL_IDS.find(_modelId) != GAMUT_A_MODEL_IDS.end()) { Debug(_log, "Recognized model id %s of light ID %d as gamut A", QSTRING_CSTR(_modelId), id ); _colorSpace.red = {0.704, 0.296}; _colorSpace.green = {0.2151, 0.7106}; _colorSpace.blue = {0.138, 0.08}; _colorBlack = {0.138, 0.08, 0.0}; } else if (GAMUT_B_MODEL_IDS.find(_modelId) != GAMUT_B_MODEL_IDS.end()) { Debug(_log, "Recognized model id %s of light ID %d as gamut B", QSTRING_CSTR(_modelId), id ); _colorSpace.red = {0.675, 0.322}; _colorSpace.green = {0.409, 0.518}; _colorSpace.blue = {0.167, 0.04}; _colorBlack = {0.167, 0.04, 0.0}; } else if (GAMUT_C_MODEL_IDS.find(_modelId) != GAMUT_C_MODEL_IDS.end()) { Debug(_log, "Recognized model id %s of light ID %d as gamut C", QSTRING_CSTR(_modelId), id ); _colorSpace.red = {0.6915, 0.3083}; _colorSpace.green = {0.17, 0.7}; _colorSpace.blue = {0.1532, 0.0475}; _colorBlack = {0.1532, 0.0475, 0.0}; } else { Warning(_log, "Did not recognize model id %s of light ID %d", QSTRING_CSTR(_modelId), id ); _colorSpace.red = {1.0, 0.0}; _colorSpace.green = {0.0, 1.0}; _colorSpace.blue = {0.0, 0.0}; _colorBlack = {0.0, 0.0, 0.0}; } _lightname = values["name"].toString().trimmed().replace("\"", ""); Info(_log, "Light ID %d (\"%s\", LED index \"%d\") created", id, QSTRING_CSTR(_lightname), ledidx ); } PhilipsHueLight::~PhilipsHueLight() { DebugIf(verbose, _log, "Light ID %d (\"%s\", LED index \"%d\") deconstructed", _id, QSTRING_CSTR(_lightname), _ledidx ); } unsigned int PhilipsHueLight::getId() const { return _id; } QString PhilipsHueLight::getOriginalState() const { return _originalState; } void PhilipsHueLight::saveOriginalState(const QJsonObject& values) { // Get state object values which are subject to change. if (!values[API_STATE].toObject().contains("on")) { Error(_log, "Got invalid state object from light ID %d", _id ); } QJsonObject lState = values[API_STATE].toObject(); _originalStateJSON = lState; QJsonObject state; state["on"] = lState["on"]; _originalColor = _colorBlack; QString c; if (state[API_STATE_ON].toBool()) { state[API_XY_COORDINATES] = lState[API_XY_COORDINATES]; state[API_BRIGHTNESS] = lState[API_BRIGHTNESS]; _on = true; _color = { state[API_XY_COORDINATES].toArray()[0].toDouble(), state[API_XY_COORDINATES].toArray()[1].toDouble(), state[API_BRIGHTNESS].toDouble() / 254.0 }; _originalColor = _color; c = QString("{ \"%1\": [%2, %3], \"%4\": %5 }").arg(API_XY_COORDINATES).arg(_originalColor.x, 0, 'd', 4).arg(_originalColor.y, 0, 'd', 4).arg(API_BRIGHTNESS).arg((_originalColor.bri * 254.0), 0, 'd', 4); DebugIf(verbose, _log, "OriginalColor state on: %s", QSTRING_CSTR(c)); _transitionTime = values[API_STATE].toObject()[API_TRANSITIONTIME].toInt(); } //Determine the original state. _originalState = QJsonDocument(state).toJson(QJsonDocument::JsonFormat::Compact).trimmed(); } void PhilipsHueLight::setOnOffState(bool on) { this->_on = on; } void PhilipsHueLight::setTransitionTime(int transitionTime) { this->_transitionTime = transitionTime; } void PhilipsHueLight::setColor(const CiColor& color) { this->_color = color; } bool PhilipsHueLight::getOnOffState() const { return _on; } int PhilipsHueLight::getTransitionTime() const { return _transitionTime; } CiColor PhilipsHueLight::getColor() const { return _color; } CiColorTriangle PhilipsHueLight::getColorSpace() const { return _colorSpace; } LedDevicePhilipsHue::LedDevicePhilipsHue(const QJsonObject& deviceConfig) : LedDevicePhilipsHueBridge(deviceConfig) , _switchOffOnBlack(false) , _brightnessFactor(1.0) , _transitionTime(1) , _isInitLeds(false) , _lightsCount(0) , _groupId(0) , _brightnessMin(0.0) , _brightnessMax(1.0) , _allLightsBlack(false) , _blackLightsTimer(nullptr) , _blackLightsTimeout(15000) , _brightnessThreshold(0.0) , _handshake_timeout_min(STREAM_SSL_HANDSHAKE_TIMEOUT_MIN.count()) , _handshake_timeout_max(STREAM_SSL_HANDSHAKE_TIMEOUT_MAX.count()) , _ssl_read_timeout(STREAM_SSL_READ_TIMEOUT.count()) , _stopConnection(false) , start_retry_left(3) , stop_retry_left(3) { } LedDevice* LedDevicePhilipsHue::construct(const QJsonObject &deviceConfig) { return new LedDevicePhilipsHue(deviceConfig); } LedDevicePhilipsHue::~LedDevicePhilipsHue() { delete _blackLightsTimer; } bool LedDevicePhilipsHue::init(const QJsonObject &deviceConfig) { verbose = deviceConfig[CONFIG_VERBOSE].toBool(false); bool isInitOK = LedDevicePhilipsHueBridge::init(deviceConfig); if ( isInitOK ) { // Initialise LedDevice configuration and execution environment _switchOffOnBlack = _devConfig[CONFIG_ON_OFF_BLACK].toBool(true); _blackLightsTimeout = _devConfig[CONFIG_BLACK_LIGHTS_TIMEOUT].toInt(15000); _brightnessFactor = _devConfig[CONFIG_BRIGHTNESSFACTOR].toDouble(1.0); _transitionTime = _devConfig[CONFIG_TRANSITIONTIME].toInt(1); _isRestoreOrigState = _devConfig[CONFIG_RESTORE_STATE].toBool(true); _groupId = static_cast(_devConfig[CONFIG_GROUPID].toInt(0)); _brightnessMin = _devConfig[CONFIG_BRIGHTNESS_MIN].toDouble(0.0); _brightnessMax = _devConfig[CONFIG_BRIGHTNESS_MAX].toDouble(1.0); _brightnessThreshold = _devConfig[CONFIG_BRIGHTNESS_THRESHOLD].toDouble(0.0); _handshake_timeout_min = _devConfig[CONFIG_SSL_HANDSHAKE_TIMEOUT_MIN].toInt(STREAM_SSL_HANDSHAKE_TIMEOUT_MIN.count()); _handshake_timeout_max = _devConfig[CONFIG_SSL_HANDSHAKE_TIMEOUT_MAX].toInt(STREAM_SSL_HANDSHAKE_TIMEOUT_MAX.count()); _ssl_read_timeout = _devConfig[CONFIG_SSL_READ_TIMEOUT].toInt(STREAM_SSL_READ_TIMEOUT.count()); if( _brightnessMin < 0.0 ) { _brightnessMin = 0.0; } if( _brightnessMax > 1.0 ) { _brightnessMax = 1.0; } if( _brightnessThreshold < 0.0 ) { _brightnessThreshold = 0.0; } if( _brightnessThreshold > 1.0 ) { _brightnessThreshold = 1.0; } if( _handshake_timeout_min <= 0 ) { _handshake_timeout_min = 1; } log( "Off on Black", "%d", static_cast( _switchOffOnBlack ) ); log( "Brightness Factor", "%f", _brightnessFactor ); log( "Transition Time", "%d", _transitionTime ); log( "Restore Original State", "%d", static_cast( _isRestoreOrigState ) ); log( "Use Hue Entertainment API", "%d", static_cast( _useHueEntertainmentAPI) ); if( _useHueEntertainmentAPI ) { log( "Entertainment API Group-ID", "%d", _groupId ); log( "Signal Timeout on Black", "%dms", _blackLightsTimeout ); log( "Brightness Min", "%f", _brightnessMin ); log( "Brightness Max", "%f", _brightnessMax ); log( "Brightness Threshold", "%f", _brightnessThreshold ); if( _groupId == 0 ) { Error(_log, "Disabling Entertainment API as Group-ID is invalid" ); _useHueEntertainmentAPI = false; } } isInitOK = initLeds(); } return isInitOK; } bool LedDevicePhilipsHue::setLights() { bool isInitOK = true; _lightIds.clear(); QJsonArray lArray; if( _useHueEntertainmentAPI && _groupId > 0 ) { lArray = getGroupLights( _groupId ); } if( lArray.empty() ) { if( _useHueEntertainmentAPI ) { _useHueEntertainmentAPI = false; Error(_log, "Group-ID [%u] is not usable - Entertainment API usage was disabled!", _groupId ); } lArray = _devConfig[ CONFIG_LIGHTIDS ].toArray(); } QString lightIDStr; if( !lArray.empty() ) { for (const QJsonValueRef id : lArray) { unsigned int lightId = id.toString().toUInt(); if( lightId > 0 ) { if(std::find(_lightIds.begin(), _lightIds.end(), lightId) == _lightIds.end()) { _lightIds.emplace_back(lightId); if(!lightIDStr.isEmpty()) { lightIDStr.append(", "); } lightIDStr.append(QString::number(lightId)); } } } std::sort( _lightIds.begin(), _lightIds.end() ); } unsigned int configuredLightsCount = static_cast(_lightIds.size()); log( "Light-IDs configured", "%d", configuredLightsCount ); if ( configuredLightsCount == 0 ) { this->setInError( "No light-IDs configured" ); isInitOK = false; } else { log( "Light-IDs", "%s", QSTRING_CSTR( lightIDStr ) ); isInitOK = updateLights( getLightMap() ); } return isInitOK; } bool LedDevicePhilipsHue::initLeds() { bool isInitOK = false; if ( !this->isInError() ) { if( setLights() ) { if( _useHueEntertainmentAPI ) { _groupName = getGroupName( _groupId ); _devConfig["latchTime"] = 0; _devConfig["host"] = _hostname; _devConfig["sslport"] = API_SSL_SERVER_PORT; _devConfig["servername"] = API_SSL_SERVER_NAME; _devConfig["rewriteTime"] = static_cast( STREAM_REWRITE_TIME.count() ); _devConfig["psk"] = _devConfig[ CONFIG_CLIENTKEY ].toString(); _devConfig["psk_identity"] = _devConfig[ CONFIG_USERNAME ].toString(); _devConfig["seed_custom"] = API_SSL_SEED_CUSTOM; _devConfig["retry_left"] = STREAM_CONNECTION_RETRYS; _devConfig["hs_attempts"] = STREAM_SSL_HANDSHAKE_ATTEMPTS; _devConfig["hs_timeout_min"] = _handshake_timeout_min; _devConfig["hs_timeout_max"] = _handshake_timeout_max; _devConfig["read_timeout"] = _ssl_read_timeout; isInitOK = ProviderUdpSSL::init( _devConfig ); if( isInitOK ) { if ( _blackLightsTimer == nullptr ) { _blackLightsTimer = new QTimer(this); connect( _blackLightsTimer, &QTimer::timeout, this, &LedDevicePhilipsHue::noSignalTimeout ); } } } else { // adapt latchTime to count of user lightIds (bridge 10Hz max overall) setLatchTime( static_cast( 100 * getLightsCount() ) ); isInitOK = true; } _isInitLeds = true; } else { isInitOK = false; } } return isInitOK; } bool LedDevicePhilipsHue::updateLights(const QMap &map) { bool isInitOK = true; // search user lightid inside map and create light if found _lights.clear(); if(!_lightIds.empty()) { unsigned int ledidx = 0; _lights.reserve(_lightIds.size()); for(const auto id : _lightIds) { if (map.contains(id)) { _lights.emplace_back(_log, id, map.value(id), ledidx); } else { Warning(_log, "Configured light-ID %d is not available at this bridge", id ); } ledidx++; } } unsigned int lightsCount = static_cast(_lights.size()); setLightsCount( lightsCount ); if( lightsCount == 0 ) { Error(_log, "No usable lights found!" ); isInitOK = false; } return isInitOK; } bool LedDevicePhilipsHue::openStream() { bool isInitOK = false; start_retry_left = 3; bool streamState = getStreamGroupState(); if ( !this->isInError() ) { // stream is already active if( streamState ) { // if same owner stop stream if(isStreamOwner(_streamOwner)) { Debug(_log, "Group: \"%s\" [%u] is in use, try to stop stream", QSTRING_CSTR(_groupName), _groupId ); if( stopStream() ) { Debug(_log, "Stream successful stopped"); //Restore Philips Hue devices state restoreState(); isInitOK = startStream(); } else { Error(_log, "Group: \"%s\" [%u] couldn't stop by user: \"%s\" - Entertainment API not usable", QSTRING_CSTR( _groupName ), _groupId, QSTRING_CSTR( _streamOwner ) ); } } else { Error(_log, "Group: \"%s\" [%u] is in use and owned by other user: \"%s\" - Entertainment API not usable", QSTRING_CSTR(_groupName), _groupId, QSTRING_CSTR(_streamOwner)); } } else { isInitOK = startStream(); } } if( isInitOK ) { // open UDP SSL Connection isInitOK = ProviderUdpSSL::initNetwork(); if( isInitOK ) { Info(_log, "Philips Hue Entertainment API successful connected! Start Streaming." ); _allLightsBlack = true; noSignalDetection(); } else { Error(_log, "Philips Hue Entertainment API not connected!" ); } } else { Error(_log, "Philips Hue Entertainment API could not be initialised!" ); } return isInitOK; } bool LedDevicePhilipsHue::startStream() { Debug(_log, "Start entertainment stream"); bool rc = false; if ( setStreamGroupState( true ) ) { start_retry_left = 3; rc = true; } else { if ( !this->isInError() ) { QThread::msleep(500); bool streamState = getStreamGroupState(); if ( !this->isInError() ) { // stream is not active if( !streamState ) { rc = ( start_retry_left-- > 0 ) ? startStream() : false; } } } } return rc; } bool LedDevicePhilipsHue::stopStream() { ProviderUdpSSL::closeSSLConnection(); bool rc = false; if ( setStreamGroupState( false ) ) { stop_retry_left = 3; rc = true; } else { if ( !this->isInError() ) { QThread::msleep(500); bool streamState = getStreamGroupState(); if ( !this->isInError() ) { // stream is still active if( streamState ) { rc = (stop_retry_left-- > 0) ? stopStream() : false; } } } } return rc; } bool LedDevicePhilipsHue::getStreamGroupState() { QJsonDocument doc = getGroupState( _groupId ); if ( !this->isInError() ) { QJsonObject obj = doc.object()[ API_STREAM ].toObject(); if( obj.isEmpty() ) { this->setInError( "no Streaming Infos in Group found" ); } else { _streamOwner = obj.value( API_STREAM_OWNER ).toString(); bool streamState = obj.value( API_STREAM_ACTIVE ).toBool(); return streamState; } } return false; } bool LedDevicePhilipsHue::setStreamGroupState(bool state) { QString active = state ? API_STREAM_ACTIVE_VALUE_TRUE : API_STREAM_ACTIVE_VALUE_FALSE; QJsonDocument doc = setGroupState( _groupId, state ); QVariant rsp = doc.toVariant(); QVariantList list = rsp.toList(); if ( !list.isEmpty() ) { QVariantMap map = list.first().toMap(); if ( !map.contains( API_SUCCESS ) ) { this->setInError( QString("set stream to %1: Neither error nor success contained in Bridge response...").arg( active ) ); } else { //Check original Hue response {"success":{"/groups/groupID/stream/active":activeYesNo}} QString valueName = QString( API_STREAM_RESPONSE_FORMAT ).arg( API_GROUPS ).arg( _groupId ).arg( API_STREAM, API_STREAM_ACTIVE ); if(!map.value( API_SUCCESS ).toMap().value( valueName ).isValid()) { //Workaround //Check diyHue response {"success":{"/groups/groupID/stream":{"active":activeYesNo}}} QString diyHueValueName = QString( "/%1/%2/%3" ).arg( API_GROUPS ).arg( _groupId ).arg( API_STREAM); QJsonObject diyHueActiveState = map.value( API_SUCCESS ).toMap().value( diyHueValueName ).toJsonObject(); if( diyHueActiveState.isEmpty() ) { this->setInError( QString("set stream to %1: Bridge response is not Valid").arg( active ) ); } else { bool groupStreamState = diyHueActiveState[API_STREAM_ACTIVE].toBool(); return ( groupStreamState == state ); } } else { bool groupStreamState = map.value( API_SUCCESS ).toMap().value( valueName ).toBool(); return ( groupStreamState == state ); } } } return false; } QByteArray LedDevicePhilipsHue::prepareStreamData() const { QByteArray msg; msg.reserve(static_cast(sizeof(HEADER) + sizeof(PAYLOAD_PER_LIGHT) * _lights.size())); msg.append(reinterpret_cast(HEADER), sizeof(HEADER)); for (const PhilipsHueLight& light : _lights) { CiColor lightC = light.getColor(); quint64 R = lightC.x * 0xffff; quint64 G = lightC.y * 0xffff; quint64 B = (lightC.x || lightC.y) ? lightC.bri * 0xffff : 0; unsigned int id = light.getId(); const uint8_t payload[] = { 0x00, 0x00, static_cast(id), static_cast((R >> 8) & 0xff), static_cast(R & 0xff), static_cast((G >> 8) & 0xff), static_cast(G & 0xff), static_cast((B >> 8) & 0xff), static_cast(B & 0xff) }; msg.append(reinterpret_cast(payload), sizeof(payload)); } return msg; } void LedDevicePhilipsHue::stop() { stopBlackTimeoutTimer(); LedDevicePhilipsHueBridge::stop(); } int LedDevicePhilipsHue::open() { int retval = 0; _isDeviceReady = true; return retval; } int LedDevicePhilipsHue::close() { int retval = -1; retval = LedDevicePhilipsHueBridge::close(); return retval; } bool LedDevicePhilipsHue::switchOn() { Debug(_log, ""); bool rc = false; if ( _isOn ) { rc = true; } else { if ( _isEnabled && _isDeviceInitialised ) { storeState(); if ( _useHueEntertainmentAPI) { if ( openStream() ) { _isOn = true; rc = true; } } else if ( powerOn() ) { _isOn = true; rc = true; } } } return rc; } bool LedDevicePhilipsHue::switchOff() { Debug(_log, ""); this->stopBlackTimeoutTimer(); stop_retry_left = 3; if (_useHueEntertainmentAPI) { stopStream(); } return LedDevicePhilipsHueBridge::switchOff(); } int LedDevicePhilipsHue::write(const std::vector & ledValues) { // lights will be empty sometimes if( _lights.empty() ) { return -1; } // more lights then leds, stop always if( ledValues.size() < getLightsCount() ) { Error(_log, "More light-IDs configured than leds, each light-ID requires one led!" ); return -1; } writeSingleLights( ledValues ); if( _useHueEntertainmentAPI && !noSignalDetection() && _isInitLeds ) { writeStream(); } return 0; } void LedDevicePhilipsHue::noSignalTimeout() { Debug(_log, "No Signal (timeout: %dms), only black color detected - stop stream for \"%s\" [%u]", _blackLightsTimer->remainingTime(), QSTRING_CSTR(_groupName), _groupId ); _stopConnection = true; switchOff(); } void LedDevicePhilipsHue::stopBlackTimeoutTimer() { if ( _blackLightsTimer != nullptr && _blackLightsTimer->isActive() ) { _blackLightsTimer->stop(); } } bool LedDevicePhilipsHue::noSignalDetection() { if( _allLightsBlack && _switchOffOnBlack) { if( !_stopConnection && _isInitLeds ) { if ( !_blackLightsTimer->isActive() ) { DebugIf( verbose, _log, "No Signal detected - timeout timer started" ); _blackLightsTimer->start( ( _blackLightsTimeout + 500 ) ); } } } else { if ( _blackLightsTimer->isActive() ) { DebugIf( verbose, _log, "Signal detected - timeout timer stopped" ); this->stopBlackTimeoutTimer(); } if( _stopConnection ) { _stopConnection = false; Debug(_log, "Signal detected - restart stream for \"%s\" [%u]", QSTRING_CSTR(_groupName), _groupId ); switchOn(); } } return _stopConnection; } int LedDevicePhilipsHue::writeSingleLights(const std::vector& ledValues) { // Iterate through lights and set colors. unsigned int idx = 0; unsigned int blackCounter = 0; for ( PhilipsHueLight& light : _lights ) { // Get color. ColorRgb color = ledValues.at(idx); // Scale colors from [0, 255] to [0, 1] and convert to xy space. CiColor xy = CiColor::rgbToCiColor(color.red / 255.0, color.green / 255.0, color.blue / 255.0, light.getColorSpace()); if( _useHueEntertainmentAPI ) { this->setColor(light, xy); if( xy.bri >= 0.0 && xy.bri <= _brightnessThreshold ) { blackCounter++; } } else { if ( _switchOffOnBlack && xy.bri == 0.0 ) { this->setOnOffState( light, false ); } else { // Write color if color has been changed. this->setState( light, true, xy ); } } idx++; } if( _useHueEntertainmentAPI ) { _allLightsBlack = ( blackCounter == _lightsCount ); } return 0; } void LedDevicePhilipsHue::writeStream() { QByteArray streamData = prepareStreamData(); writeBytes( static_cast(streamData.size()), reinterpret_cast( streamData.data() ) ); } void LedDevicePhilipsHue::setOnOffState(PhilipsHueLight& light, bool on) { if (light.getOnOffState() != on) { light.setOnOffState( on ); QString state = on ? API_STATE_VALUE_TRUE : API_STATE_VALUE_FALSE; setLightState( light.getId(), QString("{\"%1\": %2 }").arg( API_STATE_ON, state ) ); } } void LedDevicePhilipsHue::setTransitionTime(PhilipsHueLight& light) { if (light.getTransitionTime() != _transitionTime) { light.setTransitionTime( _transitionTime ); setLightState( light.getId(), QString("{\"%1\": %2 }").arg( API_TRANSITIONTIME ).arg( _transitionTime ) ); } } void LedDevicePhilipsHue::setColor(PhilipsHueLight& light, CiColor& color) { if ( light.getColor() != 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 ); setLightState( light.getId(), stateCmd ); } else { color.bri = ( qMin( _brightnessMax, _brightnessFactor * qMax( _brightnessMin, color.bri ) ) ); //if(color.x == 0.0 && color.y == 0.0) color = colorBlack; } light.setColor( color ); } } void LedDevicePhilipsHue::setState(PhilipsHueLight& light, bool on, const CiColor& color) { QString stateCmd; if ( light.getOnOffState() != on ) { light.setOnOffState( on ); QString state = on ? API_STATE_VALUE_TRUE : API_STATE_VALUE_FALSE; stateCmd += QString("\"%1\":%2,").arg( API_STATE_ON, state ); } if ( light.getTransitionTime() != _transitionTime ) { light.setTransitionTime( _transitionTime ); stateCmd += QString("\"%1\":%2,").arg( API_TRANSITIONTIME ).arg( _transitionTime ); } const int bri = qRound( qMin( 254.0, _brightnessFactor * qMax( 1.0, color.bri * 254.0 ) ) ); if ( light.getColor() != color ) { light.setColor( color ); 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 ); } if ( !stateCmd.isEmpty() ) { setLightState( light.getId(), "{" + stateCmd + "}" ); } } void LedDevicePhilipsHue::setLightsCount( unsigned int lightsCount ) { _lightsCount = lightsCount; } bool LedDevicePhilipsHue::powerOn() { if ( _isDeviceReady) { // TODO: Question: Not clear, if setstream state on will turn of the lights // or do they need to be turned off classically? if ( !_useHueEntertainmentAPI ) { //Switch off Philips Hue devices physically for ( PhilipsHueLight& light : _lights ) { setOnOffState( light, true ); } } } return true; } bool LedDevicePhilipsHue::powerOff() { if ( _isDeviceReady) { // TODO: Question: Not clear, if setstream state off will turn of the lights // or do they need to be turned off classically if ( !_useHueEntertainmentAPI ) { //Switch off Philips Hue devices physically for ( PhilipsHueLight& light : _lights ) { setOnOffState( light, false ); } } } return true; } bool LedDevicePhilipsHue::storeState() { bool rc = true; if ( _isRestoreOrigState ) { if( !_lightIds.empty() ) { for ( PhilipsHueLight& light : _lights ) { QJsonObject values = getLightState(light.getId()).object(); light.saveOriginalState(values); } } } return rc; } bool LedDevicePhilipsHue::restoreState() { bool rc = true; if ( _isRestoreOrigState ) { // Restore device's original state if( !_lightIds.empty() ) { for ( PhilipsHueLight& light : _lights ) { setLightState( light.getId(),light.getOriginalState() ); } } } return rc; } QJsonObject LedDevicePhilipsHue::discover(const QJsonObject& /*params*/) { QJsonObject devicesDiscovered; devicesDiscovered.insert("ledDeviceType", _activeDeviceType ); QJsonArray deviceList; // Discover Devices SSDPDiscover discover; discover.skipDuplicateKeys(false); 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() ); return devicesDiscovered; } QJsonObject LedDevicePhilipsHue::getProperties(const QJsonObject& params) { QJsonObject properties; // Get Phillips-Bridge device properties QString host = params["host"].toString(""); if ( !host.isEmpty() ) { QString username = params["user"].toString(""); QString filter = params["filter"].toString(""); // Resolve hostname and port (or use default API port) QStringList addressparts = QStringUtils::split(host,":", QStringUtils::SplitBehavior::SkipEmptyParts); QString apiHost = addressparts[0]; int apiPort; if ( addressparts.size() > 1 ) { apiPort = addressparts[1].toInt(); } else { apiPort = API_DEFAULT_PORT; } initRestAPI(apiHost, apiPort, username); _restApi->setPath(filter); // Perform request httpResponse response = _restApi->get(); if ( response.error() ) { Warning (_log, "%s get properties failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); } // Perform request properties.insert("properties", response.getBody().object()); } return properties; } void LedDevicePhilipsHue::identify(const QJsonObject& params) { Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() ); QJsonObject properties; // Identify Phillips-Bridge device QString host = params["host"].toString(""); if ( !host.isEmpty() ) { QString username = params["user"].toString(""); int lightId = params["lightId"].toInt(0); // Resolve hostname and port (or use default API port) QStringList addressparts = QStringUtils::split(host,":", QStringUtils::SplitBehavior::SkipEmptyParts); QString apiHost = addressparts[0]; int apiPort; if ( addressparts.size() > 1 ) { apiPort = addressparts[1].toInt(); } else { apiPort = API_DEFAULT_PORT; } initRestAPI(apiHost, apiPort, username); 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, API_STATE_VALUE_TRUE ); stateCmd += QString("\"%1\":\"%2\"").arg( "alert", "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())); } } }