diff --git a/assets/webconfig/i18n/de.json b/assets/webconfig/i18n/de.json index 479123a8..644049ff 100644 --- a/assets/webconfig/i18n/de.json +++ b/assets/webconfig/i18n/de.json @@ -426,6 +426,7 @@ "edt_dev_spec_transistionTime_title": "Übergangszeit", "edt_dev_spec_switchOffOnBlack_title": "Aus bei schwarz", "edt_dev_spec_brightnessFactor_title": "Helligkeitsfaktor", + "edt_dev_spec_restoreOriginalState_title" : "Lampen Originalzustand wiederhestellen", "edt_dev_spec_ledType_title": "LED typ", "edt_dev_spec_uid_title": "UID", "edt_dev_spec_intervall_title": "Intervall", diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 603d8710..07d9aaea 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -425,6 +425,7 @@ "edt_dev_spec_transistionTime_title" : "Transition time", "edt_dev_spec_switchOffOnBlack_title" : "Switch off on black", "edt_dev_spec_brightnessFactor_title" : "Brightness factor", + "edt_dev_spec_restoreOriginalState_title" : "Restore lights' original state", "edt_dev_spec_ledType_title" : "LED Type", "edt_dev_spec_uid_title" : "UID", "edt_dev_spec_intervall_title" : "Interval", diff --git a/assets/webconfig/js/wizard.js b/assets/webconfig/js/wizard.js index fb7ab6d2..de33979e 100644 --- a/assets/webconfig/js/wizard.js +++ b/assets/webconfig/js/wizard.js @@ -567,23 +567,35 @@ function checkHueBridge(cb,hueUser){ timeout: 2000 }) .done( function( data, textStatus, jqXHR ) { - if(Array.isArray(data) && data[0].error && data[0].error.type == 4) - cb(true); - else if(Array.isArray(data) && data[0].error) - cb(false); + if( Array.isArray(data) && data[0].error) + { + if ( data[0].error.type == 3 || data[0].error.type == 4) + { + cb(true, usr); + } + else + { + cb(false); + } + } else - cb(true); + { + cb(true, usr); + } + }) .fail( function( jqXHR, textStatus ) { cb(false); }); } -function checkUserResult(reply){ +function checkUserResult(reply, usr){ + if(reply) { $('#wiz_hue_usrstate').html(""); $('#wiz_hue_create_user').toggle(false); + $('#user').val(usr); get_hue_lights(); } else @@ -640,17 +652,22 @@ function checkBridgeResult(reply){ function identHueId(id, off) { - var on = true; if(off !== true) + { setTimeout(identHueId,1500,id,true); + var put_data = '{"on":true,"bri":254,"hue":47000,"sat":254}'; + } else - on = false; + { + var put_data = '{"on":false}'; + } $.ajax({ url: 'http://'+$('#ip').val()+'/api/'+$('#user').val()+'/lights/'+id+'/state', type: 'PUT', timeout: 2000, - data: ' {"on":'+on+', "sat":254, "bri":254,"hue":47000}' + + data: put_data }) } @@ -686,7 +703,9 @@ function beginWizardHue() //check if ip is empty/reachable/search for bridge if(conf_editor.getEditor("root.specificOptions.output").getValue() == "") + { getHueIPs(); + } else { var ip = conf_editor.getEditor("root.specificOptions.output").getValue(); @@ -719,6 +738,7 @@ function beginWizardHue() } } + var ledCount= Object.keys(lightIDs).length; window.serverConfig.leds = hueLedConfig; @@ -736,6 +756,7 @@ function beginWizardHue() d.lightIds = finalLightIds; d.username = $('#user').val(); d.type = "philipshue"; + d.hardwareLedCount = ledCount; d.transitiontime = 1; d.switchOffOnBlack = true; @@ -814,6 +835,7 @@ function get_hue_lights(){ for(var lightid in r) { + $('.lidsb').append(createTableRow([lightid+' ('+r[lightid].name+')', '',''])); } diff --git a/include/leddevice/LedDevice.h b/include/leddevice/LedDevice.h index a587d5b0..29e5e83a 100644 --- a/include/leddevice/LedDevice.h +++ b/include/leddevice/LedDevice.h @@ -59,7 +59,9 @@ public: unsigned int getLedCount() const { return _ledCount; } bool enabled() const { return _enabled; } + int getLatchTime() const { return _latchTime_ms; } + void setLatchTime( int latchTime_ms ); /// /// Check, if device is ready to be used diff --git a/libsrc/leddevice/LedDevice.cpp b/libsrc/leddevice/LedDevice.cpp index 0f1e7410..30191903 100644 --- a/libsrc/leddevice/LedDevice.cpp +++ b/libsrc/leddevice/LedDevice.cpp @@ -231,6 +231,12 @@ void LedDevice::setLedCount(unsigned int ledCount) _ledRGBWCount = _ledCount * sizeof(ColorRgbw); } +void LedDevice::setLatchTime( int latchTime_ms ) +{ + _latchTime_ms = latchTime_ms; + Debug(_log, "LatchTime updated to %dms", this->getLatchTime()); +} + int LedDevice::rewriteLeds() { int retval = -1; diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp index 1aad493c..83f162f8 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp @@ -37,13 +37,13 @@ static const char STATE_ONOFF_VALUE[] = "value"; static const char STATE_VALUE_TRUE[] = "true"; static const char STATE_VALUE_FALSE[] = "false"; -//Device Data elements +// Device Data elements static const char DEV_DATA_NAME[] = "name"; static const char DEV_DATA_MODEL[] = "model"; static const char DEV_DATA_MANUFACTURER[] = "manufacturer"; static const char DEV_DATA_FIRMWAREVERSION[] = "firmwareVersion"; -//Nanoleaf Stream Control elements +// Nanoleaf Stream Control elements //static const char STREAM_CONTROL_IP[] = "streamControlIpAddr"; static const char STREAM_CONTROL_PORT[] = "streamControlPort"; //static const char STREAM_CONTROL_PROTOCOL[] = "streamControlProtocol"; @@ -59,7 +59,7 @@ static const char API_STATE[] ="state"; static const char API_PANELLAYOUT[] = "panelLayout"; static const char API_EFFECT[] = "effects"; -//Nanoleaf ssdp services +// Nanoleaf ssdp services static const char SSDP_CANVAS[] = "nanoleaf:nl29"; static const char SSDP_LIGHTPANELS[] = "nanoleaf_aurora:light"; const int SSDP_TIMEOUT = 5000; // timout in ms @@ -132,7 +132,7 @@ bool LedDeviceNanoleaf::init(const QJsonObject &deviceConfig) if ( _hostname.isEmpty() ) { //Discover Nanoleaf device - if ( !discoverNanoleafDevice() ) + if ( !discoverDevice() ) { this->setInError("No target IP defined nor Nanoleaf device was discovered"); return false; @@ -256,24 +256,17 @@ int LedDeviceNanoleaf::open() if ( init(_devConfig) ) { - if ( !initNetwork() ) + if ( initLeds() ) { - this->setInError( "UDP Network error!" ); - } - else - { - if ( initLeds() ) - { - _deviceReady = true; - setEnable(true); - retval = 0; - } + _deviceReady = true; + setEnable(true); + retval = 0; } } return retval; } -bool LedDeviceNanoleaf::discoverNanoleafDevice() +bool LedDeviceNanoleaf::discoverDevice() { bool isDeviceFound (false); diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h index 4fc28734..5ef704f5 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h @@ -103,7 +103,7 @@ private: /// /// @return True, if Nanoleaf device was found /// - bool discoverNanoleafDevice(); + bool discoverDevice(); /// /// Change Nanoleaf device to External Control (UDP) mode diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp index 83b672cf..8a5d3064 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp @@ -1,91 +1,171 @@ // Local-Hyperion includes #include "LedDevicePhilipsHue.h" -// qt includes +// ssdp discover +#include + +// Qt includes #include +#include #include -bool operator ==(CiColor p1, CiColor p2) +// +static const bool verbose = false; +static const bool verbose3 = false; + +// Configuration settings +static const char CONFIG_ADDRESS[] = "output"; +//static const char CONFIG_PORT[] = "port"; +static const char CONFIG_USERNAME[] ="username"; +static const char CONFIG_BRIGHTNESSFACTOR [] = "brightnessFactor"; +static const char CONFIG_TRANSITIONTIME [] = "transitiontime"; +static const char CONFIG_ON_OFF_BLACK [] = "switchOffOnBlack"; +static const char CONFIG_RESTORE_STATE [] = "restoreOriginalState"; +static const char CONFIG_LIGHTIDS [] = "lightIds"; + +// Device Data elements +static const char DEV_DATA_BRIDGEID[] = "bridgeid"; +static const char DEV_DATA_MODEL[] = "modelid"; +static const char DEV_DATA_NAME[] = "name"; +//static const char DEV_DATA_MANUFACTURER[] = "manufacturer"; +static const char DEV_DATA_FIRMWAREVERSION[] = "swversion"; +static const char DEV_DATA_APIVERSION[] = "apiversion"; + +// Philips Hue OpenAPI URLs +static const char API_DEFAULT_PORT[] = "80"; +static const char API_URL_FORMAT[] = "http://%1:%2/api/%3/%4"; +static const char API_ROOT[] = ""; +static const char API_STATE[] ="state"; +static const char API_CONFIG[] = "config"; +static const char API_LIGHTS[] = "lights"; + +// List of resources +static const char API_XY_COORDINATES[] = "xy"; +static const char API_BRIGHTNESS[] = "bri"; +static const char API_TRANSITIONTIME[] = "transitiontime"; +static const char API_MODEID[] = "modelid"; + +// List of State Information +static const char API_STATE_ON[] = "on"; +static const char API_STATE_VALUE_TRUE[] = "true"; +static const char API_STATE_VALUE_FALSE[] = "false"; + +// List of Error Information +static const char API_ERROR[] = "error"; +static const char API_ERROR_ADDRESS[] = "address"; +static const char API_ERROR_DESCRIPTION[] = "description"; +static const char API_ERROR_TYPE[]="type"; + +// Phlips Hue ssdp services +static const char SSDP_ID[] = "urn:schemas-upnp-org:device:Basic:1"; +const int SSDP_TIMEOUT = 5000; // timout in ms + + +bool operator ==(const CiColor& p1, const CiColor& p2) { - return (p1.x == p2.x) && (p1.y == p2.y) && (p1.bri == p2.bri); + return ((p1.x == p2.x) && (p1.y == p2.y) && (p1.bri == p2.bri)); } -bool operator !=(CiColor p1, CiColor p2) +bool operator != (const CiColor& p1, const CiColor& p2) { return !(p1 == p2); } -CiColor CiColor::rgbToCiColor(float red, float green, float blue, CiColorTriangle colorSpace) +CiColor CiColor::rgbToCiColor(double red, double green, double blue, CiColorTriangle colorSpace) { - // Apply gamma correction. - float r = (red > 0.04045f) ? powf((red + 0.055f) / (1.0f + 0.055f), 2.4f) : (red / 12.92f); - float g = (green > 0.04045f) ? powf((green + 0.055f) / (1.0f + 0.055f), 2.4f) : (green / 12.92f); - float b = (blue > 0.04045f) ? powf((blue + 0.055f) / (1.0f + 0.055f), 2.4f) : (blue / 12.92f); - // Convert to XYZ space. - float X = r * 0.664511f + g * 0.154324f + b * 0.162028f; - float Y = r * 0.283881f + g * 0.668433f + b * 0.047685f; - float Z = r * 0.000088f + g * 0.072310f + b * 0.986039f; - // Convert to x,y space. - float cx = X / (X + Y + Z); - float cy = Y / (X + Y + Z); + 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.0f; + cx = 0.0; } if (std::isnan(cy)) { - cy = 0.0f; + cy = 0.0; } - // RGB to HSV/B Conversion after gamma correction use V for brightness, not Y from XYZ Space. - float bri = fmax(fmax(r, g), b); - CiColor xy = - { cx, cy, bri }; - // Check if the given XY value is within the color reach of our lamps. - if (!isPointInLampsReach(xy, colorSpace)) + if (std::isnan(bri)) { - // 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. - CiColor pAB = getClosestPointToPoint(colorSpace.red, colorSpace.green, xy); - CiColor pAC = getClosestPointToPoint(colorSpace.blue, colorSpace.red, xy); - CiColor pBC = getClosestPointToPoint(colorSpace.green, colorSpace.blue, xy); - // Get the distances per point and see which point is closer to our Point. - float dAB = getDistanceBetweenTwoPoints(xy, pAB); - float dAC = getDistanceBetweenTwoPoints(xy, pAC); - float dBC = getDistanceBetweenTwoPoints(xy, pBC); - float lowest = dAB; - CiColor closestPoint = pAB; - if (dAC < lowest) + 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)) { - lowest = dAC; - closestPoint = pAC; + // 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. + CiColor pAB = getClosestPointToPoint(colorSpace.red, colorSpace.green, xy); + CiColor pAC = getClosestPointToPoint(colorSpace.blue, colorSpace.red, xy); + CiColor 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; + CiColor 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; } - 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; } -float CiColor::crossProduct(CiColor p1, CiColor p2) +double CiColor::crossProduct(CiColor p1, CiColor p2) { return p1.x * p2.y - p1.y * p2.x; } bool CiColor::isPointInLampsReach(CiColor p, CiColorTriangle colorSpace) { - CiColor v1 = - { colorSpace.green.x - colorSpace.red.x, colorSpace.green.y - colorSpace.red.y }; - CiColor v2 = - { colorSpace.blue.x - colorSpace.red.x, colorSpace.blue.y - colorSpace.red.y }; - CiColor q = - { p.x - colorSpace.red.x, p.y - colorSpace.red.y }; - float s = crossProduct(q, v2) / crossProduct(v1, v2); - float t = crossProduct(v1, q) / crossProduct(v1, v2); - if ((s >= 0.0f) && (t >= 0.0f) && (s + t <= 1.0f)) + CiColor v1 = { colorSpace.green.x - colorSpace.red.x, colorSpace.green.y - colorSpace.red.y }; + CiColor v2 = { colorSpace.blue.x - colorSpace.red.x, colorSpace.blue.y - colorSpace.red.y }; + CiColor 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)) { return true; } @@ -94,250 +174,522 @@ bool CiColor::isPointInLampsReach(CiColor p, CiColorTriangle colorSpace) CiColor CiColor::getClosestPointToPoint(CiColor a, CiColor b, CiColor p) { - CiColor AP = - { p.x - a.x, p.y - a.y }; - CiColor AB = - { b.x - a.x, b.y - a.y }; - float ab2 = AB.x * AB.x + AB.y * AB.y; - float ap_ab = AP.x * AB.x + AP.y * AB.y; - float t = ap_ab / ab2; - if (t < 0.0f) + CiColor AP = { p.x - a.x, p.y - a.y }; + CiColor 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.0f; + t = 0.0; } - else if (t > 1.0f) + else if (t > 1.0) { - t = 1.0f; + t = 1.0; } - return - { a.x + AB.x * t, a.y + AB.y * t}; + return { a.x + AB.x * t, a.y + AB.y * t }; } -float CiColor::getDistanceBetweenTwoPoints(CiColor p1, CiColor p2) +double CiColor::getDistanceBetweenTwoPoints(CiColor p1, CiColor p2) { // Horizontal difference. - float dx = p1.x - p2.x; + double dx = p1.x - p2.x; // Vertical difference. - float dy = p1.y - p2.y; + double dy = p1.y - p2.y; // Absolute value. return sqrt(dx * dx + dy * dy); } -PhilipsHueBridge::PhilipsHueBridge(Logger* log, QString host, QString username) - : QObject() - , _log(log) - , host(host) - , username(username) +LedDevicePhilipsHueBridge::LedDevicePhilipsHueBridge(const QJsonObject &deviceConfig) + : LedDevice(deviceConfig) + , _networkmanager (nullptr) + , _api_major(0) + , _api_minor(0) + , _api_patch(0) + , _isHueEntertainmentReady (false) { - // setup reconnection timer - bTimer.setInterval(5000); - bTimer.setSingleShot(true); - - connect(&bTimer, &QTimer::timeout, this, &PhilipsHueBridge::bConnect); - connect(&manager, &QNetworkAccessManager::finished, this, &PhilipsHueBridge::resolveReply); + _devConfig = deviceConfig; + _deviceReady = false; } -void PhilipsHueBridge::bConnect(void) +LedDevicePhilipsHueBridge::~LedDevicePhilipsHueBridge() { - if(username.isEmpty() || host.isEmpty()) + if (_networkmanager != nullptr) { - Error(_log,"Username or IP Address is empty!"); + delete _networkmanager; + _networkmanager = nullptr; + } +} + +bool LedDevicePhilipsHueBridge::init(const QJsonObject &deviceConfig) +{ + // Overwrite non supported/required features + _devConfig["latchTime"] = 0; + if (deviceConfig["rewriteTime"].toInt(0) > 0) + { + Info (_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 = LedDevice::init(deviceConfig); + + Debug(_log, "DeviceType : %s", QSTRING_CSTR( this->getActiveDeviceType() )); + Debug(_log, "LedCount : %u", this->getLedCount()); + Debug(_log, "ColorOrder : %s", QSTRING_CSTR( this->getColorOrder() )); + Debug(_log, "RefreshTime : %d", _refresh_timer_interval); + Debug(_log, "LatchTime : %d", this->getLatchTime()); + + if ( isInitOK ) + { + //Set hostname as per configuration and_defaultHost default port + QString address = deviceConfig[ CONFIG_ADDRESS ].toString(); + + if (! address.isEmpty() ) + { + QStringList addressparts = address.split(":", QString::SkipEmptyParts); + + _hostname = addressparts[0]; + if ( addressparts.size() > 1) + { + _api_port = addressparts[1]; + } + else + { + _api_port = API_DEFAULT_PORT; + } + } + _username = deviceConfig[ CONFIG_USERNAME ].toString(); + + Debug(_log, "Hostname/IP : %s", QSTRING_CSTR( _hostname )); + Debug(_log, "Port : %s", QSTRING_CSTR( _api_port )); + } + return isInitOK; +} + +int LedDevicePhilipsHueBridge::open( ) +{ + return open (_hostname,_api_port, _username ); +} + +int LedDevicePhilipsHueBridge::open( const QString& hostname, const QString& port, const QString& username ) +{ + _deviceInError = false; + bool isInitOK = true; + + //If host not configured then discover device + if ( hostname.isEmpty() ) + { + //Discover Nanoleaf device + if ( !discoverDevice() ) + { + this->setInError("No target IP defined nor Philips Hue Bridge was discovered"); + return false; + } } else { - QString url = QString("http://%1/api/%2").arg(host).arg(username); - Debug(_log, "Connect to bridge %s", QSTRING_CSTR(url)); - - QNetworkRequest request(url); - manager.get(request); + _hostname = hostname; + _api_port = port; } -} -void PhilipsHueBridge::resolveReply(QNetworkReply* reply) -{ - // TODO use put request also for network error checking with decent threshold - if(reply->operation() == QNetworkAccessManager::GetOperation) + _username = username; + + //Get Philips Hue Bridge details and configuration + if ( _networkmanager == nullptr) { - if(reply->error() == QNetworkReply::NoError) + _networkmanager = new QNetworkAccessManager(); + } + + // Read Lights and Light-Ids + QString url = getUrl(_hostname, _api_port, _username, API_ROOT ); + QJsonDocument doc = getJson( url ); + + DebugIf(verbose, _log, "doc: [%s]", QString(QJsonDocument(doc).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + + if ( this->isInError() ) + { + isInitOK = false; + } + else + { + QJsonObject jsonConfigInfo = doc.object()[API_CONFIG].toObject(); + if ( verbose ) { - QByteArray response = reply->readAll(); - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(response, &error); - if (error.error != QJsonParseError::NoError) - { - Error(_log, "Got invalid response from bridge"); - return; - } - // check for authorization - if(doc.isArray()) - { - Error(_log, "Authorization failed, username invalid"); - return; - } + std::cout << "jsonConfigInfo: [" << QString(QJsonDocument(jsonConfigInfo).toJson(QJsonDocument::Compact)).toUtf8().constData() << "]" << std::endl; + } - QJsonObject obj = doc.object()["lights"].toObject(); + 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(); - if(obj.isEmpty()) - { - Error(_log, "Bridge has no registered bulbs/stripes"); - return; - } + QStringList apiVersionParts = _deviceAPIVersion.split(".", QString::SkipEmptyParts); + if ( !apiVersionParts.isEmpty()) + { + _api_major = apiVersionParts[0].toUInt(); + _api_minor = apiVersionParts[1].toUInt(); + _api_patch = apiVersionParts[2].toUInt(); - // get all available light ids and their values - QStringList keys = obj.keys(); - QMap map; - for (int i = 0; i < keys.size(); ++i) + if ( _api_major > 1 || (_api_major == 1 && _api_minor >= 22) ) { - map.insert(keys.at(i).toInt(), obj.take(keys.at(i)).toObject()); + _isHueEntertainmentReady = true; } - emit newLights(map); + } + + Debug(_log, "Bridge Name : %s", QSTRING_CSTR( deviceName )); + Debug(_log, "Model : %s", QSTRING_CSTR( _deviceModel )); + Debug(_log, "Bridge-ID : %s", QSTRING_CSTR( deviceBridgeID )); + Debug(_log, "SoftwareVersion : %s", QSTRING_CSTR( _deviceFirmwareVersion)); + Debug(_log, "API-Version : %u.%u.%u", _api_major,_api_minor, _api_patch ); + Debug(_log, "EntertainmentReady: %d", _isHueEntertainmentReady); + + 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 = keys.size(); + _lightsMap.clear(); + + for (uint i = 0; i < _ledCount; ++i) + { + _lightsMap.insert(keys.at(i).toInt(), jsonLightsInfo.take(keys.at(i)).toObject()); + } + + if (getLedCount() == 0 ) + { + setInError("No light-IDs found at the Philips Hue Bridge"); } else { - Error(_log,"Network Error: %s", QSTRING_CSTR(reply->errorString())); - bTimer.start(); + Debug(_log, "Lights found : %u", getLedCount() ); } } - reply->deleteLater(); + return isInitOK; } -void PhilipsHueBridge::post(QString route, QString content) +bool LedDevicePhilipsHueBridge::discoverDevice() { - //Debug(_log, "Post %s: %s", QSTRING_CSTR(QString("http://IP/api/USR/%1").arg(route)), QSTRING_CSTR(content)); + bool isDeviceFound (false); - QNetworkRequest request(QString("http://%1/api/%2/%3").arg(host).arg(username).arg(route)); - manager.put(request, content.toLatin1()); -} + // device searching by ssdp + QString address; + SSDPDiscover discover; -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 = -{ "LLC020", "LST002", "LCT011", "LCT012", "LCT010", "LCT014", "LCT015", "LCT016", "LCT024" }; - -PhilipsHueLight::PhilipsHueLight(Logger* log, PhilipsHueBridge* bridge, unsigned int id, QJsonObject values) - : _log(log) - , bridge(bridge) - , id(id) -{ - // Get state object values which are subject to change. - if (!values["state"].toObject().contains("on")) + // Discover Philips Hue Bridge + address = discover.getFirstService(STY_WEBSERVER, SSDP_ID, SSDP_TIMEOUT); + if ( address.isEmpty() ) { - Error(_log, "Got invalid state object from light ID %d", id); - } - QJsonObject state; - state["on"] = values["state"].toObject()["on"]; - on = false; - if (values["state"].toObject()["on"].toBool()) - { - state["xy"] = values["state"].toObject()["xy"]; - state["bri"] = values["state"].toObject()["bri"]; - on = true; - - color = { - (float) state["xy"].toArray()[0].toDouble(), - (float) state["xy"].toArray()[1].toDouble(), - (float) state["bri"].toDouble() / 255.0f - }; - transitionTime = values["state"].toObject()["transitiontime"].toInt(); - } - // Determine the model id. - modelId = values["modelid"].toString().trimmed().replace("\"", ""); - // Determine the original state. - originalState = QJsonDocument(state).toJson(QJsonDocument::JsonFormat::Compact).trimmed(); - // 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", modelId.toStdString().c_str(), id); - colorSpace.red = - { 0.704f, 0.296f}; - colorSpace.green = - { 0.2151f, 0.7106f}; - colorSpace.blue = - { 0.138f, 0.08f}; - } - 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", modelId.toStdString().c_str(), id); - colorSpace.red = - { 0.675f, 0.322f}; - colorSpace.green = - { 0.409f, 0.518f}; - colorSpace.blue = - { 0.167f, 0.04f}; - } - 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", modelId.toStdString().c_str(), id); - colorSpace.red = - { 0.6915f, 0.3083f}; - colorSpace.green = - { 0.17f, 0.7f}; - colorSpace.blue = - { 0.1532f, 0.0475f}; + Warning(_log, "No Philips Hue Bridge discovered"); } else { - Warning(_log, "Did not recognize model id %s of light ID %d", modelId.toStdString().c_str(), id); - colorSpace.red = - { 1.0f, 0.0f}; - colorSpace.green = - { 0.0f, 1.0f}; - colorSpace.blue = - { 0.0f, 0.0f}; + // Philips Hue Bridge found + Info(_log, "Philips Hue Bridge discovered at [%s]", QSTRING_CSTR( address )); + isDeviceFound = true; + QStringList addressparts = address.split(":", QString::SkipEmptyParts); + _hostname = addressparts[0]; + _api_port = addressparts[1]; + } + return isDeviceFound; +} + +const QMap& LedDevicePhilipsHueBridge::getLightMap(void) +{ + return _lightsMap; +} + +QString LedDevicePhilipsHueBridge::getUrl(QString host, QString port, QString auth_token, QString endpoint) const { + return QString(API_URL_FORMAT).arg(host, port, auth_token, endpoint); +} + +QJsonDocument LedDevicePhilipsHueBridge::getJson(QString url) +{ + + DebugIf(verbose, _log, "GET: [%s]", QSTRING_CSTR( url )); + + // Perfrom request + QNetworkRequest request(url); + QNetworkReply* reply = _networkmanager->get(request); + // Connect requestFinished signal to quit slot of the loop. + QEventLoop loop; + loop.connect(reply, SIGNAL(finished()), SLOT(quit())); + // Go into the loop until the request is finished. + loop.exec(); + + QJsonDocument jsonDoc; + if(reply->operation() == QNetworkAccessManager::GetOperation) + { + jsonDoc = handleReply( reply ); + } + // Free space. + reply->deleteLater(); + // Return response + return jsonDoc; +} + +QJsonDocument LedDevicePhilipsHueBridge::putJson(QString url, QString json) +{ + + DebugIf(verbose, _log, "PUT: [%s] [%s]", QSTRING_CSTR( url ), QSTRING_CSTR( json ) ); + // Perfrom request + QNetworkRequest request(url); + QNetworkReply* reply = _networkmanager->put(request, json.toUtf8()); + // Connect requestFinished signal to quit slot of the loop. + QEventLoop loop; + loop.connect(reply, SIGNAL(finished()), SLOT(quit())); + // Go into the loop until the request is finished. + loop.exec(); + + QJsonDocument jsonDoc; + if(reply->operation() == QNetworkAccessManager::PutOperation) + { + jsonDoc = handleReply( reply ); + } + // Free space. + reply->deleteLater(); + + // Return response + return jsonDoc; +} + +QJsonDocument LedDevicePhilipsHueBridge::handleReply(QNetworkReply* const &reply ) +{ + QJsonDocument jsonDoc; + + int httpStatusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + DebugIf(verbose, _log, "Reply.httpStatusCode [%d]", httpStatusCode ); + QString errorReason; + + if(reply->error() == QNetworkReply::NoError) + { + if ( httpStatusCode != 204 ){ + QByteArray response = reply->readAll(); + QJsonParseError error; + jsonDoc = QJsonDocument::fromJson(response, &error); + if (error.error != QJsonParseError::NoError) + { + this->setInError ( "Got invalid response" ); + } + else + { + QString strJson(jsonDoc.toJson(QJsonDocument::Compact)); + DebugIf(verbose, _log, "Reply: [%s]", strJson.toUtf8().constData() ); + + QVariantList rspList = jsonDoc.toVariant().toList(); + if ( !rspList.isEmpty() ) + { + QVariantMap map = rspList.first().toMap(); + if ( map.contains(API_ERROR) ) + { + // API call failsed 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(); + + Debug(_log, "Error Type : %s", QSTRING_CSTR( errorType )); + Debug(_log, "Error Address : %s", QSTRING_CSTR( errorAddress )); + Debug(_log, "Error Description : %s", QSTRING_CSTR( errorDesc )); + + errorReason = QString ("(%1) %2, Resource:%3").arg(errorType, errorDesc, errorAddress); + this->setInError ( errorReason ); + } + } + } + } + } + else + { + if ( httpStatusCode > 0 ) { + QString httpReason = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ).toString(); + QString advise; + switch ( httpStatusCode ) { + case 400: + advise = "Check Request Body"; + break; + case 401: + advise = "Check Authentication Token (API Key)"; + break; + case 404: + advise = "Check Resource given"; + break; + default: + break; + } + errorReason = QString ("%1:%2 [%3 %4] - %5").arg(_hostname, _api_port, QString(httpStatusCode) , httpReason, advise); + } + else { + errorReason = QString ("%1:%2 - %3").arg(_hostname, _api_port, reply->errorString()); + } + this->setInError ( errorReason ); + } + // Return response + return jsonDoc; +} + +void LedDevicePhilipsHueBridge::post(const QString& route, const QString& content) +{ + QString url = getUrl(_hostname, _api_port, _username, route ); + putJson( url, content ); +} + +void LedDevicePhilipsHueBridge::setLightState(const unsigned int lightId, QString state) +{ + Debug(_log, "SetLightState [%u]: %s", lightId, QSTRING_CSTR(state)); + post( QString("%1/%2/%3").arg(API_LIGHTS).arg(lightId).arg(API_STATE), state ); +} + +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 = + { "LLC020", "LST002", "LCT011", "LCT012", "LCT010", "LCT014", "LCT015", "LCT016", "LCT024" }; + +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}; } - Info(_log,"Light ID %d created", id); + saveOriginalState(values); + + _lightname = values["name"].toString().trimmed().replace("\"", ""); + Info(_log,"Light ID %d (\"%s\", LED index \"%d\") created", id, QSTRING_CSTR(_lightname), ledidx); } PhilipsHueLight::~PhilipsHueLight() { - // Restore the original state. - set(originalState); } -void PhilipsHueLight::set(QString state) +unsigned int PhilipsHueLight::getId() const { - bridge->post(QString("lights/%1/state").arg(id), state); + return _id; } -void PhilipsHueLight::setOn(bool on) +QString PhilipsHueLight::getOriginalState() { - if (this->on != on) + return _originalState; +} + +void PhilipsHueLight::saveOriginalState(const QJsonObject& values) +{ + // Get state object values which are subject to change. + if (!values[API_STATE].toObject().contains("on")) { - QString arg = on ? "true" : "false"; - set(QString("{ \"on\": %1 }").arg(arg)); + Error(_log, "Got invalid state object from light ID %d", _id); } - this->on = on; + 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(unsigned int transitionTime) { - if (this->transitionTime != transitionTime) - { - set(QString("{ \"transitiontime\": %1 }").arg(transitionTime)); - } - this->transitionTime = transitionTime; + this->_transitionTime = transitionTime; } -void PhilipsHueLight::setColor(CiColor color, float brightnessFactor) +void PhilipsHueLight::setColor(const CiColor& color) { - if (this->color != color) - { - const int bri = qRound(qMin(254.0f, brightnessFactor * qMax(1.0f, color.bri * 254.0f))); - set(QString("{ \"xy\": [%1, %2], \"bri\": %3 }").arg(color.x, 0, 'f', 4).arg(color.y, 0, 'f', 4).arg(bri)); - } - this->color = color; + this->_color = color; +} + +bool PhilipsHueLight::getOnOffState() const +{ + return _on; +} + +unsigned int PhilipsHueLight::getTransitionTime() const +{ + return _transitionTime; } CiColor PhilipsHueLight::getColor() const { - return color; + return _color; } CiColorTriangle PhilipsHueLight::getColorSpace() const { - return colorSpace; + return _colorSpace; +} + +LedDevicePhilipsHue::LedDevicePhilipsHue(const QJsonObject& deviceConfig) + : LedDevicePhilipsHueBridge(deviceConfig) + , _switchOffOnBlack (false) + , _brightnessFactor(1.0) + , _transitionTime (1) + , _isRestoreOrigState (true) +{ + _devConfig = deviceConfig; + _deviceReady = false; } LedDevice* LedDevicePhilipsHue::construct(const QJsonObject &deviceConfig) @@ -345,74 +697,132 @@ LedDevice* LedDevicePhilipsHue::construct(const QJsonObject &deviceConfig) return new LedDevicePhilipsHue(deviceConfig); } -LedDevicePhilipsHue::LedDevicePhilipsHue(const QJsonObject& deviceConfig) - : LedDevice(deviceConfig) - , _bridge(nullptr) -{ - -} - LedDevicePhilipsHue::~LedDevicePhilipsHue() { - switchOff(); - delete _bridge; -} - -void LedDevicePhilipsHue::start() -{ - _bridge = new PhilipsHueBridge(_log, _devConfig["output"].toString(), _devConfig["username"].toString()); - _deviceReady = init(_devConfig); - - connect(_bridge, &PhilipsHueBridge::newLights, this, &LedDevicePhilipsHue::newLights); - connect(this, &LedDevice::enableStateChanged, this, &LedDevicePhilipsHue::stateChanged); } bool LedDevicePhilipsHue::init(const QJsonObject &deviceConfig) { - switchOffOnBlack = deviceConfig["switchOffOnBlack"].toBool(true); - brightnessFactor = (float) deviceConfig["brightnessFactor"].toDouble(1.0); - transitionTime = deviceConfig["transitiontime"].toInt(1); - QJsonArray lArray = deviceConfig["lightIds"].toArray(); + bool isInitOK = LedDevicePhilipsHueBridge::init(deviceConfig); - QJsonObject newDC = deviceConfig; - if(!lArray.empty()) + if ( isInitOK ) { - for(const auto i : lArray) + // Initiatiale LedDevice configuration and execution environment + _switchOffOnBlack = _devConfig[CONFIG_ON_OFF_BLACK].toBool(true); + _brightnessFactor = _devConfig[CONFIG_BRIGHTNESSFACTOR].toDouble(1.0); + _transitionTime = _devConfig[CONFIG_TRANSITIONTIME].toInt(1); + _isRestoreOrigState = _devConfig[CONFIG_RESTORE_STATE].toBool(true); + QJsonArray lArray = _devConfig[CONFIG_LIGHTIDS].toArray(); + + _lightIds.clear(); + if(!lArray.empty()) { - lightIds.push_back(i.toInt()); + for(const auto i : lArray) + { + _lightIds.push_back(i.toInt()); + } } - // get light info from bridge - _bridge->bConnect(); - // adapt latchTime to count of user lightIds (bridge 10Hz max overall) - newDC.insert("latchTime",QJsonValue(100*(int)lightIds.size())); + uint configuredLightsCount = _lightIds.size(); + Debug(_log, "Off on Black : %d", _switchOffOnBlack ); + Debug(_log, "Brightness Factor : %f", _brightnessFactor ); + Debug(_log, "Transition Time : %d", _transitionTime ); + Debug(_log, "Light IDs defined : %d", configuredLightsCount ); + + if ( configuredLightsCount == 0) + { + setInError("No light-IDs configured"); + isInitOK = false; + } } - else - { - Error(_log,"No light ID provided, abort"); - } - - LedDevice::init(newDC); - - return true; + return isInitOK; } -void LedDevicePhilipsHue::newLights(QMap map) +bool LedDevicePhilipsHue::initLeds() { - if(!lightIds.empty()) + bool isInitOK = false; + + if ( !isInError() ) + { + updateLights( getLightMap() ); + // adapt latchTime to count of user lightIds (bridge 10Hz max overall) + setLatchTime( static_cast( 100 * getLightsCount() ) ); + isInitOK = true; + } + + return isInitOK; +} + +void LedDevicePhilipsHue::updateLights(QMap map) +{ + if(!_lightIds.empty()) { // search user lightid inside map and create light if found - lights.clear(); - for(const auto id : lightIds) + _lights.clear(); + + unsigned int ledidx = 0; + _lights.reserve(_lightIds.size()); + for(const auto id : _lightIds) { if (map.contains(id)) { - lights.push_back(PhilipsHueLight(_log, _bridge, id, map.value(id))); + _lights.emplace_back(_log, id, map.value(id), ledidx); } else { - Error(_log,"Light id %d isn't used on this bridge", id); + Warning(_log,"Configured light-ID %d is not available at this bridge", id); } + ledidx++; + } + setLightsCount ( _lights.size() ); + } +} + +int LedDevicePhilipsHue::open() +{ + int retval = -1; + QString errortext; + _deviceReady = false; + + // General initialisation and configuration of LedDevice + if ( init(_devConfig) ) + { + if ( LedDevicePhilipsHueBridge::open() ) + // Open/Start LedDevice based on configuration + { + if ( initLeds() ) + { + // Everything is OK -> enable device + _deviceReady = true; + setEnable(true); + retval = 0; + } + } + } + return retval; +} + +void LedDevicePhilipsHue::restoreOriginalState() +{ + if(!_lightIds.empty()) + { + for (PhilipsHueLight& light : _lights) + { + setLightState(light.getId(),light.getOriginalState()); + } + } +} + +void LedDevicePhilipsHue::close() +{ + LedDevicePhilipsHueBridge::close(); + + if ( _deviceReady) + { + if ( _isRestoreOrigState ) + { + //Restore Philips Hue devices state + restoreOriginalState(); } } } @@ -420,47 +830,139 @@ void LedDevicePhilipsHue::newLights(QMap map) int LedDevicePhilipsHue::write(const std::vector & ledValues) { // lights will be empty sometimes - if(lights.empty()) return -1; + if(_lights.empty()) return -1; // more lights then leds, stop always - if(ledValues.size() < lights.size()) + if(ledValues.size() < getLightsCount() ) { - Error(_log,"More LightIDs configured than leds, each LightID requires one led!"); + Error(_log,"More light-IDs configured than leds, each light-ID requires one led!"); return -1; } - // 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.0f, color.green / 255.0f, color.blue / 255.0f, - light.getColorSpace()); - - if (switchOffOnBlack && xy.bri == 0) - { - light.setOn(false); - } - else - { - light.setOn(true); - // Write color if color has been changed. - light.setTransitionTime(transitionTime); - light.setColor(xy, brightnessFactor); - } - - idx++; - } + writeSingleLights (ledValues); return 0; } -void LedDevicePhilipsHue::stateChanged(bool newState) +int LedDevicePhilipsHue::writeSingleLights(const std::vector& ledValues) { - if(newState) - _bridge->bConnect(); - else - lights.clear(); + // 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()); + + if (_switchOffOnBlack && xy.bri == 0.0) + { + this->setOnOffState(light, false); + } + else + { + // Write color if color has been changed. + this->setState(light, true, xy, _brightnessFactor,_transitionTime); + } + idx++; + } + return 0; +} + +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, unsigned int transitionTime) +{ + if (light.getTransitionTime() != transitionTime) + { + light.setTransitionTime( transitionTime ); + setLightState( light.getId(), QString("{\"%1\": %2 }").arg(API_TRANSITIONTIME).arg( transitionTime ) ); + } +} + +void LedDevicePhilipsHue::setColor(PhilipsHueLight& light, const CiColor& color, double brightnessFactor) +{ + const int bri = qRound(qMin(254.0, brightnessFactor * qMax(1.0, color.bri * 254.0))); + if (light.getColor() != color) + { + light.setColor( color) ; + 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 ); + } +} + +void LedDevicePhilipsHue::setState(PhilipsHueLight& light, bool on, const CiColor& color, double brightnessFactor, unsigned int transitionTime) +{ + + 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; +} + + +int LedDevicePhilipsHue::switchOn() +{ + if ( _deviceReady) + { + //Switch on Philips Hue devices physically + for (PhilipsHueLight& light : _lights) + { + setOnOffState(light,true); + } + } + return 0; +} + +int LedDevicePhilipsHue::switchOff() +{ + //Set all LEDs to Black + int rc = LedDevice::switchOff(); + + if ( _deviceReady) + { + //Switch off Philips Hue devices physically + for (PhilipsHueLight& light : _lights) + { + setOnOffState(light,false); + } + } + return rc; } diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.h b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.h index 655fe1cc..96332796 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.h @@ -19,11 +19,11 @@ struct CiColorTriangle; struct CiColor { /// X component. - float x; + double x; /// Y component. - float y; + double y; /// The brightness. - float bri; + double bri; /// /// Converts an RGB color to the Hue xy color space and brightness. @@ -37,7 +37,7 @@ struct CiColor /// /// @return color point /// - static CiColor rgbToCiColor(float red, float green, float blue, CiColorTriangle colorSpace); + static CiColor rgbToCiColor(double red, double green, double blue, CiColorTriangle colorSpace); /// /// @param p the color point to check @@ -53,7 +53,7 @@ struct CiColor /// /// @return the cross product between p1 and p2 /// - static float crossProduct(CiColor p1, CiColor p2); + static double crossProduct(CiColor p1, CiColor p2); /// /// @param a reference point one @@ -73,11 +73,11 @@ struct CiColor /// /// @return the distance between the two points /// - static float getDistanceBetweenTwoPoints(CiColor p1, CiColor p2); + static double getDistanceBetweenTwoPoints(CiColor p1, CiColor p2); }; -bool operator==(CiColor p1, CiColor p2); -bool operator!=(CiColor p1, CiColor p2); +bool operator==(const CiColor& p1, const CiColor& p2); +bool operator!=(const CiColor& p1, const CiColor& p2); /** * Color triangle to define an available color space for the hue lamps. @@ -87,74 +87,11 @@ struct CiColorTriangle CiColor red, green, blue; }; -class PhilipsHueBridge : public QObject -{ - Q_OBJECT - -private: - Logger* _log; - /// QNetworkAccessManager for sending requests. - QNetworkAccessManager manager; - /// Ip address of the bridge - QString host; - /// User name for the API ("newdeveloper") - QString username; - /// Timer for bridge reconnect interval - QTimer bTimer; - -private slots: - /// - /// Receive all replies and check for error, schedule reconnect on issues - /// Emits newLights() on success when triggered from connect() - /// - void resolveReply(QNetworkReply* reply); - -public slots: - /// - /// Connect to bridge to check availbility and user - /// - void bConnect(void); - -signals: - /// - /// Emits with a QMap of current bridge light/value pairs - /// - void newLights(QMap map); - -public: - PhilipsHueBridge(Logger* log, QString host, QString username); - - /// - /// @param route the route of the POST request. - /// - /// @param content the content of the POST request. - /// - void post(QString route, QString content); -}; - /** * Simple class to hold the id, the latest color, the color space and the original state. */ class PhilipsHueLight { -private: - Logger* _log; - PhilipsHueBridge* bridge; - /// light id - unsigned int id; - bool on; - unsigned int transitionTime; - CiColor color; - /// The model id of the hue lamp which is used to determine the color space. - QString modelId; - CiColorTriangle colorSpace; - /// The json string of the original state. - QString originalState; - - /// - /// @param state the state as json object to set - /// - void set(QString state); public: // Hue system model ids (http://www.developers.meethue.com/documentation/supported-lights). @@ -172,32 +109,183 @@ public: /// @param bridge the bridge /// @param id the light id /// - PhilipsHueLight(Logger* log, PhilipsHueBridge* bridge, unsigned int id, QJsonObject values); + PhilipsHueLight(Logger* log, unsigned int id, QJsonObject values, unsigned int ledidx); ~PhilipsHueLight(); /// /// @param on /// - void setOn(bool on); + void setOnOffState(bool on); /// /// @param transitionTime the transition time between colors in multiples of 100 ms /// - void setTransitionTime(unsigned int transitionTime); + void setTransitionTime(unsigned int _transitionTime); /// /// @param color the color to set - /// @param brightnessFactor the factor to apply to the CiColor#bri value /// - void setColor(CiColor color, float brightnessFactor = 1.0f); + void setColor(const CiColor& _color); + + + unsigned int getId() const; + + bool getOnOffState() const; + unsigned int getTransitionTime() const; CiColor getColor() const; /// /// @return the color space of the light determined by the model id reported by the bridge. CiColorTriangle getColorSpace() const; + + QString getOriginalState(); + + +private: + + void saveOriginalState(const QJsonObject& values); + + Logger* _log; + /// light id + unsigned int _id; + unsigned int _ledidx; + bool _on; + unsigned int _transitionTime; + CiColor _color; + /// darkes blue color in hue lamp GAMUT = black + CiColor _colorBlack; + /// The model id of the hue lamp which is used to determine the color space. + QString _modelId; + QString _lightname; + CiColorTriangle _colorSpace; + + /// The json string of the original state. + QJsonObject _originalStateJSON; + + QString _originalState; + CiColor _originalColor; }; +class LedDevicePhilipsHueBridge : public LedDevice +{ + Q_OBJECT + +public: + + explicit LedDevicePhilipsHueBridge(const QJsonObject &deviceConfig); + ~LedDevicePhilipsHueBridge(); + + /// + /// Sets configuration + /// + /// @param deviceConfig the json device config + /// @return true if success + virtual bool init(const QJsonObject &deviceConfig) override; + + + + /// + /// @param route the route of the POST request. + /// + /// @param content the content of the POST request. + /// + void post(const QString& route, const QString& content); + + void setLightState(unsigned int lightId = 0, QString state = ""); + + const QMap& getLightMap(); + +// /// Set device in error state +// /// +// /// @param errorMsg The error message to be logged +// /// +// virtual void setInError( const QString& errorMsg) override; + +public slots: + /// + /// Connect to bridge to check availbility and user + /// + virtual int open(void) override; + virtual int open( const QString& hostname, const QString& port, const QString& username ); + + //signals: + // /// + // /// Emits with a QMap of current bridge light/value pairs + // /// + // void newLights(QMap map); +protected: + + /// Ip address of the bridge + QString _hostname; + QString _api_port; + /// User name for the API ("newdeveloper") + QString _username; + +private: + + /// + /// Discover device via SSDP identifiers + /// + /// @return True, if device was found + /// + bool discoverDevice(); + + /// + /// Get command as url + /// + /// @param host Hostname or IP + /// @param port IP-Port + /// @param _auth_token Authorization token + /// @param Endpoint command for request + /// @return Url to execute endpoint/command + /// + QString getUrl(QString host, QString port, QString auth_token, QString endpoint) const; + + /// + /// Execute GET request + /// + /// @param url GET request for url + /// @return Response from device + /// + QJsonDocument getJson(QString url); + + /// + /// Execute PUT request + /// + /// @param Url for PUT request + /// @param json Command for request + /// @return Response from device + /// + QJsonDocument putJson(QString url, QString json); + + /// + /// Handle replys for GET and PUT requests + /// + /// @param reply Network reply + /// @return Response for request, if no error + /// + QJsonDocument handleReply(QNetworkReply* const &reply ); + + /// QNetworkAccessManager for sending requests. + QNetworkAccessManager* _networkmanager; + + //Philips Hue Bridge details + QString _deviceModel; + QString _deviceFirmwareVersion; + QString _deviceAPIVersion; + + uint _api_major; + uint _api_minor; + uint _api_patch; + + bool _isHueEntertainmentReady; + + QMap _lightsMap; +}; + + + /** * Implementation for the Philips Hue system. * @@ -206,7 +294,7 @@ public: * * @author ntim (github), bimsarck (github) */ -class LedDevicePhilipsHue: public LedDevice +class LedDevicePhilipsHue: public LedDevicePhilipsHueBridge { Q_OBJECT @@ -227,20 +315,66 @@ public: /// constructs leddevice static LedDevice* construct(const QJsonObject &deviceConfig); -public slots: - /// thread start - virtual void start() override; + /// + /// Sets configuration + /// + /// @param deviceConfig the json device config + /// @return true if success + virtual bool init(const QJsonObject &deviceConfig) override; + + /// Switch the device on + virtual int switchOn() override; + + /// Switch the device off + virtual int switchOff() override; -private slots: /// creates new PhilipsHueLight(s) based on user lightid with bridge feedback /// /// @param map Map of lightid/value pairs of bridge /// void newLights(QMap map); - void stateChanged(bool newState); + unsigned int getLightsCount() const { return _lightsCount; } + void setLightsCount( unsigned int lightsCount); + + void setOnOffState(PhilipsHueLight& light, bool on); + void setTransitionTime(PhilipsHueLight& light, unsigned int transitionTime); + void setColor(PhilipsHueLight& light, const CiColor& color, double brightnessFactor); + void setState(PhilipsHueLight& light, bool on, const CiColor& color, double brightnessFactor, unsigned int transitionTime); + + void restoreOriginalState(); + +public slots: + + /// + /// Closes the output device. + /// Includes switching-off the device and stopping refreshes + /// + virtual void close() override; + +private slots: + /// creates new PhilipsHueLight(s) based on user lightid with bridge feedback + /// + /// @param map Map of lightid/value pairs of bridge + /// + void updateLights(QMap map); protected: + + /// + /// Opens and initiatialises the output device + /// + /// @return Zero on succes (i.e. device is ready and enabled) else negative + /// + virtual int open() override; + + /// + /// Get Philips Hue device details and configuration + /// + /// @return True, if Nanoleaf device capabilities fit configuration + /// + bool initLeds(); + /// /// Writes the RGB-Color values to the leds. /// @@ -249,21 +383,27 @@ protected: /// @return Zero on success else negative /// virtual int write(const std::vector & ledValues) override; - bool init(const QJsonObject &deviceConfig) override; private: - /// bridge class - PhilipsHueBridge* _bridge; + + int writeSingleLights(const std::vector& ledValues); /// - bool switchOffOnBlack; + bool _switchOffOnBlack; /// The brightness factor to multiply on color change. - float brightnessFactor; - /// Transition time in multiples of 100 ms. + double _brightnessFactor; + /// Transition time in multiples of 100 ms. /// The default of the Hue lights is 400 ms, but we may want it snapier. - int transitionTime; + unsigned int _transitionTime; + + bool _isRestoreOrigState; + /// Array of the light ids. - std::vector lightIds; + std::vector _lightIds; /// Array to save the lamps. - std::vector lights; + std::vector _lights; + + unsigned int _lightsCount; + + }; diff --git a/libsrc/leddevice/dev_spi/ProviderSpi.cpp b/libsrc/leddevice/dev_spi/ProviderSpi.cpp index 4787484a..588c8ddd 100644 --- a/libsrc/leddevice/dev_spi/ProviderSpi.cpp +++ b/libsrc/leddevice/dev_spi/ProviderSpi.cpp @@ -93,7 +93,7 @@ int ProviderSpi::open() } if ( retval < 0 ) { - errortext = QString ("Failed to open device (%1). Error Code: %2").arg(_deviceName, retval); + errortext = QString ("Failed to open device (%1). Error Code: %2").arg(_deviceName).arg(retval); } } diff --git a/libsrc/leddevice/schemas/schema-philipshue.json b/libsrc/leddevice/schemas/schema-philipshue.json index 9ebe806d..d90a85d6 100644 --- a/libsrc/leddevice/schemas/schema-philipshue.json +++ b/libsrc/leddevice/schemas/schema-philipshue.json @@ -35,6 +35,13 @@ "maximum" : 10.0, "propertyOrder" : 5 }, + "restoreOriginalState": { + "type": "boolean", + "title":"edt_dev_spec_restoreOriginalState_title", + "default" : true, + "propertyOrder" : 6 + }, + "lightIds": { "type": "array", "title":"edt_dev_spec_lightid_title", @@ -45,7 +52,7 @@ "minimum" : 0, "title" : "edt_dev_spec_lightid_itemtitle" }, - "propertyOrder" : 6 + "propertyOrder" : 7 } }, "additionalProperties": true