/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2020, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. * This project including source code and documentation is protected by * copyright law, and remains the property of nymea GmbH. All rights, including * reproduction, publication, editing and translation, are reserved. The use of * this project is subject to the terms of a license agreement to be concluded * with nymea GmbH in accordance with the terms of use of nymea GmbH, available * under https://nymea.io/license * * GNU Lesser General Public License Usage * Alternatively, this project may be redistributed and/or modified under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; version 3. This project is distributed in the hope that * it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this project. If not, see . * * For any further details and any questions please contact us under * contact@nymea.io or see our FAQ/Licensing Information on * https://nymea.io/license/faq * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "integrationpluginlifx.h" #include "integrations/integrationplugin.h" #include "types/param.h" #include "plugininfo.h" #include "platform/platformzeroconfcontroller.h" #include "network/zeroconf/zeroconfservicebrowser.h" #include #include #include #include #include #include #include #include #include IntegrationPluginLifx::IntegrationPluginLifx() { } void IntegrationPluginLifx::init() { m_connectedStateTypeIds.insert(colorBulbThingClassId, colorBulbConnectedStateTypeId); m_connectedStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbConnectedStateTypeId); m_connectedStateTypeIds.insert(lifxAccountThingClassId, lifxAccountConnectedStateTypeId); m_powerStateTypeIds.insert(colorBulbThingClassId, colorBulbPowerStateTypeId); m_powerStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbPowerStateTypeId); m_brightnessStateTypeIds.insert(colorBulbThingClassId, colorBulbBrightnessStateTypeId); m_brightnessStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbBrightnessStateTypeId); m_colorTemperatureStateTypeIds.insert(colorBulbThingClassId, colorBulbColorTemperatureStateTypeId); m_colorTemperatureStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbColorTemperatureStateTypeId); m_idParamTypeIds.insert(colorBulbThingClassId, colorBulbThingIdParamTypeId); m_idParamTypeIds.insert(dimmableBulbThingClassId, dimmableBulbThingIdParamTypeId); m_serviceBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_hap._tcp"); // discovers all homekit devices // TODO for LAN connection, get id and device features // QFile file; // file.setFileName("/tmp/products.json"); // if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { // qCWarning(dcLifx()) << "Could not open products file" << file.errorString() << "file name:" << file.fileName(); // } else { // QJsonDocument productsJson = QJsonDocument::fromJson(file.readAll()); // file.close(); // if (!productsJson.isArray()) { // qCWarning(dcLifx()) << "Products JSON is not a valid array"; // } else { // QJsonArray productsArray = productsJson.array().first().toObject().value("products").toArray(); // foreach (QJsonValue value, productsArray) { // QJsonObject object = value.toObject(); // LifxLan::LifxProduct product; // product.pid = object["pid"].toInt(); // product.name = object["name"].toString(); // qCDebug(dcLifx()) << "Lifx product JSON, found product. PID:" << product.pid << "Name" << product.name; // QJsonObject features = object["features"].toObject(); // product.color = features["color"].toBool(); // product.infrared = features["infrared"].toBool(); // product.matrix = features["matrix"].toBool(); // product.multizone = features["multizone"].toBool(); // product.minColorTemperature = features["temperature_range"].toArray().first().toInt(); // product.maxColorTemperature = features["temperature_range"].toArray().last().toInt(); // product.chain = features["chain"].toBool(); // m_lifxProducts.insert(product.pid, product); // } // } // } m_networkManager = hardwareManager()->networkManager(); } void IntegrationPluginLifx::startPairing(ThingPairingInfo *info) { QUrl url("https://api.lifx.com/v1"); QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, info, [reply, info] { if (reply->error() == QNetworkReply::NetworkError::HostNotFoundError) { info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("LIFX server is not reachable.")); } else { info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter your user name and token. Get the token from https://cloud.lifx.com/settings")); } }); } void IntegrationPluginLifx::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) { QNetworkRequest request; request.setUrl(QUrl("https://api.lifx.com/v1/lights/all")); request.setRawHeader("Authorization","Bearer "+secret.toUtf8()); QNetworkReply *reply = m_networkManager->get(request); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, info, [info, reply, secret, username, this] { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // check HTTP status code if (status != 200) { // Error setting up device with invalid token info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("The token is invalid.")); return; } qCDebug(dcLifx()) << "Confirm pairing successfull"; pluginStorage()->beginGroup(info->thingId().toString()); pluginStorage()->setValue("username", username); pluginStorage()->setValue("token", secret); pluginStorage()->endGroup(); info->finish(Thing::ThingErrorNoError); }); } void IntegrationPluginLifx::discoverThings(ThingDiscoveryInfo *info) { // NOTE: the LAN API is not yet finished, to enable LAN discovery add "discovery" to the createMethods if ((info->thingClassId() == colorBulbThingClassId) || (info->thingClassId() == dimmableBulbThingClassId)) { QHash descriptors; foreach (const ZeroConfServiceEntry avahiEntry, m_serviceBrowser->serviceEntries()) { if (!avahiEntry.name().contains("lifx", Qt::CaseSensitivity::CaseInsensitive)) { continue; } QString id; QString model; foreach (const QString &txt, avahiEntry.txt()) { //qCDebug(dcLifx()) << "txt entry. Key:" << txt.split("=").first() << "value:" << txt.split("=").last(); if (txt.startsWith("id", Qt::CaseSensitivity::CaseInsensitive)) { id = txt.split("=").last(); } else if (txt.startsWith("md")) { model = txt.split("=").last(); } } if (descriptors.contains(id)) { // Might appear multiple times, IPv4 and IPv6 continue; } qCDebug(dcLifx()) << "Found LIFX device" << model << "ID" << id; ThingDescriptor descriptor(info->thingClassId(), model, avahiEntry.name() + " (" + avahiEntry.hostAddress().toString() + ")"); ParamList params; params << Param(m_idParamTypeIds.value(info->thingClassId()), id); params << Param(m_hostAddressParamTypeIds.value(info->thingClassId()), avahiEntry.hostAddress().toString()); params << Param(m_portParamTypeIds.value(info->thingClassId()), avahiEntry.port()); descriptor.setParams(params); Things existing = myThings().filterByParam(m_idParamTypeIds.value(info->thingClassId()), id); if (existing.count() > 0) { descriptor.setThingId(existing.first()->id()); } descriptors.insert(id, descriptor); } info->addThingDescriptors(descriptors.values()); info->finish(Thing::ThingErrorNoError); } else { Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8()); } } void IntegrationPluginLifx::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); if (thing->thingClassId() == colorBulbThingClassId || thing->thingClassId() == dimmableBulbThingClassId) { if (thing->parentId().isNull()) { // Lifx LAN //LifxLan *lifx = new LifxLan(, this); //if(lifx->enable()) { // m_lifxLanConnections.insert(thing, lifx); //TODO async setup for LAN devices // info->finish(Thing::ThingErrorNoError); //} else { // lifx->deleteLater(); info->finish(Thing::ThingErrorSetupFailed); //} } else { // Lifx Cloud info->finish(Thing::ThingErrorNoError); } } else if (thing->thingClassId() == lifxAccountThingClassId) { pluginStorage()->beginGroup(thing->id().toString()); QByteArray token = pluginStorage()->value("token").toByteArray(); QByteArray username = pluginStorage()->value("username").toByteArray(); pluginStorage()->endGroup(); if (token.isEmpty()) { qCWarning(dcLifx()) << "Lifx setup, token is not stored"; return info->finish(Thing::ThingErrorAuthenticationFailure); } thing->setStateValue(lifxAccountUserDisplayNameStateTypeId, username); LifxCloud *lifxCloud = new LifxCloud(hardwareManager()->networkManager(), this); m_asyncCloudSetups.insert(lifxCloud, info); connect(info, &ThingSetupInfo::aborted, info, [lifxCloud, this] { m_asyncCloudSetups.remove(lifxCloud); lifxCloud->deleteLater(); }); connect(lifxCloud, &LifxCloud::lightsListReceived, this, &IntegrationPluginLifx::onLifxCloudLightsListReceived); connect(lifxCloud, &LifxCloud::scenesListReceived, this, &IntegrationPluginLifx::onLifxCloudScenesListReceived); connect(lifxCloud, &LifxCloud::requestExecuted, this, &IntegrationPluginLifx::onLifxCloudRequestExecuted); connect(lifxCloud, &LifxCloud::connectionChanged, this, &IntegrationPluginLifx::onLifxCloudConnectionChanged); connect(lifxCloud, &LifxCloud::authenticationChanged, this, &IntegrationPluginLifx::onLifxCloudAuthenticationChanged); lifxCloud->setAuthorizationToken(token); lifxCloud->listLights(); QTimer::singleShot(2000, info, [this, info] { setupThing(info); }); } else { Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); } } void IntegrationPluginLifx::postSetupThing(Thing *thing) { if (!m_pluginTimer) { m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(15); connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() { foreach (LifxLan *lifx, m_lifxLanConnections) { Q_UNUSED(lifx) //TODO update LAN device states } foreach (LifxCloud *lifx, m_lifxCloudConnections) { lifx->listLights(); } }); } if (thing->thingClassId() == lifxAccountThingClassId) { thing->setStateValue(lifxAccountConnectedStateTypeId, true); thing->setStateValue(lifxAccountLoggedInStateTypeId, true); } } void IntegrationPluginLifx::executeAction(ThingActionInfo *info) { Thing *thing = info->thing(); Action action = info->action(); bool cloudDevice = false; LifxLan *lifx = nullptr; LifxCloud *lifxCloud = nullptr; if (m_lifxLanConnections.contains(thing)) { // Local connection first lifx = m_lifxLanConnections.value(thing); } else if (m_lifxCloudConnections.contains(myThings().findById(thing->parentId()))) { lifxCloud = m_lifxCloudConnections.value(myThings().findById(thing->parentId())); cloudDevice = true; } else { qCWarning(dcLifx()) << "Could not find any LIFX connection for thing" << thing->name(); return info->finish(Thing::ThingErrorHardwareFailure); } if (thing->thingClassId() == colorBulbThingClassId) { QByteArray lightId = thing->paramValue(colorBulbThingIdParamTypeId).toByteArray(); if (action.actionTypeId() == colorBulbPowerActionTypeId) { bool power = action.param(colorBulbPowerActionPowerParamTypeId).value().toBool(); int requestId; if (cloudDevice) { requestId = lifxCloud->setPower(lightId, power); } else { requestId = lifx->setPower(power); } connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); m_asyncActions.insert(requestId, info); } else if (action.actionTypeId() == colorBulbBrightnessActionTypeId) { if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()){ if (cloudDevice) { lifxCloud->setPower(lightId, true); } else { lifx->setPower(true); } } int brightness = info->action().param(colorBulbBrightnessActionBrightnessParamTypeId).value().toInt(); int requestId; if (cloudDevice) { requestId = lifxCloud->setBrightnesss(lightId, brightness); } else { requestId = lifx->setBrightness(brightness); } connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); m_asyncActions.insert(requestId, info); } else if (action.actionTypeId() == colorBulbColorActionColorParamTypeId) { QRgb color = QColor(action.param(colorBulbColorActionColorParamTypeId).value().toString()).rgba(); if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()){ if (cloudDevice) { lifxCloud->setPower(lightId, true); } else { lifx->setPower(true); } } int requestId; if (cloudDevice) { requestId = lifxCloud->setColor(lightId, color); } else { requestId = lifx->setColor(color); } connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); m_asyncActions.insert(requestId, info); } else if (action.actionTypeId() == colorBulbColorTemperatureActionTypeId) { int colorTemperature = 6500 - (action.param(colorBulbColorTemperatureActionColorTemperatureParamTypeId).value().toUInt() * 8); //range 2500 to 6500 kelvin if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()){ if (cloudDevice) { lifxCloud->setPower(lightId, true); } else { lifx->setPower(true); } } int requestId; if (cloudDevice) { requestId = lifxCloud->setColorTemperature(lightId, colorTemperature); } else { requestId = lifx->setColorTemperature(colorTemperature); } connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); m_asyncActions.insert(requestId, info); } else if (action.actionTypeId() == colorBulbEffectStateTypeId) { if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()){ if (cloudDevice) { lifxCloud->setPower(lightId, true); } else { lifx->setPower(true); } } QString effectString = action.param(colorBulbEffectActionEffectParamTypeId).value().toString(); int requestId; LifxCloud::Effect effect = LifxCloud::EffectNone; if (effectString == "None") { effect = LifxCloud::EffectNone; } else if (effectString == "Breathe") { effect = LifxCloud::EffectBreathe; } else if (effectString == "Pulse") { effect = LifxCloud::EffectPulse; } if (cloudDevice) { //QColor color = QColor(thing->stateValue(colorBulbColorStateTypeId).toString()); requestId = lifxCloud->setEffect(lightId, effect, "#FFFFFF"); } else { qCWarning(dcLifx()) << "LAN devices are not yet supported"; info->finish(Thing::ThingErrorHardwareNotAvailable); return; } connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); m_asyncActions.insert(requestId, info); } else { Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8()); } } else if (thing->thingClassId() == dimmableBulbThingClassId) { QByteArray lightId = thing->paramValue(dimmableBulbThingIdParamTypeId).toByteArray(); if (action.actionTypeId() == dimmableBulbPowerActionTypeId) { bool power = action.param(dimmableBulbPowerActionPowerParamTypeId).value().toBool(); int requestId; if (cloudDevice) { requestId = lifxCloud->setPower(lightId, power); } else { requestId = lifx->setPower(power); } connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); } else if (action.actionTypeId() == dimmableBulbBrightnessActionTypeId) { int brightness = action.param(dimmableBulbBrightnessActionBrightnessParamTypeId).value().toInt(); if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()){ if (cloudDevice) { lifxCloud->setPower(lightId, true); } else { lifx->setPower(true); } } int requestId; if (cloudDevice) { requestId = lifxCloud->setBrightnesss(lightId, brightness); } else { requestId = lifx->setBrightness(brightness); } connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); m_asyncActions.insert(requestId, info); } else { Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8()); } } else { Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); } } void IntegrationPluginLifx::thingRemoved(Thing *thing) { if (thing->thingClassId() == colorBulbThingClassId || thing->thingClassId() == dimmableBulbThingClassId) { if (m_lifxLanConnections.contains(thing)) m_lifxLanConnections.take(thing)->deleteLater(); } else if (thing->thingClassId() == lifxAccountThingClassId) { if (m_lifxCloudConnections.contains(thing)) m_lifxCloudConnections.take(thing)->deleteLater(); } if (myThings().isEmpty()) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); m_pluginTimer = nullptr; } } void IntegrationPluginLifx::browseThing(BrowseResult *result) { Thing *thing = result->thing(); LifxCloud *lifxCloud = m_lifxCloudConnections.value(thing); if (!lifxCloud) return; lifxCloud->listScenes(); m_asyncBrowseResults.insert(lifxCloud, result); connect(result, &BrowseResult::aborted, this, [lifxCloud, this]{m_asyncBrowseResults.remove(lifxCloud);}); } void IntegrationPluginLifx::browserItem(BrowserItemResult *result) { Q_UNUSED(result) qCDebug(dcLifx()) << "BrowserItem called"; } void IntegrationPluginLifx::executeBrowserItem(BrowserActionInfo *info) { Thing *thing = info->thing(); LifxCloud *lifxCloud = m_lifxCloudConnections.value(thing); int requestId = lifxCloud->activateScene(info->browserAction().itemId()); m_asyncBrowserItem.insert(requestId, info); connect(info, &BrowserActionInfo::aborted, this, [requestId, this] {m_asyncBrowserItem.remove(requestId);}); } void IntegrationPluginLifx::onLifxLanConnectionChanged(bool connected) { Q_UNUSED(connected) LifxLan *lifx = static_cast(sender()); Thing *thing = m_lifxLanConnections.key(lifx); if (!thing) return; thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), connected); } void IntegrationPluginLifx::onLifxLanRequestExecuted(int requestId, bool success) { if (m_asyncActions.contains(requestId)) { ThingActionInfo *info = m_asyncActions.take(requestId); if (success) { info->finish(Thing::ThingErrorNoError); } else { info->finish(Thing::ThingErrorHardwareFailure); } } else if (m_asyncBrowserItem.contains(requestId)) { BrowserActionInfo *info = m_asyncBrowserItem.take(requestId); if (success) { info->finish(Thing::ThingErrorNoError); } else { info->finish(Thing::ThingErrorHardwareNotAvailable); } } } void IntegrationPluginLifx::onLifxCloudConnectionChanged(bool connected) { LifxCloud *lifxCloud = static_cast(sender()); Thing *accountThing = m_lifxCloudConnections.key(lifxCloud); if (!accountThing) return; accountThing->setStateValue(m_connectedStateTypeIds.value(accountThing->thingClassId()), connected); foreach (Thing *thing, myThings().filterByParentId(accountThing->id())) { if (!connected) thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), connected); } } void IntegrationPluginLifx::onLifxCloudAuthenticationChanged(bool authenticated) { LifxCloud *lifxCloud = static_cast(sender()); Thing *accountThing = m_lifxCloudConnections.key(lifxCloud); if (!accountThing) return; accountThing->setStateValue(lifxAccountLoggedInStateTypeId, authenticated); } void IntegrationPluginLifx::onLifxCloudLightsListReceived(const QList &lights) { LifxCloud *lifxCloud = static_cast(sender()); if (m_asyncCloudSetups.contains(lifxCloud)) { ThingSetupInfo *info = m_asyncCloudSetups.take(lifxCloud); m_lifxCloudConnections.insert(info->thing(), lifxCloud); info->finish(Thing::ThingErrorNoError); } ThingDescriptors thingDescriptors; Q_FOREACH(LifxCloud::Light light, lights) { Thing *parentThing = m_lifxCloudConnections.key(lifxCloud); if (!parentThing) { qCWarning(dcLifx()) << "Could not find thing to cloud connection"; return; } ThingClassId thingClassId; if (light.product.capabilities.color) { thingClassId = colorBulbThingClassId; } else if (light.product.capabilities.colorTemperature) { thingClassId = dimmableBulbThingClassId; } else { qCWarning(dcLifx()) << "LIFX product is not supported"; } qCDebug(dcLifx()) << "Light product:" << light.id << light.uuid << light.label << light.product.identifier; ThingDescriptor thingDescriptor(thingClassId, light.product.name, light.location.name, parentThing->id()); foreach (Thing * thing, myThings().filterByParam(m_idParamTypeIds.value(thingClassId), light.id)) { thing->setStateValue(m_connectedStateTypeIds.value(thingClassId), light.connected); thing->setStateValue(m_brightnessStateTypeIds.value(thingClassId), light.brightness*100.00); thing->setStateValue(m_colorTemperatureStateTypeIds.value(thingClassId), light.colorTemperature); //TODO Kelvin to mired thing->setStateValue(m_powerStateTypeIds.value(thingClassId), light.power); if (thingClassId == colorBulbThingClassId) { thing->setStateValue(colorBulbColorStateTypeId, light.color); } thingDescriptor.setThingId(thing->id()); break; } ParamList params; params << Param(m_idParamTypeIds.value(thingDescriptor.thingClassId()), light.id); params << Param(m_hostAddressParamTypeIds.value(thingDescriptor.thingClassId()), "-"); params << Param(m_portParamTypeIds.value(thingDescriptor.thingClassId()), 0); thingDescriptor.setParams(params); thingDescriptors.append(thingDescriptor); } if (!thingDescriptors.isEmpty()) autoThingsAppeared(thingDescriptors); } void IntegrationPluginLifx::onLifxCloudRequestExecuted(int requestId, bool success) { if (m_asyncActions.contains(requestId)) { ThingActionInfo *info = m_asyncActions.take(requestId); if (!info) { return; } if (success) { info->finish(Thing::ThingErrorNoError); } else { info->finish(Thing::ThingErrorHardwareNotAvailable); } } else if (m_asyncBrowserItem.contains(requestId)) { BrowserActionInfo *info = m_asyncBrowserItem.value(requestId); if (!info) { return; } if (success) { info->finish(Thing::ThingErrorNoError); } else { info->finish(Thing::ThingErrorHardwareNotAvailable); } } } void IntegrationPluginLifx::onLifxCloudScenesListReceived(const QList &scenes) { LifxCloud *lifxCloud = static_cast(sender()); Thing *thing = m_lifxCloudConnections.key(lifxCloud); if (!thing) return; qCDebug(dcLifx()) << "Scene list received, count: " << scenes.length(); if (m_asyncBrowseResults.contains(lifxCloud)) { BrowseResult *result = m_asyncBrowseResults.take(lifxCloud); foreach (LifxCloud::Scene scene, scenes) { BrowserItem item; item.setId(scene.id); item.setBrowsable(false); item.setExecutable(true); item.setDisplayName(scene.name); item.setDisabled(false); result->addItem(item); } result->finish(Thing::ThingErrorNoError); } }