// Local-Hyperion includes #include "LedDevicePhilipsHue.h" #include #include #include "qendian.h" #include // mDNS discover #ifdef ENABLE_MDNS #include #include #endif #include // Constants namespace { bool verbose = false; bool verbose3 = false; // Configuration settings const char CONFIG_HOST[] = "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_API_V2[] = "useAPIv2"; const char CONFIG_USE_HUE_ENTERTAINMENT_API[] = "useEntertainmentAPI"; const char CONFIG_groupId[] = "groupId"; const char CONFIG_VERBOSE[] = "verbose"; // Philips Hue OpenAPI URLs const int API_DEFAULT_PORT = -1; //Use default port per communication scheme const char API_ROOT[] = "/"; const char API_BASE_PATH_V1[] = "api"; const char API_BASE_PATH_V2[] = "/clip/v2/resource"; const char API_AUTH_PATH_V1[] = "auth/v1"; const char API_RESOURCE_CONFIG[] = "config"; const char API_RESOURCE_LIGHTS[] = "lights"; const char API_RESOURCE_GROUPS[] = "groups"; //V2 const char API_RESOURCE_DEVICE[] = "device"; const char API_RESOURCE_LIGHT[] = "light"; const char API_RESOURCE_ENTERTAINMENT[] = "entertainment"; const char API_RESOURCE_ENTERTAINMENT_CONFIGURATION[] = "entertainment_configuration"; // Device Data elements const char DEV_DATA_BRIDGEID[] = "bridgeid"; const char DEV_DATA_SOFTWAREVERSION[] = "swversion"; const char DEV_DATA_APIVERSION[] = "apiversion"; const char DEV_DATA_METADATA[] = "metadata"; const char DEV_DATA_NAME[] = "name"; const char DEV_DATA_ARCHETYPE[] = "archetype"; const char DEV_DATA_PRODUCTDATA[] = "product_data"; const char DEV_DATA_PRODUCT[] = "product_name"; const char DEV_DATA_MODEL[] = "model_id"; const char DEV_DATA_PRODUCT_V1[] = "productname"; const char DEV_DATA_MODEL_V1[] = "modelid"; // List of Group / Stream Information const char API_GROUP_NAME[] = "name"; const char API_GROUP_TYPE[] = "type"; const char API_GROUP_TYPE_ENTERTAINMENT_V1[] = "Entertainment"; const char API_GROUP_TYPE_ENTERTAINMENT_CONFIGURATION[] = "entertainment_configuration"; const char API_OWNER[] = "owner"; 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_RESPONSE_FORMAT[] = "/%1/%2/%3/%4"; const char API_STREAM_STATUS[] = "status"; const char API_STREAM_ACTIVE_V2[] = "active_streamer"; const char API_LIGHT_SERVICES[] = "light_services"; const char API_CHANNELS[] = "channels"; const char API_RID[] = "rid"; // List of light resources const char API_LIGTH_ID[] = "lightId"; const char API_LIGTH_ID_v1[] = "lightId_v1"; const char API_COLOR[] = "color"; const char API_GRADIENT[] = "gradient"; const char API_XY_COORDINATES[] = "xy"; const char API_X_COORDINATE[] = "x"; const char API_Y_COORDINATE[] = "y"; const char API_BRIGHTNESS[] = "bri"; const char API_TRANSITIONTIME[] = "transitiontime"; const char API_DYNAMICS[] = "dynamics"; const char API_DURATION[] = "duration"; const char API_DIMMING[] = "dimming"; // List of State Information const char API_STATE[] = "state"; const char API_STATE_ON[] = "on"; // List of Action Information const char API_ACTION[] = "action"; const char API_ACTION_START[] = "start"; const char API_ACTION_STOP[] = "stop"; const char API_ACTION_BREATHE[] = "breathe"; const char API_ALERT[] = "alert"; const char API_SELECT[] = "select"; // List of Data/Error Information const char API_DATA[] = "data"; const char API_ERROR[] = "error"; const char API_ERROR_ADDRESS[] = "address"; const char API_ERROR_DESCRIPTION[] = "description"; const char API_ERROR_TYPE[] = "type"; const char API_ERRORS[] = "errors"; // List of Success Information const char API_SUCCESS[] = "success"; // List of custom HTTP Headers const char HTTP_HEADER_APPLICATION_KEY[] = "hue-application-key"; // 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 char API_SSL_CA_CERTIFICATE_RESSOURCE[] = ":/philips_hue_ca.pem"; const int STREAM_CONNECTION_RETRYS = 20; const int STREAM_SSL_HANDSHAKE_ATTEMPTS = 5; const int SSL_CIPHERSUITES[2] = { MBEDTLS_TLS_PSK_WITH_AES_128_GCM_SHA256, 0 }; const int DEV_FIRMWAREVERSION_APIV2 = 1948086000; //Enable rewrites that Hue-Bridge does not close the connection ("After 10 seconds of no activity the connection is closed automatically, and status is set back to inactive.") constexpr std::chrono::milliseconds STREAM_REWRITE_TIME{5000}; //Streaming message header and payload definition const uint8_t HEADER[] = { 'H', 'u', 'e', 'S', 't', 'r', 'e', 'a', 'm', //protocol 0x01, 0x00, //version 1.0 0x01, //sequence number 1 0x00, 0x00, //Reserved write 0’s 0x00, // 0x00 = RGB; 0x01 = XY Brightness 0x00, // Reserved, write 0’s }; const uint8_t PAYLOAD_PER_LIGHT[] = { 0x01, 0x00, 0x06, //light ID //color: 16 bpc 0xff, 0xff, //Red 0xff, 0xff, //Green 0xff, 0xff, //Blue }; //API v2 - Streaming message header and payload definition const uint8_t HEADER_V2[] = { 'H', 'u', 'e', 'S', 't', 'r', 'e', 'a', 'm', //protocol 0x02, 0x00, //version 2.0 0x01, //sequence number 1 0x00, 0x00, //Reserved write 0’s 0x00, // 0x00 = RGB; 0x01 = XY Brightness 0x00, // Reserved }; const char* ENTERTAINMENT_ID[36]; const uint8_t PAYLOAD_PER_CHANNEL_V2[] = { 0xff, //channel id 0xff, 0xff, 0xff, 0xff, 0xff, 0xff //color }; } //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, bool candyGamma) { double cx; double cy; double bri; if (red + green + blue > 0) { // Apply gamma correction. double r = red; double g = green; double b = blue; if (candyGamma) { r = (red > 0.04045) ? pow((red + 0.055) / (1.0 + 0.055), 2.4) : (red / 12.92); g = (green > 0.04045) ? pow((green + 0.055) / (1.0 + 0.055), 2.4) : (green / 12.92); 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) { 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) , _useEntertainmentAPI(false) , _isAPIv2Ready(false) , _isDiyHue(false) , _api_major(0) , _api_minor(0) , _api_patch(0) , _isHueEntertainmentReady(false) { #ifdef ENABLE_MDNS QMetaObject::invokeMethod(&MdnsBrowser::getInstance(), "browseForServiceType", Qt::QueuedConnection, Q_ARG(QByteArray, MdnsServiceRegister::getServiceType(_activeDeviceType))); #endif } LedDevicePhilipsHueBridge::~LedDevicePhilipsHueBridge() { delete _restApi; _restApi = nullptr; } bool LedDevicePhilipsHueBridge::init(const QJsonObject &deviceConfig) { DebugIf( verbose, _log, "deviceConfig: [%s]", QJsonDocument(_devConfig).toJson(QJsonDocument::Compact).constData() ); bool isInitOK = false; //Set hostname as per configuration and default port _hostName = deviceConfig[CONFIG_HOST].toString(); _apiPort = deviceConfig[CONFIG_PORT].toInt(); _authToken = deviceConfig[CONFIG_USERNAME].toString(); Debug(_log, "Hostname/IP: %s", QSTRING_CSTR(_hostName) ); _useApiV2 = deviceConfig[CONFIG_USE_HUE_API_V2].toBool(false); Debug(_log, "Use Hue API v2: %s", _useApiV2 ? "Yes" : "No" ); if( _useEntertainmentAPI ) { setLatchTime( 0); _devConfig["sslport"] = API_SSL_SERVER_PORT; _devConfig["servername"] = API_SSL_SERVER_NAME; _devConfig["psk"] = _devConfig[ CONFIG_CLIENTKEY ].toString(); if (_useApiV2) { // psk_identity is to be set later when application-id was resolved _devConfig["psk_identity"] = ""; } else { _devConfig["psk_identity"] = _authToken; } _devConfig["seed_custom"] = API_SSL_SEED_CUSTOM; _devConfig["retry_left"] = STREAM_CONNECTION_RETRYS; _devConfig["hs_attempts"] = STREAM_SSL_HANDSHAKE_ATTEMPTS; _devConfig["hs_timeout_min"] = 600; _devConfig["hs_timeout_max"] = 1000; _port = API_SSL_SERVER_PORT; isInitOK = ProviderUdpSSL::init(_devConfig); } else { isInitOK = LedDevice::init(_devConfig); // NOLINT } return isInitOK; } bool LedDevicePhilipsHueBridge::openRestAPI() { bool isInitOK {true}; if (_restApi == nullptr) { _restApi = new ProviderRestApi(_address.toString(), _apiPort); _restApi->setLogger(_log); } else { _restApi->setHost(_address.toString()); _restApi->setPort(_apiPort); } if (_apiPort == 0 || _apiPort == 80 || _apiPort == 443) { _apiPort = API_DEFAULT_PORT; _restApi->setPort(_apiPort); } _restApi->setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); return isInitOK; } bool LedDevicePhilipsHueBridge::checkApiError(const QJsonDocument &response, bool supressError) { bool apiError = false; QString errorReason; DebugIf(verbose, _log, "Reply: [%s]", response.toJson(QJsonDocument::Compact).constData()); if (_useApiV2) { QJsonObject obj = response.object(); if (obj.contains(API_ERRORS)) { QJsonArray errorList = obj.value(API_ERRORS).toArray(); if (!errorList.isEmpty()) { QStringList errors; for (const QJsonValue &error : qAsConst(errorList)) { QString errorString = error.toObject()[API_ERROR_DESCRIPTION].toString(); if (!errorString.contains("may not have effect")) { errors << errorString; } } if (!errors.isEmpty()) { errorReason = errors.join(","); apiError = true; } } } } else { QJsonArray responseList = response.array(); if (!responseList.isEmpty()) { QJsonObject respose = responseList.first().toObject(); if (respose.contains(API_ERROR)) { QJsonObject error = respose.value(API_ERROR).toObject(); int errorType = error.value(API_ERROR_TYPE).toInt(); QString errorDesc = error.value(API_ERROR_DESCRIPTION).toString(); QString errorAddress = error.value(API_ERROR_ADDRESS).toString(); if( errorType != 901 ) { errorReason = QString ("(%1) %2, Resource:%3").arg(errorType).arg(errorDesc, errorAddress); apiError = true; } } } } if (apiError) { if (!supressError) { this->setInError(errorReason); } else { Warning(_log, "Suppresing error: %s", QSTRING_CSTR(errorReason)); } } return apiError; } int LedDevicePhilipsHueBridge::open() { int retval = -1; _isDeviceReady = false; if (NetUtils::resolveHostToAddress(_log, _hostName, _address)) { if ( openRestAPI() ) { QJsonDocument bridgeDetails = retrieveBridgeDetails(); if ( !bridgeDetails.isEmpty() ) { setBridgeDetails(bridgeDetails, true); if (_useApiV2) { if ( configureSsl() ) { if (retrieveApplicationId()) { setPSKidentity(_applicationID); } } } else { if (_isAPIv2Ready) { Warning(_log,"Your Hue Bridge supports a newer API. Reconfigure your device in Hyperion to benefit from new features."); } } if (!isInError() ) { setBaseApiEnvironment(_useApiV2); if (initLightsMap() && initDevicesMap() && initEntertainmentSrvsMap()) { if ( _useEntertainmentAPI ) { if (initGroupsMap()) { // 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( _useEntertainmentAPI ) { retval = ProviderUdpSSL::close(); } return retval; } bool LedDevicePhilipsHueBridge::configureSsl() { _restApi->setAlternateServerIdentity(_deviceBridgeId); if (_isDiyHue) { _restApi->acceptSelfSignedCertificates(true); } bool success = _restApi->setCaCertificate(API_SSL_CA_CERTIFICATE_RESSOURCE); if (!success) { this->setInError ( "Failed to configure Hue Bridge for SSL", false ); } return success; } 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::retrieveBridgeDetails() { QJsonDocument bridgeDetails; if ( openRestAPI() ) { setBaseApiEnvironment(false, API_BASE_PATH_V1); bridgeDetails = get( API_RESOURCE_CONFIG ); } return bridgeDetails; } bool LedDevicePhilipsHueBridge::retrieveApplicationId() { bool rc {false}; setBaseApiEnvironment(true, API_ROOT); _restApi->setPath(API_AUTH_PATH_V1); httpResponse response = _restApi->get(); if ( !response.error() ) { _applicationID = response.getHeader("hue-application-id"); rc = true; } else { QString errorReason = QString("Failed to get application-id from Hue Bridge, error: '%1'").arg(response.getErrorReason()); this->setInError ( errorReason, false ); } return rc; } QJsonDocument LedDevicePhilipsHueBridge::retrieveDeviceDetails(const QString& deviceId ) { QStringList resourcePath; if (_useApiV2) { resourcePath << API_RESOURCE_DEVICE; if (!deviceId.isEmpty()) { resourcePath << deviceId; } } return get( resourcePath ); } QJsonDocument LedDevicePhilipsHueBridge::retrieveLightDetails(const QString& lightId ) { QStringList resourcePath; if (_useApiV2) { resourcePath << API_RESOURCE_LIGHT; if (!lightId.isEmpty()) { resourcePath << lightId; } } else { resourcePath << API_RESOURCE_LIGHTS; if (!lightId.isEmpty()) { resourcePath << lightId << API_STATE; } } return get( resourcePath ); } QJsonDocument LedDevicePhilipsHueBridge::retrieveGroupDetails(const QString& groupId ) { QStringList resourcePath; if (_useApiV2) { resourcePath << API_RESOURCE_ENTERTAINMENT_CONFIGURATION; if (!groupId.isEmpty()) { resourcePath << groupId; } } else { resourcePath << API_RESOURCE_GROUPS; if (!groupId.isEmpty()) { resourcePath << groupId; } } return get( resourcePath ); } QJsonDocument LedDevicePhilipsHueBridge::retrieveEntertainmentSrvDetails(const QString& entertainmentID ) { QStringList resourcePath; if (_useApiV2) { resourcePath << API_RESOURCE_ENTERTAINMENT; if (!entertainmentID.isEmpty()) { resourcePath << entertainmentID; } } return get( resourcePath ); } bool LedDevicePhilipsHueBridge::isApiEntertainmentReady(const QString& apiVersion) { bool ready {false}; QStringList apiVersionParts = QStringUtils::split(apiVersion,".", 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) ) { ready = true; } } Debug(_log,"API version [%s] %s Entertainment API ready", QSTRING_CSTR(apiVersion), ready ? "is" : "is not" ); return ready; } bool LedDevicePhilipsHueBridge::isAPIv2Ready(int swVersion) { bool ready {true}; if (swVersion < DEV_FIRMWAREVERSION_APIV2) { ready = false; } Debug(_log,"Firmware version [%d] %s API v2 ready", swVersion, ready ? "is" : "is not" ); return ready; } void LedDevicePhilipsHueBridge::setBaseApiEnvironment(bool apiV2, const QString& path) { if ( _restApi != nullptr ) { QStringList basePath; if (apiV2) { _restApi->setScheme("https"); if (!path.isEmpty()) { basePath << path; } else { basePath << API_BASE_PATH_V2; } _restApi->setHeader(HTTP_HEADER_APPLICATION_KEY, _authToken.toUtf8()); } else { _restApi->setScheme("http"); if (!path.isEmpty()) { basePath << path; } else { //Base-path is api-path + authentication token (here username) basePath << API_BASE_PATH_V1 << _authToken; } } _restApi->setBasePath(basePath); DebugIf(verbose, _log,"New BasePath: %s", QSTRING_CSTR(_restApi->getBasePath())); } } bool LedDevicePhilipsHueBridge::initDevicesMap() { bool isInitOK = false; if ( !this->isInError() ) { QJsonDocument deviceDetails = retrieveDeviceDetails(); if ( !deviceDetails.isEmpty() ) { setDevicesMap( deviceDetails ); isInitOK = true; } } return isInitOK; } bool LedDevicePhilipsHueBridge::initLightsMap() { bool isInitOK = false; if ( !this->isInError() ) { QJsonDocument lightDetails = retrieveLightDetails(); if ( !lightDetails.isEmpty() ) { setLightsMap( lightDetails ); isInitOK = true; } } return isInitOK; } bool LedDevicePhilipsHueBridge::initGroupsMap() { bool isInitOK = false; if ( !this->isInError() ) { QJsonDocument groupDetails = retrieveGroupDetails(); if ( !groupDetails.isEmpty() ) { setGroupMap( groupDetails ); isInitOK = true; } } return isInitOK; } bool LedDevicePhilipsHueBridge::initEntertainmentSrvsMap() { bool isInitOK = false; if ( !this->isInError() ) { QJsonDocument entertainmentSrvDetails = retrieveEntertainmentSrvDetails(); if ( !entertainmentSrvDetails.isEmpty() ) { setEntertainmentSrvMap( entertainmentSrvDetails ); isInitOK = true; } } return isInitOK; } void LedDevicePhilipsHueBridge::setBridgeDetails(const QJsonDocument &doc, bool isLogging) { QJsonObject jsonConfigInfo = doc.object(); if ( verbose ) { std::cout << "jsonConfigInfo: [" << QJsonDocument(jsonConfigInfo).toJson(QJsonDocument::Compact).constData() << "]" << std::endl; } _deviceName = jsonConfigInfo[DEV_DATA_NAME].toString(); if (_deviceName.startsWith("DiyHue", Qt::CaseInsensitive)) { _isDiyHue = true; } _deviceModel = jsonConfigInfo[DEV_DATA_MODEL_V1].toString(); _deviceBridgeId = jsonConfigInfo[DEV_DATA_BRIDGEID].toString(); _deviceFirmwareVersion = jsonConfigInfo[DEV_DATA_SOFTWAREVERSION].toString().toInt(); _deviceAPIVersion = jsonConfigInfo[DEV_DATA_APIVERSION].toString(); _isHueEntertainmentReady = isApiEntertainmentReady(_deviceAPIVersion); _isAPIv2Ready = isAPIv2Ready(_deviceFirmwareVersion); if( _useEntertainmentAPI ) { DebugIf( !_isHueEntertainmentReady, _log, "Bridge is not Entertainment API Ready - Entertainment API usage was disabled!" ); _useEntertainmentAPI = _isHueEntertainmentReady; } if (isLogging) { log( "Bridge Name", "%s", QSTRING_CSTR( _deviceName )); log( "Bridge-ID", "%s", QSTRING_CSTR( _deviceBridgeId )); log( "Model", "%s", QSTRING_CSTR( _deviceModel )); log( "Firmware version", "%d", _deviceFirmwareVersion ); log( "API-Version", "%u.%u.%u", _api_major, _api_minor, _api_patch ); log( "API v2 ready", "%s", _isAPIv2Ready ? "Yes" : "No" ); log( "Entertainment ready", "%s", _isHueEntertainmentReady ? "Yes" : "No" ); log( "DIYHue", "%s", _isDiyHue ? "Yes" : "No" ); } } void LedDevicePhilipsHueBridge::setDevicesMap(const QJsonDocument &doc) { _devicesMap.clear(); if (_useApiV2) { QJsonArray devices = doc.array(); for (const QJsonValue &device : qAsConst(devices)) { QString deviceId = device.toObject().value("id").toString(); _devicesMap.insert(deviceId, device.toObject()); } } } void LedDevicePhilipsHueBridge::setLightsMap(const QJsonDocument &doc) { _lightsMap.clear(); if (_useApiV2) { QJsonArray lights = doc.array(); for (const QJsonValue &light : qAsConst(lights)) { QString lightId = light.toObject().value("id").toString(); _lightsMap.insert(lightId, light.toObject()); } } else { QJsonObject jsonLightsInfo = doc.object(); DebugIf(verbose, _log, "jsonLightsInfo: [%s]", QJsonDocument(jsonLightsInfo).toJson(QJsonDocument::Compact).constData() ); // Get all available light ids and their values QStringList keys = jsonLightsInfo.keys(); for ( int i = 0; i < keys.count(); ++i ) { QString key = keys.at(i); _lightsMap.insert(key, jsonLightsInfo[key].toObject()); } } _lightsCount = _lightsMap.count(); if ( _lightsCount == 0 ) { this->setInError( "No light-IDs found at the Philips Hue Bridge" ); } else { log( "Lights at Bridge found", "%d", _lightsCount ); } } void LedDevicePhilipsHueBridge::setGroupMap(const QJsonDocument &doc) { _groupsMap.clear(); if (_useApiV2) { QJsonArray groups = doc.array(); for (const QJsonValue &group : qAsConst(groups)) { QString groupId = group.toObject().value("id").toString(); _groupsMap.insert(groupId, group.toObject()); } } else { QJsonObject jsonGroupsInfo = doc.object(); DebugIf(verbose, _log, "jsonGroupsInfo: [%s]", QJsonDocument(jsonGroupsInfo).toJson(QJsonDocument::Compact).constData() ); // Get all available group ids and their values QStringList keys = jsonGroupsInfo.keys(); int _groupsCount = keys.size(); for ( int i = 0; i < _groupsCount; ++i ) { _groupsMap.insert( keys.at(i), jsonGroupsInfo.take(keys.at(i)).toObject() ); } } } void LedDevicePhilipsHueBridge::setEntertainmentSrvMap(const QJsonDocument &doc) { _entertainmentMap.clear(); if (_useApiV2) { QJsonArray entertainmentSrvs = doc.array(); for (const QJsonValue &entertainmentSrv : qAsConst(entertainmentSrvs)) { QString entertainmentSrvId = entertainmentSrv.toObject().value("id").toString(); _entertainmentMap.insert(entertainmentSrvId, entertainmentSrv.toObject()); } } } QMap LedDevicePhilipsHueBridge::getDevicesMap() const { return _devicesMap; } QMap LedDevicePhilipsHueBridge::getLightMap() const { return _lightsMap; } QMap LedDevicePhilipsHueBridge::getGroupMap() const { return _groupsMap; } QMap LedDevicePhilipsHueBridge::getEntertainmentMap() const { return _entertainmentMap; } QJsonObject LedDevicePhilipsHueBridge::getDeviceDetails(const QString& deviceId) { DebugIf( verbose, _log, "[%s]", QSTRING_CSTR(deviceId) ); return _devicesMap.value(deviceId); } QJsonObject LedDevicePhilipsHueBridge::getLightDetails(const QString& lightId) { DebugIf( verbose, _log, "[%s]", QSTRING_CSTR(lightId) ); return _lightsMap.value(lightId); } QJsonDocument LedDevicePhilipsHueBridge::setLightState(const QString& lightId, const QJsonObject& state) { DebugIf( verbose, _log, "[%s] ", QSTRING_CSTR(lightId) ); QStringList resourcePath; QJsonObject cmd; if (_useApiV2) { resourcePath << API_RESOURCE_LIGHT << lightId; cmd = state; } else { resourcePath << API_RESOURCE_LIGHTS << lightId << API_STATE; cmd = state; } return put(resourcePath, cmd); } QJsonDocument LedDevicePhilipsHueBridge::getGroupDetails(const QString& groupId) { DebugIf( verbose, _log, "[%s]", QSTRING_CSTR(groupId) ); return retrieveGroupDetails(groupId); } QString LedDevicePhilipsHueBridge::getGroupName(const QString& groupId ) const { QString groupName; if( _groupsMap.contains( groupId ) ) { QJsonObject group = _groupsMap.value( groupId ); groupName = group.value( API_GROUP_NAME ).toString().trimmed().replace("\"", ""); DebugIf( verbose, _log, "GroupId [%s]: GroupName: %s", QSTRING_CSTR(groupId), QSTRING_CSTR(groupName) ); } else { Error(_log, "Group ID %s does not exist on this bridge", QSTRING_CSTR(groupId) ); } return groupName; } QStringList LedDevicePhilipsHueBridge::getGroupLights(const QString& groupId) const { QStringList groupLights; // search user groupId inside _groupsMap and create light if found if( _groupsMap.contains( groupId ) ) { QJsonObject group = _groupsMap.value( groupId ); QString groupName = getGroupName( groupId ); QString type = group.value( API_GROUP_TYPE ).toString(); if( type == API_GROUP_TYPE_ENTERTAINMENT_V1 || type == API_GROUP_TYPE_ENTERTAINMENT_CONFIGURATION) { if (_useApiV2) { QJsonArray lightServices = group.value( API_LIGHT_SERVICES ).toArray(); for (const QJsonValue &light : qAsConst(lightServices)) { groupLights.append( light.toObject().value(API_RID).toString()); } } else { groupLights = group.value( API_RESOURCE_LIGHTS ).toVariant().toStringList(); } Info(_log, "Entertainment Group \"%s\" [%s] with %d lights found", QSTRING_CSTR(groupName), QSTRING_CSTR(groupId), groupLights.size() ); } else { Error(_log, "Group ID (%s)[%s] is not an entertainment group", QSTRING_CSTR(groupName), QSTRING_CSTR(groupId)); } } else { Error(_log, "Group ID [%s] does not exist on this bridge", QSTRING_CSTR(groupId) ); } return groupLights; } QJsonDocument LedDevicePhilipsHueBridge::setGroupState(const QString& groupId, bool state) { QStringList resourcePath; QJsonObject cmd; if (_useApiV2) { resourcePath << API_RESOURCE_ENTERTAINMENT_CONFIGURATION << groupId; cmd.insert(API_ACTION, state ? API_ACTION_START : API_ACTION_STOP); } else { resourcePath << API_RESOURCE_GROUPS << groupId; cmd.insert(API_STREAM, QJsonObject {{API_STREAM_ACTIVE, state }}); } return put(resourcePath, cmd); } QJsonObject LedDevicePhilipsHueBridge::getEntertainmentSrvDetails(const QString& deviceId) { DebugIf( verbose, _log, "getEntertainmentSrvDetails [%s]", QSTRING_CSTR(deviceId) ); QJsonObject details; for (const QJsonObject& entertainmentSrv : qAsConst(_entertainmentMap)) { QJsonObject owner = entertainmentSrv[API_OWNER].toObject(); if (owner[API_RID] == deviceId) { details = entertainmentSrv; break; } } return details; } int LedDevicePhilipsHueBridge::getGroupChannelsCount(const QString& groupId) const { int channelsCount {0}; // search user groupId inside _groupsMap and create light if found if( _groupsMap.contains( groupId ) ) { QJsonObject group = _groupsMap.value( groupId ); QString groupName = getGroupName( groupId ); QString type = group.value( API_GROUP_TYPE ).toString(); if(type == API_GROUP_TYPE_ENTERTAINMENT_CONFIGURATION) { if (_useApiV2) { QJsonArray channels = group.value( API_CHANNELS ).toArray(); channelsCount = channels.size(); } Info(_log, "Entertainment Group \"%s\" [%s] with %d channels found", QSTRING_CSTR(groupName), QSTRING_CSTR(groupId), channelsCount ); } else { Error(_log, "Group ID (%s)[%s] is not an entertainment group", QSTRING_CSTR(groupName), QSTRING_CSTR(groupId)); } } else { Error(_log, "Group ID [%s] does not exist on this bridge", QSTRING_CSTR(groupId) ); } return channelsCount; } QJsonDocument LedDevicePhilipsHueBridge::get(const QString& route) { return get(QStringList{route}); } QJsonDocument LedDevicePhilipsHueBridge::get(const QStringList& routeElements) { _restApi->setPath(routeElements); httpResponse response = _restApi->get(); if (response.error()) { if (routeElements.isEmpty() && ( response.getNetworkReplyError() == QNetworkReply::UnknownNetworkError || response.getNetworkReplyError() == QNetworkReply::ConnectionRefusedError || response.getNetworkReplyError() == QNetworkReply::RemoteHostClosedError || response.getNetworkReplyError() == QNetworkReply::OperationCanceledError )) { Warning(_log, "API request (Get): The Hue Bridge is not ready."); } else { QString errorReason = QString("API request (Get) failed with error: '%1'").arg(response.getErrorReason()); this->setInError ( errorReason ); } } else { if (!checkApiError(response.getBody())) { if (_useApiV2) { QJsonObject obj = response.getBody().object(); if (obj.contains(API_DATA)) { return QJsonDocument {obj.value(API_DATA).toArray()}; } } } } _restApi->clearPath(); return response.getBody(); } QJsonDocument LedDevicePhilipsHueBridge::put(const QStringList& routeElements, const QJsonObject& content, bool supressError) { _restApi->setPath(routeElements); httpResponse response = _restApi->put(content); if (response.error()) { QString errorReason = QString("API request (Put) failed with error: '%1'").arg(response.getErrorReason()); this->setInError ( errorReason ); } else { if (!checkApiError(response.getBody(), supressError)) { if (_useApiV2) { QJsonObject obj = response.getBody().object(); if (obj.contains(API_DATA)) { return QJsonDocument {obj.value(API_DATA).toArray()}; } } } } _restApi->clearPath(); return response.getBody(); } bool LedDevicePhilipsHueBridge::isStreamOwner(const QString &streamOwner) const { bool isOwner {false}; if (_useApiV2) { if ( streamOwner != "" && streamOwner == _applicationID) { isOwner = true; } } else { if ( streamOwner != "" && streamOwner == _authToken ) { isOwner = true; } } return isOwner; } QJsonArray LedDevicePhilipsHueBridge::discoverSsdp() { QJsonArray deviceList; SSDPDiscover discover; discover.skipDuplicateKeys(true); discover.setSearchFilter(SSDP_FILTER, SSDP_FILTER_HEADER); QString searchTarget = SSDP_ID; if (discover.discoverServices(searchTarget) > 0) { deviceList = discover.getServicesDiscoveredJson(); } return deviceList; } QJsonObject LedDevicePhilipsHueBridge::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 ); #else QString discoveryMethod("ssdp"); deviceList = discover(); #endif devicesDiscovered.insert("discoveryMethod", discoveryMethod); devicesDiscovered.insert("devices", deviceList); Debug(_log, "devicesDiscovered: [%s]", QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact).constData() ); return devicesDiscovered; } QJsonObject LedDevicePhilipsHueBridge::getProperties(const QJsonObject& params) { DebugIf(verbose, _log, "params: [%s]", QJsonDocument(params).toJson(QJsonDocument::Compact).constData()); QJsonObject properties; _hostName = params[CONFIG_HOST].toString(""); _apiPort = params[CONFIG_PORT].toInt(); _authToken = params[CONFIG_USERNAME].toString(""); Info(_log, "Get properties for %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName) ); if (NetUtils::resolveHostToAddress(_log, _hostName, _address)) { QJsonDocument bridgeDetails = retrieveBridgeDetails(); if ( !bridgeDetails.isEmpty() ) { setBridgeDetails(bridgeDetails); if ( openRestAPI() ) { _useApiV2 = _isAPIv2Ready; if (_authToken == API_RESOURCE_CONFIG) { properties.insert("properties", bridgeDetails.object()); properties.insert("isEntertainmentReady",_isHueEntertainmentReady); properties.insert("isAPIv2Ready",_isAPIv2Ready); } else { if (_useApiV2) { configureSsl(); } if (!isInError() ) { setBaseApiEnvironment(_useApiV2); QString filter = params["filter"].toString(""); _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())); } properties.insert("properties", response.getBody().object()); properties.insert("isEntertainmentReady",_isHueEntertainmentReady); properties.insert("isAPIv2Ready",_isAPIv2Ready); } } } } DebugIf(verbose, _log, "properties: [%s]", QJsonDocument(properties).toJson(QJsonDocument::Compact).constData()); } return properties; } QJsonObject LedDevicePhilipsHueBridge::addAuthorization(const QJsonObject& params) { Debug(_log, "params: [%s]", QJsonDocument(params).toJson(QJsonDocument::Compact).constData()); QJsonObject responseBody; // New Phillips-Bridge device client/application key _hostName = params[CONFIG_HOST].toString(""); _apiPort = params[CONFIG_PORT].toInt(); Info(_log, "Add authorized user for %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName) ); if (NetUtils::resolveHostToAddress(_log, _hostName, _address)) { QJsonDocument bridgeDetails = retrieveBridgeDetails(); if ( !bridgeDetails.isEmpty() ) { setBridgeDetails(bridgeDetails); if ( openRestAPI() ) { _useApiV2 = _isAPIv2Ready; if (_useApiV2) { configureSsl(); } if (!isInError() ) { setBaseApiEnvironment(_useApiV2, API_BASE_PATH_V1); QJsonObject clientKeyCmd{ {"devicetype", "hyperion#" + QHostInfo::localHostName()}, {"generateclientkey", true } }; _restApi->setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); httpResponse response = _restApi->post(clientKeyCmd); if (response.error()) { Warning(_log, "%s generation of authorization/client key failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); } else { if (!checkApiError(response.getBody(),false)) { responseBody = response.getBody().array().first().toObject().value("success").toObject(); } } } } } } return responseBody; } 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", "LCX002", "LLC020", "LST002" }; PhilipsHueLight::PhilipsHueLight(Logger* log, bool useApiV2, const QString& id, const QJsonObject& lightAttributes, int onBlackTimeToPowerOff, int onBlackTimeToPowerOn) : _log(log) , _useApiV2(useApiV2) , _id(id) , _on(false) , _transitionTime(0) , _color({ 0.0, 0.0, 0.0 }) , _hasColor(false) , _colorBlack({ 0.0, 0.0, 0.0 }) , _lastSendColorTime(0) , _lastBlackTime(-1) , _lastWhiteTime(-1) , _blackScreenTriggered(false) , _onBlackTimeToPowerOff(onBlackTimeToPowerOff) , _onBlackTimeToPowerOn(onBlackTimeToPowerOn) { if ( _useApiV2 ) { QJsonObject lightOwner = lightAttributes[API_OWNER].toObject(); _deviceId = lightOwner[API_RID].toString(); _gamutType = lightAttributes[API_COLOR].toObject()["gamut_type"].toString(); } else { _name = lightAttributes[DEV_DATA_NAME].toString().trimmed().replace("\"", ""); _model = lightAttributes[DEV_DATA_MODEL_V1].toString(); _product = lightAttributes[DEV_DATA_PRODUCT_V1].toString(); // Find id in the sets and set the appropriate color space. if (GAMUT_A_MODEL_IDS.find(_model) != GAMUT_A_MODEL_IDS.end()) { _gamutType = "A"; } else if (GAMUT_B_MODEL_IDS.find(_model) != GAMUT_B_MODEL_IDS.end()) { _gamutType = "B"; } else if (GAMUT_C_MODEL_IDS.find(_model) != GAMUT_C_MODEL_IDS.end()) { _gamutType = "C"; } else { _gamutType = ""; } } if (_gamutType.isEmpty()) { Warning(_log, "Light \"%s\" [%s], did not recognize model [%s]", QSTRING_CSTR(_name), QSTRING_CSTR(id), QSTRING_CSTR(_model)); } // Set the appropriate color space. if (_gamutType == "A") { _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 (_gamutType == "B") { _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 (_gamutType == "C") { _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 { _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}; } } void PhilipsHueLight::setDeviceDetails(const QJsonObject& details) { if (!details.isEmpty()) { QJsonObject metaData = details[DEV_DATA_METADATA].toObject(); _name = metaData[DEV_DATA_NAME].toString(); _archeType = metaData[DEV_DATA_ARCHETYPE].toString(); QJsonObject productData = details[DEV_DATA_PRODUCTDATA].toObject(); _product = productData[DEV_DATA_PRODUCT].toString(); _model = productData[DEV_DATA_MODEL].toString(); } } void PhilipsHueLight::setEntertainmentSrvDetails(const QJsonObject& details) { if (!details.isEmpty()) { QJsonObject segmentData = details["segments"].toObject(); _maxSegments = segmentData["max_segments"].toInt(1); } else { _maxSegments = 1; } } void PhilipsHueLight::blackScreenTriggered() { _blackScreenTriggered = true; } bool PhilipsHueLight::isBusy() { bool temp = true; qint64 _currentTime = QDateTime::currentMSecsSinceEpoch(); if (_currentTime - _lastSendColorTime >= 100) { _lastSendColorTime = _currentTime; temp = false; } return temp; } void PhilipsHueLight::setBlack() { Debug(_log,""); CiColor black; black.bri = 0; black.x = 0; black.y = 0; setColor(black); } bool PhilipsHueLight::isBlack(bool isBlack) { if (!isBlack) { _lastBlackTime = 0; return false; } if (_lastBlackTime == 0) { _lastBlackTime = QDateTime::currentMSecsSinceEpoch(); return false; } qint64 _currentTime = QDateTime::currentMSecsSinceEpoch(); return _currentTime - _lastBlackTime >= _onBlackTimeToPowerOff; } bool PhilipsHueLight::isWhite(bool isWhite) { if (!isWhite) { _lastWhiteTime = 0; return false; } if (_lastWhiteTime == 0) { _lastWhiteTime = QDateTime::currentMSecsSinceEpoch(); return false; } qint64 _currentTime = QDateTime::currentMSecsSinceEpoch(); return _currentTime - _lastWhiteTime >= _onBlackTimeToPowerOn; } QString PhilipsHueLight::getId() const { return _id; } QString PhilipsHueLight::getdeviceId() const { return _deviceId; } QString PhilipsHueLight::getProduct() const { return _product; } QString PhilipsHueLight::getModel() const { return _model; } QString PhilipsHueLight::getName() const { return _name; } QString PhilipsHueLight::getArcheType() const { return _archeType; } int PhilipsHueLight::getMaxSegments() const { return _maxSegments; } QJsonObject PhilipsHueLight::getOriginalState() const { return _originalState; } void PhilipsHueLight::saveOriginalState(const QJsonObject& values) { Debug(_log,"Light: %s, id: %s", QSTRING_CSTR(_name), QSTRING_CSTR(_id)); if ( _useApiV2 ) { _originalState[API_STATE_ON] = values[API_STATE_ON]; QJsonValue color = values[API_COLOR]; _originalState.insert(API_COLOR, QJsonObject {{API_XY_COORDINATES, color[API_XY_COORDINATES] }}); _originalState[API_GRADIENT] = values[API_GRADIENT]; } else { if (_blackScreenTriggered) { _blackScreenTriggered = false; return; } // 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 %s", QSTRING_CSTR(_id) ); } QJsonObject lState = values[API_STATE].toObject(); _originalStateJSON = lState; QJsonObject state; state["on"] = lState["on"]; _originalColor = CiColor(); _originalColor.bri = 0; _originalColor.x = 0; _originalColor.y = 0; 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); Debug(_log, "Philips original state stored: %s", QSTRING_CSTR(c)); _transitionTime = values[API_STATE].toObject()[API_TRANSITIONTIME].toInt(); } //Determine the original state. _originalState = state; } } void PhilipsHueLight::setOnOffState(bool on) { Debug(_log,"Light: %s, id: %s -> %s", QSTRING_CSTR(_name), QSTRING_CSTR(_id), on ? "On" : "Off"); this->_on = on; } void PhilipsHueLight::setTransitionTime(int transitionTime) { this->_transitionTime = transitionTime; } void PhilipsHueLight::setColor(const CiColor& color) { this->_hasColor = true; this->_color = color; } bool PhilipsHueLight::getOnOffState() const { return _on; } int PhilipsHueLight::getTransitionTime() const { return _transitionTime; } CiColor PhilipsHueLight::getColor() const { return _color; } bool PhilipsHueLight::hasColor() const { return _hasColor; } CiColorTriangle PhilipsHueLight::getColorSpace() const { return _colorSpace; } LedDevicePhilipsHue::LedDevicePhilipsHue(const QJsonObject& deviceConfig) : LedDevicePhilipsHueBridge(deviceConfig) , _switchOffOnBlack(false) , _brightnessFactor(1.0) , _transitionTime(1) , _isInitLeds(false) , _lightsCount(0) , _blackLightsTimeout(15000) , _blackLevel(0.0) , _onBlackTimeToPowerOff(100) , _onBlackTimeToPowerOn(100) , _candyGamma(true) , _groupStreamState(false) { } LedDevice* LedDevicePhilipsHue::construct(const QJsonObject &deviceConfig) { return new LedDevicePhilipsHue(deviceConfig); } LedDevicePhilipsHue::~LedDevicePhilipsHue() { } bool LedDevicePhilipsHue::init(const QJsonObject &deviceConfig) { bool isInitOK {false}; if (!verbose) { verbose = deviceConfig[CONFIG_VERBOSE].toBool(false); } // Initialise LedDevice configuration and execution environment _useEntertainmentAPI = deviceConfig[CONFIG_USE_HUE_ENTERTAINMENT_API].toBool(false); // Overwrite non supported/required features if ( deviceConfig["rewriteTime"].toInt(0) > 0 ) { InfoIf ( ( !_useEntertainmentAPI ), _log, "Device Philips Hue does not require rewrites. Refresh time is ignored." ); _devConfig["rewriteTime"] = 0; } _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 = _devConfig[CONFIG_groupId].toString(); _blackLevel = _devConfig["blackLevel"].toDouble(0.0); _onBlackTimeToPowerOff = _devConfig["onBlackTimeToPowerOff"].toInt(100); _onBlackTimeToPowerOn = _devConfig["onBlackTimeToPowerOn"].toInt(100); _candyGamma = _devConfig["candyGamma"].toBool(true); if (_blackLevel < 0.0) { _blackLevel = 0.0; } if (_blackLevel > 1.0) { _blackLevel = 1.0; } if (LedDevicePhilipsHueBridge::init(_devConfig)) { log( "Off on Black", "%s", _switchOffOnBlack ? "Yes" : "No" ); log( "Brightness Factor", "%f", _brightnessFactor ); log( "Transition Time", "%d", _transitionTime ); log( "Restore Original State", "%s", _isRestoreOrigState ? "Yes" : "No" ); log( "Use Hue Entertainment API", "%s", _useEntertainmentAPI ? "Yes" : "No" ); log("Brightness Threshold", "%f", _blackLevel); log("CandyGamma", "%s", _candyGamma ? "Yes" : "No" ); log("Time powering off when black", "%s", _onBlackTimeToPowerOff ? "Yes" : "No" ); log("Time powering on when signalled", "%s", _onBlackTimeToPowerOn ? "Yes" : "No" ); if( _useEntertainmentAPI ) { log( "Entertainment API Group-ID", "%s", QSTRING_CSTR(_groupId) ); if( _groupId.isEmpty() ) { Error(_log, "Disabling usage of Entertainment API - Group-ID is invalid [%s]", QSTRING_CSTR(_groupId) ); _useEntertainmentAPI = false; } } // Everything is OK -> enable _isDeviceInitialised = true; isInitOK = true; } return isInitOK; } bool LedDevicePhilipsHue::setLights() { bool isInitOK = true; _lightIds.clear(); QStringList lights; if( _useEntertainmentAPI && !_groupId.isEmpty() ) { lights = getGroupLights( _groupId ); } if( lights.empty() ) { if( _useEntertainmentAPI ) { _useEntertainmentAPI = false; Error(_log, "Group-ID [%s] is not usable - Entertainment API usage was disabled!", QSTRING_CSTR(_groupId) ); } lights = _devConfig[ CONFIG_lightIdS ].toVariant().toStringList(); } _lightIds = lights; int configuredLightsCount = _lightIds.size(); if ( configuredLightsCount == 0 ) { this->setInError( "No light-IDs configured" ); isInitOK = false; } else { Debug(_log, "Lights configured: %d", configuredLightsCount ); if (updateLights( getLightMap())) { if (_useApiV2) { _channelsCount = getGroupChannelsCount (_groupId); Debug(_log, "Channels configured: %d", _channelsCount ); int ledsCount = getLedCount(); if ( ledsCount == _channelsCount) { isInitOK = true; } else { QString errorText = QString("Number of hardware LEDs configured [%1] do not match the Entertainment lights' channel number [%2]."\ " Please update your configuration.").arg(ledsCount).arg(_channelsCount ); setInError(errorText, false); isInitOK = false; } } } else { isInitOK = false; } } return isInitOK; } bool LedDevicePhilipsHue::initLeds() { bool isInitOK = false; if ( !this->isInError() ) { if( setLights() ) { if( _useEntertainmentAPI ) { _groupName = getGroupName( _groupId ); } else { // adapt latchTime to count of user lightIds (bridge 10Hz max overall) setLatchTime( 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()) { _lights.reserve(static_cast(_lightIds.size())); for(const auto &id : qAsConst(_lightIds)) { if (map.contains(id)) { _lights.emplace_back(_log, _useApiV2, id, map.value(id), _onBlackTimeToPowerOff, _onBlackTimeToPowerOn); } else { Warning(_log, "Configured light-ID %s is not available at this bridge", QSTRING_CSTR(id) ); } } } int lightsCount = static_cast(_lights.size()); setLightsCount( lightsCount ); if( lightsCount == 0 ) { Error(_log, "No usable lights found!" ); isInitOK = false; } else { //Populate additional light details int i {1}; for (PhilipsHueLight& light : _lights) { light.setDeviceDetails(getDeviceDetails(light.getdeviceId())); light.setEntertainmentSrvDetails(getEntertainmentSrvDetails(light.getdeviceId())); Info(_log,"Light[%d]: \"%s\" [%s], Product: %s, Model: %s, Segments [%d]", i, QSTRING_CSTR(light.getName()), QSTRING_CSTR(light.getId()), QSTRING_CSTR(light.getProduct()), QSTRING_CSTR(light.getModel()), light.getMaxSegments() ); ++i; } } return isInitOK; } bool LedDevicePhilipsHue::openStream() { bool isInitOK = false; bool streamState = getStreamGroupState(); if ( !this->isInError() ) { // stream is already active if( streamState ) { // if same owner stop stream if(isStreamOwner(_streamOwner)) { Debug(_log, "Group: \"%s\" [%s] is in use, try to stop stream", QSTRING_CSTR(_groupName), QSTRING_CSTR(_groupId) ); if( stopStream() ) { Debug(_log, "Stream successful stopped"); //Restore Philips Hue devices state restoreState(); isInitOK = startStream(); } else { Error(_log, "Group: \"%s\" [%s] couldn't stop by user: \"%s\" - Entertainment API not usable", QSTRING_CSTR( _groupName ), QSTRING_CSTR(_groupId), QSTRING_CSTR( _streamOwner ) ); } } else { Error(_log, "Group: \"%s\" [%s] is in use and owned by other user: \"%s\" - Entertainment API not usable", QSTRING_CSTR(_groupName), QSTRING_CSTR(_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."); } else { Error(_log, "Philips Hue Entertainment API not connected!"); } } else { Error(_log, "Philips Hue Entertainment API could not be initialized!"); } return isInitOK; } bool LedDevicePhilipsHue::startStream() { int retries {3}; while (!setStreamGroupState(true) && --retries > 0) { Debug(_log, "Start Entertainment stream. Retrying..."); QThread::msleep(500); } bool rc = (retries > 0); if (rc) { Debug(_log, "The Entertainment stream started successfully"); } else { this->setInError("The Entertainment stream failed to start. Give up."); } return rc; } bool LedDevicePhilipsHue::stopStream() { stopConnection(); int retries = 3; while (!setStreamGroupState(false) && --retries > 0) { Debug(_log, "Stop Entertainment stream. Retrying..."); QThread::msleep(500); } bool rc = (retries > 0); if (rc) { Debug(_log, "The Entertainment stream stopped successfully"); } else { this->setInError("The Entertainment stream did NOT stop. Give up."); } return rc; } bool LedDevicePhilipsHue::getStreamGroupState() { bool streamState {false}; QJsonDocument doc = getGroupDetails( _groupId ); DebugIf(verbose, _log, "GroupDetails: [%s]", QJsonDocument(doc).toJson(QJsonDocument::Compact).constData()); if ( !this->isInError() ) { if (_useApiV2) { QJsonArray groups = doc.array(); if (groups.isEmpty()) { this->setInError( "No Entertainment/Streaming details in Group found" ); } else { QJsonObject group = groups[0].toObject(); QString streamStaus = group.value(API_STREAM_STATUS).toString(); if ( streamStaus == API_STREAM_ACTIVE) { streamState = true; } QJsonObject streamer = group.value(API_STREAM_ACTIVE_V2).toObject(); _streamOwner = streamer[API_RID].toString(); } } else { QJsonObject obj = doc.object()[ API_STREAM ].toObject(); if( obj.isEmpty() ) { this->setInError( "No Entertainment/Streaming details in Group found" ); } else { streamState = obj.value( API_STREAM_ACTIVE ).toBool(); _streamOwner = obj.value( API_OWNER ).toString(); } } } return streamState; } bool LedDevicePhilipsHue::setStreamGroupState(bool state) { QJsonDocument doc = setGroupState( _groupId, state ); DebugIf(verbose, _log, "StreamGroupState: [%s]", QJsonDocument(doc).toJson(QJsonDocument::Compact).constData()); if (_useApiV2) { if (doc.isEmpty()) { _groupStreamState = false; } else { _groupStreamState = state; } return (_groupStreamState == state); } else { QJsonArray response = doc.array(); if (!response.isEmpty()) { QJsonObject msg = response.first().toObject(); if ( !msg.contains( API_SUCCESS ) ) { QString active = state ? API_STREAM_ACTIVE_VALUE_TRUE : API_STREAM_ACTIVE_VALUE_FALSE; Warning(_log, "%s", QSTRING_CSTR(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}} QJsonObject success = msg.value(API_SUCCESS).toObject(); QString valueName = QString( API_STREAM_RESPONSE_FORMAT ).arg( API_RESOURCE_GROUPS, _groupId, API_STREAM, API_STREAM_ACTIVE ); QJsonValue result = success.value(valueName); if (result.isUndefined()) { //Workaround //Check diyHue response {"success":{"/groups/groupId/stream":{"active":activeYesNo}}} QString diyHueValueName = QString( "/%1/%2/%3" ).arg( API_RESOURCE_GROUPS, _groupId, API_STREAM); result = success.value(diyHueValueName).toObject().value(API_STREAM_ACTIVE); } _groupStreamState = result.toBool(); return (_groupStreamState == state); } } } return false; } void LedDevicePhilipsHue::stop() { LedDevicePhilipsHueBridge::stop(); } int LedDevicePhilipsHue::open() { int retval = -1; if ( LedDevicePhilipsHueBridge::open() == 0) { if (initLeds()) { retval = 0; } } return retval; } int LedDevicePhilipsHue::write(const std::vector & ledValues) { // lights will be empty sometimes if( _lights.empty() ) { return -1; } // more lights than LEDs, stop always if( static_cast(ledValues.size()) < getLightsCount() ) { Error(_log, "More light-IDs configured than LEDs, each light-ID requires one LED!" ); return -1; } int rc {0}; if (_isOn) { if (!_useApiV2) { rc = writeSingleLights( ledValues ); } if (_useEntertainmentAPI && _isInitLeds) { rc= writeStreamData(ledValues); } } return rc; } int LedDevicePhilipsHue::writeSingleLights(const std::vector& ledValues) { // Iterate through lights and set colors. unsigned int idx = 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(), _candyGamma); if (_switchOffOnBlack && xy.bri <= _blackLevel && light.isBlack(true)) { xy.bri = 0; xy.x = 0; xy.y = 0; if( _useEntertainmentAPI ) { if (light.getOnOffState()) { this->setColor(light, xy); this->setOnOffState(light, false); } } else { if (light.getOnOffState()) { setState(light, false, xy); } } } else { bool currentstate = light.getOnOffState(); if (_switchOffOnBlack && xy.bri > _blackLevel && light.isWhite(true)) { if (!currentstate) { xy.bri = xy.bri / 2; } if (_useEntertainmentAPI) { this->setOnOffState(light, true); this->setColor(light, xy); } else { this->setState(light, true, xy); } } else if (!_switchOffOnBlack) { // Write color if color has been changed. if (_useEntertainmentAPI) { this->setOnOffState(light, true); this->setColor(light, xy); } else { this->setState( light, true, xy ); } } } if (xy.bri > _blackLevel) { light.isBlack(false); } else if (xy.bri <= _blackLevel) { light.isWhite(false); } ++idx; } return 0; } int LedDevicePhilipsHue::writeStreamData(const std::vector& ledValues, bool flush) { QByteArray msg; if (_useApiV2) { int ledsCount = static_cast(ledValues.size()); if ( ledsCount != _channelsCount ) { QString errorText = QString("Number of LEDs configured via the layout [%1] do not match the Entertainment lights' channel number [%2]."\ " Please update your configuration.").arg(ledsCount).arg(_channelsCount); this->setInError(errorText, false); return -1; } // "HueStream", //protocol // 0x02, 0x00, //version 2.0 // 0x07, //sequence number 7 // 0x00, 0x00, //reserved // 0x00, //color mode RGB // 0x00, //reserved // "1a8d99cc-967b-44f2-9202-43f976c0fa6b", //entertainment configuration id // 0x00, //channel id 0 // 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, //red // 0x01, //channel id 1 // 0x00, 0x00, 0xff, 0xff, 0x00, 0x00 //green // 0x02, //channel id 2 // 0x00, 0x00, 0x00, 0x00, 0xff, 0xff //blue // 0x03, //channel id 3 // 0xff, 0xff, 0xff, 0xff, 0xff, 0xff //white // //etc for channel ids 4-7 msg.reserve(static_cast(sizeof(HEADER_V2) + sizeof(ENTERTAINMENT_ID) + sizeof(PAYLOAD_PER_CHANNEL_V2) * _lights.size())); msg.append(reinterpret_cast(HEADER_V2), sizeof(HEADER_V2)); msg.append(_groupId.toLocal8Bit()); uint8_t maxChannels = static_cast(ledValues.size()); ColorRgb color; for (uint8_t channel = 0; channel < maxChannels; ++channel) { if (channel < 20) // v2 max 20 channels { color = static_cast(ledValues.at(channel)); quint16 R = static_cast(color.red << 8); quint16 G = static_cast(color.green << 8); quint16 B = static_cast(color.blue<< 8); msg.append(static_cast(channel)); const uint16_t payload[] = { qToBigEndian(R), qToBigEndian(G), qToBigEndian(B) }; msg.append(reinterpret_cast(payload), sizeof(payload)); } } } else { // "HueStream", //protocol // 0x01, 0x00, //version 1.0 // 0x07, //sequence number 7 // 0x00, 0x00, //reserved // 0x00, //color mode RGB // 0x00, //reserved // 0x00, 0x00, 0x01, //light ID 1 // 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, //red // 0x00, 0x00, 0x04, //light ID 4 // 0x00, 0x00, 0x00, 0x00, 0xff, 0xff //blue msg.reserve(static_cast(sizeof(HEADER) + sizeof(PAYLOAD_PER_LIGHT) * _lights.size())); msg.append(reinterpret_cast(HEADER), sizeof(HEADER)); ColorRgb color; uint8_t i = 0; for (const PhilipsHueLight& light : _lights) { if (i < 10) // max 10 lights { uint8_t id = static_cast(light.getId().toInt()); color = static_cast(ledValues.at(i)); quint16 R = static_cast(color.red << 8); quint16 G = static_cast(color.green << 8); quint16 B = static_cast(color.blue<< 8); msg.append(2, 0x00); msg.append(static_cast(id)); const uint16_t payload[] = { qToBigEndian(R), qToBigEndian(G), qToBigEndian(B) }; msg.append(reinterpret_cast(payload), sizeof(payload)); } ++i; } } if (verbose3) { qDebug() << "Msg Hex:" << msg.toHex(':'); } writeBytes(msg, flush); return 0; } void LedDevicePhilipsHue::setOnOffState(PhilipsHueLight& light, bool on, bool force) { if (light.getOnOffState() != on || force) { Debug(_log,"id: %s, on: %d", QSTRING_CSTR(light.getId()), on); QStringList resourcePath; QJsonObject cmd; if (_useApiV2) { resourcePath << API_RESOURCE_LIGHT << light.getId(); cmd.insert(API_STATE_ON, QJsonObject {{API_STATE_ON, on }}); } else { resourcePath << API_RESOURCE_LIGHTS << light.getId() << API_STATE; cmd.insert(API_STATE_ON, on); } put(resourcePath, cmd); if (!isInError()) { light.setOnOffState( on ); } } } void LedDevicePhilipsHue::setTransitionTime(PhilipsHueLight& light) { if (light.getTransitionTime() != _transitionTime) { QStringList resourcePath; QJsonObject cmd; if (_useApiV2) { resourcePath << API_RESOURCE_LIGHT << light.getId(); cmd.insert(API_DYNAMICS, QJsonObject {{API_DURATION, _transitionTime }}); } else { resourcePath << API_RESOURCE_LIGHTS << light.getId() << API_STATE; cmd.insert(API_TRANSITIONTIME, _transitionTime); } put(resourcePath, cmd); if (!isInError()) { light.setTransitionTime( _transitionTime ); } } } void LedDevicePhilipsHue::setColor(PhilipsHueLight& light, CiColor& color) { if (!light.hasColor() || light.getColor() != color) { if( !_useEntertainmentAPI ) { QStringList resourcePath; QJsonObject cmd; if (!light.hasColor() || light.getColor() != color) { if (_useApiV2) { resourcePath << API_RESOURCE_LIGHT << light.getId(); // Brightness is 0-100 %, Brightness percentage. value cannot be 0, writing 0 changes it to lowest possible brightness const double bri = qMin(_brightnessFactor * color.bri * 100, 100.0); QJsonObject colorXY; colorXY[API_X_COORDINATE] = color.x; colorXY[API_Y_COORDINATE] = color.y; cmd.insert(API_COLOR, QJsonObject {{API_DURATION, colorXY }}); cmd.insert(API_DIMMING, QJsonObject {{API_BRIGHTNESS, bri }}); } else { resourcePath << API_RESOURCE_LIGHTS << light.getId() << API_STATE; const int bri = qRound(qMin(254.0, _brightnessFactor * qMax(1.0, color.bri * 254.0))); QJsonArray colorXY; colorXY.append(color.x); colorXY.append(color.y); cmd.insert(API_XY_COORDINATES, colorXY); cmd.insert(API_BRIGHTNESS, bri); } } put(resourcePath, cmd); } else { color.bri = (qMin(1.0, _brightnessFactor * qMax(0.0, color.bri))); } if (!isInError()) { light.setColor( color ); } } } void LedDevicePhilipsHue::setState(PhilipsHueLight& light, bool on, const CiColor& color) { QStringList resourcePath; QJsonObject cmd; bool forceCmd {false}; if (light.getOnOffState() != on) { forceCmd = true; if (_useApiV2) { cmd.insert(API_STATE_ON, QJsonObject {{API_STATE_ON, on }}); } else { cmd.insert(API_STATE_ON, on); } } if (!_useEntertainmentAPI && light.getOnOffState()) { if (light.getTransitionTime() != _transitionTime) { if (_useApiV2) { cmd.insert(API_DYNAMICS, QJsonObject {{API_DURATION, _transitionTime }}); } else { cmd.insert(API_TRANSITIONTIME, _transitionTime); } } if (!light.hasColor() || light.getColor() != color) { if (!light.isBusy() || forceCmd) { if (_useApiV2) { // Brightness is 0-100 %, Brightness percentage. value cannot be 0, writing 0 changes it to lowest possible brightness const double bri = qMin(_brightnessFactor * color.bri * 100, 100.0); QJsonObject colorXY; colorXY[API_X_COORDINATE] = color.x; colorXY[API_Y_COORDINATE] = color.y; cmd.insert(API_COLOR, QJsonObject {{API_DURATION, colorXY }}); cmd.insert(API_DIMMING, QJsonObject {{API_BRIGHTNESS, bri }}); } else { const int bri = qRound(qMin(254.0, _brightnessFactor * qMax(1.0, color.bri * 254.0))); QJsonArray colorXY; colorXY.append(color.x); colorXY.append(color.y); cmd.insert(API_XY_COORDINATES, colorXY); cmd.insert(API_BRIGHTNESS, bri); } } } } if (!cmd.isEmpty()) { if (_useApiV2) { resourcePath << API_RESOURCE_LIGHT << light.getId(); } else { resourcePath << API_RESOURCE_LIGHTS << light.getId() << API_STATE; } put(resourcePath, cmd); if (!isInError()) { light.setTransitionTime( _transitionTime ); light.setColor( color ); light.setOnOffState( on ); } } } void LedDevicePhilipsHue::setLightsCount(int lightsCount) { _lightsCount = lightsCount; } bool LedDevicePhilipsHue::switchOn() { bool rc {false}; if ( _isOn ) { Debug(_log, "Device %s is already on. Skipping.", QSTRING_CSTR(_activeDeviceType)); rc = true; } else { if ( _isDeviceReady ) { Info(_log, "Switching device %s ON", QSTRING_CSTR(_activeDeviceType)); if ( storeState() ) { if (_useEntertainmentAPI) { if (openStream()) { if (startConnection()) { if ( (!_useApiV2 || _isDiyHue) ) //DiyHue does not auto switch on, if stream starts { powerOn(); } _isOn = true; setRewriteTime(STREAM_REWRITE_TIME.count()); } } else { // TODO: Failed to OpenStream - should retry } } else { if ( powerOn() ) { _isOn = true; } } } if (_isOn) { Info(_log, "Device %s is ON", QSTRING_CSTR(_activeDeviceType)); emit enableStateChanged(_isEnabled); rc =true; } else { Warning(_log, "Failed switching device %s ON", QSTRING_CSTR(_activeDeviceType)); } } } return rc; } bool LedDevicePhilipsHue::switchOff() { bool rc {false}; if ( !_isOn ) { rc = true; } else { if ( _isDeviceInitialised ) { if ( _isDeviceReady ) { if (_useEntertainmentAPI && _groupStreamState) { Info(_log, "Switching device %s OFF", QSTRING_CSTR(_activeDeviceType)); setRewriteTime(0); if ( _isRestoreOrigState ) { _isOn = false; stopStream(); rc = restoreState(); } else { _isOn = false; rc = stopStream(); if ( (!_useApiV2 || _isDiyHue) ) //DiyHue does not auto switch off, if stream stopps { rc = powerOff(); } } if (rc) { Info(_log, "Device %s is OFF", QSTRING_CSTR(_activeDeviceType)); rc = true; } else { Warning(_log, "Failed switching device %s OFF", QSTRING_CSTR(_activeDeviceType)); } } else { Debug(_log,"LedDevicePhilipsHueBridge::switchOff()"); rc = LedDevicePhilipsHueBridge::switchOff(); } } else { rc = true; } } } return rc; } bool LedDevicePhilipsHue::powerOn() { bool rc {true}; if (_isDeviceReady) { for ( PhilipsHueLight& light : _lights ) { setOnOffState(light, true, true); } } return rc; } bool LedDevicePhilipsHue::powerOff() { bool rc {true}; if (_isDeviceReady) { for ( PhilipsHueLight& light : _lights ) { setOnOffState(light, false, true); } } return rc; } bool LedDevicePhilipsHue::storeState() { bool rc {true}; if ( _isRestoreOrigState ) { if( !_lightIds.empty() ) { for ( PhilipsHueLight& light : _lights ) { QJsonObject values = getLightDetails(light.getId()); 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; } void LedDevicePhilipsHue::identify(const QJsonObject& params) { DebugIf(verbose, _log, "params: [%s]", QJsonDocument(params).toJson(QJsonDocument::Compact).constData()); QJsonObject properties; _hostName = params[CONFIG_HOST].toString(""); _apiPort = params[CONFIG_PORT].toInt(); _authToken = params[CONFIG_USERNAME].toString(""); QString lighName = params["lightName"].toString(); Info(_log, "Identify %s, Light: \"%s\" @hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(lighName), QSTRING_CSTR(_hostName) ); if (NetUtils::resolveHostToAddress(_log, _hostName, _address)) { QJsonDocument bridgeDetails = retrieveBridgeDetails(); if ( !bridgeDetails.isEmpty() ) { setBridgeDetails(bridgeDetails); if ( openRestAPI() ) { _useApiV2 = _isAPIv2Ready; // DIYHue does not provide v2 Breathe effects, yet -> fall back to v1 if (_isDiyHue) { _useApiV2 = false; } if (_useApiV2) { configureSsl(); } if (!isInError() ) { setBaseApiEnvironment(_useApiV2); QStringList resourcepath; QJsonObject cmd; if (_useApiV2) { QString lightId = params[API_LIGTH_ID].toString(); resourcepath << API_RESOURCE_LIGHT << lightId; cmd.insert(API_ALERT, QJsonObject {{API_ACTION, API_ACTION_BREATHE}}); } else { bool on {true}; QString lightId = params[API_LIGTH_ID_v1].toString(); resourcepath << lightId << API_STATE; cmd.insert(API_STATE_ON, on); cmd.insert(API_ALERT, API_SELECT); } _restApi->setPath(resourcepath); // Perform request httpResponse response = _restApi->put(cmd); if (response.error()) { Warning(_log, "%s identification failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); } } } } } }