From ebd094c57b5429f20de2c38dd20b8a1a7cb484f6 Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Tue, 24 Mar 2020 10:51:52 +0100 Subject: [PATCH] added pin setup method and discovery --- aqi/airqualityindex.cpp | 231 +++++++++++++++++++++++ aqi/airqualityindex.h | 91 +++++++++ aqi/aqi.pro | 2 + aqi/integrationpluginaqi.cpp | 294 +++++++++++++++++------------ aqi/integrationpluginaqi.h | 14 +- aqi/integrationpluginaqi.json | 26 ++- debian/nymea-plugin-aqi.install.in | 2 +- 7 files changed, 528 insertions(+), 132 deletions(-) create mode 100644 aqi/airqualityindex.cpp create mode 100644 aqi/airqualityindex.h diff --git a/aqi/airqualityindex.cpp b/aqi/airqualityindex.cpp new file mode 100644 index 00000000..3bb20fc0 --- /dev/null +++ b/aqi/airqualityindex.cpp @@ -0,0 +1,231 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "airqualityindex.h" +#include "extern-plugininfo.h" + +#include +#include +#include +#include +#include + +AirQualityIndex::AirQualityIndex(NetworkAccessManager *networkAccessManager, const QString &apiKey, QObject *parent) : + QObject(parent), + m_networkAccessManager(networkAccessManager), + m_apiKey(apiKey) +{ + +} + +void AirQualityIndex::setApiKey(const QString &apiKey) +{ + m_apiKey = apiKey; +} + +QUuid AirQualityIndex::searchByName(const QString &name) +{ + if (m_apiKey.isEmpty()) + qCWarning(dcAirQualityIndex()) << "API key is not set"; + + QUuid requestId; + QUrl url; + url.setUrl(m_baseUrl); + url.setPath("/search/"); + QUrlQuery query; + query.addQueryItem("token", m_apiKey); + query.addQueryItem("keyword", name); + url.setQuery(query); + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader("User-Agent", "nymea"); + + QNetworkReply *reply = m_networkAccessManager->get(request); + connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + if (status == 400) { + qCWarning(dcAirQualityIndex()) << "Request error due to exceeded request quota"; + } + requestExecuted(requestId, false); + qCWarning(dcAirQualityIndex()) << "Request error:" << status << reply->errorString(); + return; + } + QByteArray rawData = reply->readAll(); + qCDebug(dcAirQualityIndex()) << "Search response" << rawData; + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(rawData, &error); + if (error.error != QJsonParseError::NoError) { + emit requestExecuted(requestId, false); + qCWarning(dcAirQualityIndex()) << "Received invalide JSON object"; + return; + } + emit requestExecuted(requestId, true); + + QList stations; + QVariantList stationList = doc.toVariant().toMap().value("data").toList(); + foreach (QVariant stationVariant, stationList) { + Station station; + station.aqi = stationVariant.toMap().value("aqi").toInt(); + station.idx = stationVariant.toMap().value("idx").toInt(); + station.measurementTime = QTime::fromString(stationVariant.toMap().value("time").toMap().value("s").toString()); + station.timezone = stationVariant.toMap().value("time").toMap().value("tz").toString(); + station.name = stationVariant.toMap().value("city").toMap().value("name").toString(); + station.url = QUrl(stationVariant.toMap().value("city").toMap().value("url").toString()); + station.location.latitude = stationVariant.toMap().value("city").toMap().value("geo").toList().first().toDouble(); + station.location.longitude = stationVariant.toMap().value("city").toMap().value("geo").toList().last().toDouble(); + stations.append(station); + } + if (!stations.isEmpty()) + emit stationsReceived(requestId, stations); + }); + return requestId; +} + +QUuid AirQualityIndex::getDataByIp() +{ + if (m_apiKey.isEmpty()) + qCWarning(dcAirQualityIndex()) << "API key is not set"; + + QUuid requestId; + QUrl url; + url.setUrl(m_baseUrl); + url.setPath("/feed/here/"); + QUrlQuery query; + query.addQueryItem("token", m_apiKey); + url.setQuery(query); + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader("User-Agent", "nymea"); + qCDebug(dcAirQualityIndex()) << "Get data by IP request" << url.toString(); + QNetworkReply *reply = m_networkAccessManager->get(request); + connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + if (status == 400) { + qCWarning(dcAirQualityIndex()) << "Request error due to exceeded request quota"; + } + requestExecuted(requestId, false); + qCWarning(dcAirQualityIndex()) << "Request error:" << status << reply->errorString(); + return; + } + parseData(requestId, reply->readAll()); + }); + return requestId; +} + +QUuid AirQualityIndex::getDataByGeolocation(const QString &lat, const QString &lng) +{ + if (m_apiKey.isEmpty()) + qCWarning(dcAirQualityIndex()) << "API key is not set"; + + QUuid requestId; + QUrl url; + url.setUrl(m_baseUrl); + url.setPath("/feed/geo:"+lat+";"+lng+"/"); + QUrlQuery query; + query.addQueryItem("token", m_apiKey); + url.setQuery(query); + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader("User-Agent", "nymea"); + qCDebug(dcAirQualityIndex()) << "Get data by geo location request" << url.toString(); + QNetworkReply *reply = m_networkAccessManager->get(request); + connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { + reply->deleteLater(); + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check HTTP status code + if (status != 200 || reply->error() != QNetworkReply::NoError) { + if (status == 400) { + qCWarning(dcAirQualityIndex()) << "Request error due to exceeded request quota"; + } + requestExecuted(requestId, false); + qCWarning(dcAirQualityIndex()) << "Request error:" << status << reply->errorString(); + return; + } + requestExecuted(requestId, true); + parseData(requestId, reply->readAll()); + }); + return requestId; +} + + +void AirQualityIndex::parseData(QUuid requestId, const QByteArray &data) +{ + qCDebug(dcAirQualityIndex()) << "Parsing data" << data; + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + emit requestExecuted(requestId, false); + qCWarning(dcAirQualityIndex()) << "Received invalide JSON object"; + return; + } + emit requestExecuted(requestId, true); + Station station; + station.aqi = doc.toVariant().toMap().value("data").toMap().value("aqi").toInt(); + station.idx = doc.toVariant().toMap().value("data").toMap().value("idx").toInt(); + + QVariantMap city = doc.toVariant().toMap().value("data").toMap().value("city").toMap(); + if (city["geo"].toList().length() == 2) { + station.location.latitude = city["geo"].toList().first().toDouble(); + station.location.longitude = city["geo"].toList().last().toDouble(); + } else { + qCWarning(dcAirQualityIndex()) << "Parse data: geo location data list error" << city["geo"]; + } + station.name = city["name"].toString(); + station.url = city["url"].toString(); + + QVariantMap time = doc.toVariant().toMap().value("data").toMap().value("time").toMap(); + station.timezone = time["tz"].toString(); + station.measurementTime = QTime::fromString(time["s"].toString()); + emit stationsReceived(requestId, QList() << station); + + QVariantMap iaqi = doc.toVariant().toMap().value("data").toMap().value("iaqi").toMap(); + AirQualityData aqiData; + aqiData.humidity = iaqi["h"].toMap().value("v").toDouble(); + aqiData.pressure = iaqi["p"].toMap().value("v").toDouble(); + aqiData.pm25 = iaqi["pm25"].toMap().value("v").toInt(); + aqiData.pm10 = iaqi["pm10"].toMap().value("v").toInt(); + aqiData.so2 = iaqi["so2"].toMap().value("v").toDouble(); + aqiData.no2 = iaqi["no2"].toMap().value("v").toDouble(); + aqiData.o3 = iaqi["o3"].toMap().value("v").toDouble(); + aqiData.co = iaqi["co"].toMap().value("v").toDouble(); + aqiData.temperature = iaqi["t"].toMap().value("v").toDouble(); + aqiData.windSpeed = iaqi["w"].toMap().value("v").toDouble(); + emit dataReceived(requestId, aqiData); +} diff --git a/aqi/airqualityindex.h b/aqi/airqualityindex.h new file mode 100644 index 00000000..5f15b8d1 --- /dev/null +++ b/aqi/airqualityindex.h @@ -0,0 +1,91 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef AIRQUALITYINDEX_H +#define AIRQUALITYINDEX_H + +#include "network/networkaccessmanager.h" + +#include +#include +#include + +class AirQualityIndex : public QObject +{ + Q_OBJECT +public: + struct AirQualityData { + double humidity; + double pressure; + int pm25; + int pm10; + double so2; + double no2; + double o3; + double co; + double temperature; + double windSpeed; + }; + + struct GeoData { + double latitude; + double longitude; + }; + + struct Station { + int idx; + int aqi; + QTime measurementTime; + QString timezone; + QString name; + GeoData location; + QUrl url; + }; + + explicit AirQualityIndex(NetworkAccessManager *networkAccessManager, const QString &apiKey, QObject *parent = nullptr); + void setApiKey(const QString &apiKey); + QUuid searchByName(const QString &name); + QUuid getDataByIp(); + QUuid getDataByGeolocation(const QString &lat, const QString &lng); + +private: + NetworkAccessManager *m_networkAccessManager; + QString m_baseUrl = "https://api.waqi.info"; + QString m_apiKey; + + void parseData(QUuid requestId, const QByteArray &data); + +signals: + void stationsReceived(QUuid requestId, QList stations); + void requestExecuted(QUuid requestId, bool success); + void dataReceived(QUuid requestId, const AirQualityData &data); +}; + +#endif // AIRQUALITYINDEX_H diff --git a/aqi/aqi.pro b/aqi/aqi.pro index 679f1419..0c7b9893 100644 --- a/aqi/aqi.pro +++ b/aqi/aqi.pro @@ -3,8 +3,10 @@ include(../plugins.pri) QT+= network SOURCES += \ + airqualityindex.cpp \ integrationpluginaqi.cpp \ HEADERS += \ + airqualityindex.h \ integrationpluginaqi.h \ diff --git a/aqi/integrationpluginaqi.cpp b/aqi/integrationpluginaqi.cpp index 6b37c3c6..4e340505 100644 --- a/aqi/integrationpluginaqi.cpp +++ b/aqi/integrationpluginaqi.cpp @@ -32,59 +32,106 @@ #include "plugininfo.h" #include -#include -#include -#include -#include -#include IntegrationPluginAqi::IntegrationPluginAqi() { } +void IntegrationPluginAqi::startPairing(ThingPairingInfo *info) +{ + info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter your API token for Air Quality Index")); +} + +void IntegrationPluginAqi::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) +{ + Q_UNUSED(username) + + QNetworkRequest request(QUrl("https://api.waqi.info/feed/here/?token="+secret)); + QNetworkReply *reply = hardwareManager()->networkManager()->get(request); + connect(reply, &QNetworkReply::finished, info, [this, reply, info, secret](){ + reply->deleteLater(); + + 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("This token is not valid.")); + return; + } + + pluginStorage()->beginGroup(info->thingId().toString()); + pluginStorage()->setValue("apiKey", secret); + pluginStorage()->endGroup(); + info->finish(Thing::ThingErrorNoError); + }); +} + +void IntegrationPluginAqi::discoverThings(ThingDiscoveryInfo *info) +{ + if (!m_aqiConnection) { + QString apiKey = "74d31bb5ad9bcdeaed48097418b55188cb56d450"; //temporary key for discovery + m_aqiConnection = new AirQualityIndex(hardwareManager()->networkManager(), apiKey, this); + connect(m_aqiConnection, &AirQualityIndex::requestExecuted, this, &IntegrationPluginAqi::onRequestExecuted); + connect(m_aqiConnection, &AirQualityIndex::dataReceived, this, &IntegrationPluginAqi::onAirQualityDataReceived); + connect(m_aqiConnection, &AirQualityIndex::stationsReceived, this, &IntegrationPluginAqi::onAirQualityStationsReceived); + + connect(info, &ThingDiscoveryInfo::aborted, [this] { + m_aqiConnection->deleteLater(); + m_aqiConnection = nullptr; + }); + } else { + qCDebug(dcAirQualityIndex()) << "AQI connection alread created"; + } + QUuid requestId = m_aqiConnection->getDataByIp(); + m_asyncDiscovery.insert(requestId, info); +} + void IntegrationPluginAqi::setupThing(ThingSetupInfo *info) { if (info->thing()->thingClassId() == airQualityIndexThingClassId) { + if (!m_aqiConnection) { + pluginStorage()->beginGroup(info->thing()->id().toString()); + QString apiKey = pluginStorage()->value("apiKey").toString(); + pluginStorage()->endGroup(); + m_aqiConnection = new AirQualityIndex(hardwareManager()->networkManager(), apiKey, this); + connect(m_aqiConnection, &AirQualityIndex::requestExecuted, this, &IntegrationPluginAqi::onRequestExecuted); + connect(m_aqiConnection, &AirQualityIndex::dataReceived, this, &IntegrationPluginAqi::onAirQualityDataReceived); + connect(m_aqiConnection, &AirQualityIndex::stationsReceived, this, &IntegrationPluginAqi::onAirQualityStationsReceived); - if (!myThings().filterByThingClassId(info->thing()->thingClassId()).isEmpty()) { - if (!myThings().findById(info->thing()->id())) { - info->finish(Thing::ThingErrorSetupFailed, tr("Service is already in use.")); - return; - } + QString longitude = info->thing()->paramValue(airQualityIndexThingLongitudeParamTypeId).toString(); + QString latitude = info->thing()->paramValue(airQualityIndexThingLatitudeParamTypeId).toString(); + QUuid requestId = m_aqiConnection->getDataByGeolocation(latitude, longitude); + m_asyncSetups.insert(requestId, info); + + connect(info, &ThingSetupInfo::aborted, [requestId, this] { + m_asyncSetups.remove(requestId); + //m_aqiConnection->deleteLater(); + //m_aqiConnection = nullptr; + }); + } else { + info->finish(Thing::ThingErrorNoError); } - - QUrl url; - url.setUrl(m_baseUrl); - url.setPath("/feed/here/"); - url.setQuery("token="+configValue(airQualityIndexPluginApiKeyParamTypeId).toString()); - QNetworkRequest request; - request.setUrl(url); - request.setRawHeader("User-Agent", "nymea 1.0"); - - QNetworkReply *reply = hardwareManager()->networkManager()->get(request); - connect(reply, &QNetworkReply::finished, this, [reply, info, this] { - reply->deleteLater(); - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - // Check HTTP status code - if (status != 200 || reply->error() != QNetworkReply::NoError) { - if (status == 400 || status == 401) { - - } - qCWarning(dcAirQualityIndex()) << "Request error:" << status << reply->errorString(); - return info->finish(Thing::ThingErrorSetupFailed, reply->errorString()); - } - return info->finish(Thing::ThingErrorNoError); - }); + } else { + qCWarning(dcAirQualityIndex()) << "setupThing - thing class id not found" << info->thing()->thingClassId(); + info->finish(Thing::ThingErrorSetupFailed); } } void IntegrationPluginAqi::postSetupThing(Thing *thing) { - Q_UNUSED(thing); - getDataByIp(); + if (thing->thingClassId() == airQualityIndexThingClassId) { + + if (!m_aqiConnection) + return; + + QString longitude = thing->paramValue(airQualityIndexThingLongitudeParamTypeId).toString(); + QString latitude = thing->paramValue(airQualityIndexThingLatitudeParamTypeId).toString(); + QUuid requestId = m_aqiConnection->getDataByGeolocation(latitude, longitude); + m_asyncRequests.insert(requestId, thing->id()); + } if(!m_pluginTimer) { m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(60); @@ -95,103 +142,114 @@ void IntegrationPluginAqi::postSetupThing(Thing *thing) void IntegrationPluginAqi::thingRemoved(Thing *thing) { - Q_UNUSED(thing); + if (thing->thingClassId() == airQualityIndexThingClassId) { + + } if (myThings().empty()) { - hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); - m_pluginTimer = nullptr; + if (!m_pluginTimer) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + m_pluginTimer = nullptr; + } + if (!m_aqiConnection) { + m_aqiConnection->deleteLater(); + m_aqiConnection = nullptr; + } } } -void IntegrationPluginAqi::getDataByIp() +void IntegrationPluginAqi::onAirQualityDataReceived(QUuid requestId, AirQualityIndex::AirQualityData data) { - QUrl url; - url.setUrl(m_baseUrl); - url.setPath("/feed/here/"); - url.setQuery("token="+configValue(airQualityIndexPluginApiKeyParamTypeId).toString()); - QNetworkRequest request; - request.setUrl(url); - request.setRawHeader("User-Agent", "nymea"); + if (m_asyncSetups.contains(requestId)) { + ThingSetupInfo *info = m_asyncSetups.value(requestId); + return info->finish(Thing::ThingErrorNoError); + } - QNetworkReply *reply = hardwareManager()->networkManager()->get(request); - connect(reply, &QNetworkReply::finished, this, [reply, this] { - reply->deleteLater(); - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - // Check HTTP status code - if (status != 200 || reply->error() != QNetworkReply::NoError) { - foreach (Thing *thing, myThings().filterByThingClassId(airQualityIndexThingClassId)) { - thing->setStateValue(airQualityIndexConnectedStateTypeId, true); - } - qCWarning(dcAirQualityIndex()) << "Request error:" << status << reply->errorString(); + if (m_asyncRequests.contains(requestId)) { + Thing * thing = myThings().findById(m_asyncRequests.take(requestId)); + if (!thing) return; + + //thing->setStateValue(airQualityIndexStationNameStateTypeId, data); + thing->setStateValue(airQualityIndexConnectedStateTypeId, true); + thing->setStateValue(airQualityIndexCoStateTypeId, data.co); + thing->setStateValue(airQualityIndexHumidityStateTypeId, data.humidity); + thing->setStateValue(airQualityIndexTemperatureStateTypeId, data.temperature); + thing->setStateValue(airQualityIndexPressureStateTypeId, data.pressure); + thing->setStateValue(airQualityIndexO3StateTypeId, data.o3); + thing->setStateValue(airQualityIndexNo2StateTypeId, data.no2); + thing->setStateValue(airQualityIndexSo2StateTypeId, data.so2); + thing->setStateValue(airQualityIndexPm10StateTypeId, data.pm10); + thing->setStateValue(airQualityIndexPm25StateTypeId, data.pm25); + thing->setStateValue(airQualityIndexWindSpeedStateTypeId, data.windSpeed); + + if (data.pm25 <= 50.00) { + thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Good"); + thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("None")); + } else if ((data.pm25 > 50.00) && (data.pm25 <= 100.00)) { + thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Moderate"); + thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("Active children and adults, and people with respiratory disease, such as asthma, should limit prolonged outdoor exertion.")); + } else if ((data.pm25 > 100.00) && (data.pm25 <= 150.00)) { + thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Unhealthy for Sensitive Groups"); + thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("Active children and adults, and people with respiratory disease, such as asthma, should limit prolonged outdoor exertion.")); + } else if ((data.pm25 > 150.00) && (data.pm25 <= 200.00)) { + thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Unhealthy"); + thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("Active children and adults, and people with respiratory disease, such as asthma, should avoid prolonged outdoor exertion; everyone else, especially children, should limit prolonged outdoor exertion")); + } else if ((data.pm25 > 200.00) && (data.pm25 <= 300.00)) { + thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Very Unhealthy"); + thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("Active children and adults, and people with respiratory disease, such as asthma, should avoid all outdoor exertion; everyone else, especially children, should limit outdoor exertion.")); + } else { + thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Hazardous"); + thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("Everyone should avoid all outdoor exertion")); } - QJsonParseError error; - QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error); - if (error.error != QJsonParseError::NoError) { - qDebug(dcAirQualityIndex()) << "Received invalide JSON object"; + } +} + +void IntegrationPluginAqi::onAirQualityStationsReceived(QUuid requestId, QList stations) +{ + if (m_asyncDiscovery.contains(requestId)) { + ThingDiscoveryInfo *info = m_asyncDiscovery.take(requestId); + foreach(AirQualityIndex::Station station, stations) { + ThingDescriptor descriptor(airQualityIndexThingClassId, station.name, "Air Quality Index Station"); + ParamList params; + params << Param(airQualityIndexThingLatitudeParamTypeId, station.location.latitude); + params << Param(airQualityIndexThingLongitudeParamTypeId, station.location.longitude); + descriptor.setParams(params); + info->addThingDescriptor(descriptor); + } + info->finish(Thing::ThingErrorNoError); + } + + if (m_asyncRequests.contains(requestId)) { + Thing *thing = myThings().findById(m_asyncRequests.take(requestId)); + if (!thing) return; - } - QVariantMap city = data.toVariant().toMap().value("data").toMap().value("city").toMap(); - //double lat = city["geo"].toList().takeAt(0).toDouble(); - //double lng = city["geo"].toList().takeAt(1).toDouble(); - QString name = city["name"].toString(); - QString url = city["url"].toString(); - - QVariantMap iaqi = data.toVariant().toMap().value("data").toMap().value("iaqi").toMap(); - double humidity = iaqi["h"].toMap().value("v").toDouble(); - double pressure = iaqi["p"].toMap().value("v").toDouble(); - int pm25 = iaqi["pm25"].toMap().value("v").toInt(); - int pm10 = iaqi["pm10"].toMap().value("v").toInt(); - double so2 = iaqi["so2"].toMap().value("v").toDouble(); - double no2 = iaqi["no2"].toMap().value("v").toDouble(); - double o3 = iaqi["o3"].toMap().value("v").toDouble(); - double co = iaqi["co"].toMap().value("v").toDouble(); - double temperature = iaqi["t"].toMap().value("v").toDouble(); - double windSpeed = iaqi["w"].toMap().value("v").toDouble(); - - foreach (Thing *thing, myThings().filterByThingClassId(airQualityIndexThingClassId)) { - thing->setStateValue(airQualityIndexStationNameStateTypeId, name); - thing->setStateValue(airQualityIndexConnectedStateTypeId, true); - thing->setStateValue(airQualityIndexCoStateTypeId, co); - thing->setStateValue(airQualityIndexHumidityStateTypeId, humidity); - thing->setStateValue(airQualityIndexTemperatureStateTypeId, temperature); - thing->setStateValue(airQualityIndexPressureStateTypeId, pressure); - thing->setStateValue(airQualityIndexO3StateTypeId, o3); - thing->setStateValue(airQualityIndexNo2StateTypeId, no2); - thing->setStateValue(airQualityIndexSo2StateTypeId, so2); - thing->setStateValue(airQualityIndexPm10StateTypeId, pm10); - thing->setStateValue(airQualityIndexPm25StateTypeId, pm25); - thing->setStateValue(airQualityIndexWindSpeedStateTypeId, windSpeed); - - if (pm25 <= 50.00) { - thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Good"); - thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("None")); - } else if ((pm25 > 50.00) && (pm25 <= 100.00)) { - thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Moderate"); - thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("Active children and adults, and people with respiratory disease, such as asthma, should limit prolonged outdoor exertion.")); - } else if ((pm25 > 100.00) && (pm25 <= 150.00)) { - thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Unhealthy for Sensitive Groups"); - thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("Active children and adults, and people with respiratory disease, such as asthma, should limit prolonged outdoor exertion.")); - } else if ((pm25 > 150.00) && (pm25 <= 200.00)) { - thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Unhealthy"); - thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("Active children and adults, and people with respiratory disease, such as asthma, should avoid prolonged outdoor exertion; everyone else, especially children, should limit prolonged outdoor exertion")); - } else if ((pm25 > 200.00) && (pm25 <= 300.00)) { - thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Very Unhealthy"); - thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("Active children and adults, and people with respiratory disease, such as asthma, should avoid all outdoor exertion; everyone else, especially children, should limit outdoor exertion.")); - } else { - thing->setStateValue(airQualityIndexAirQualityStateTypeId, "Hazardous"); - thing->setStateValue(airQualityIndexCautionaryStatementStateTypeId, tr("Everyone should avoid all outdoor exertion")); - } - } - }); + thing->setStateValue(airQualityIndexConnectedStateTypeId, true); + } } void IntegrationPluginAqi::onPluginTimer() { - getDataByIp(); + if (!m_aqiConnection) + return; + + foreach (Thing *thing, myThings().filterByThingClassId(airQualityIndexThingClassId)) { + + QString longitude = thing->paramValue(airQualityIndexThingLongitudeParamTypeId).toString(); + QString latitude = thing->paramValue(airQualityIndexThingLatitudeParamTypeId).toString(); + QUuid requestId = m_aqiConnection->getDataByGeolocation(latitude, longitude); + m_asyncRequests.insert(requestId, thing->id()); + } } +void IntegrationPluginAqi::onRequestExecuted(QUuid requestId, bool success) +{ + if (m_asyncRequests.contains(requestId)) { - + Thing *thing = myThings().findById(m_asyncRequests.value(requestId)); + thing->setStateValue(airQualityIndexConnectedStateTypeId, success); + if (!success) + m_asyncRequests.remove(requestId); + } +} diff --git a/aqi/integrationpluginaqi.h b/aqi/integrationpluginaqi.h index 3b55eace..14808bfd 100644 --- a/aqi/integrationpluginaqi.h +++ b/aqi/integrationpluginaqi.h @@ -34,6 +34,7 @@ #include "plugintimer.h" #include "integrations/integrationplugin.h" #include "network/networkaccessmanager.h" +#include "airqualityindex.h" #include #include @@ -49,19 +50,26 @@ class IntegrationPluginAqi : public IntegrationPlugin public: explicit IntegrationPluginAqi(); + void startPairing(ThingPairingInfo *info) override; + void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override; + void discoverThings(ThingDiscoveryInfo *info) override; void setupThing(ThingSetupInfo *info) override; void thingRemoved(Thing *thing) override; void postSetupThing(Thing *thing) override; private: PluginTimer *m_pluginTimer = nullptr; - QString m_baseUrl = "https://api.waqi.info"; - QString m_apiKey; + AirQualityIndex *m_aqiConnection = nullptr; - void getDataByIp(); + QHash m_asyncDiscovery; + QHash m_asyncSetups; + QHash m_asyncRequests; private slots: void onPluginTimer(); + void onRequestExecuted(QUuid requestId, bool success); + void onAirQualityDataReceived(QUuid requestId, AirQualityIndex::AirQualityData data); + void onAirQualityStationsReceived(QUuid requestId, QList stations); }; #endif // INTEGRATIONPLUGINAQI_H diff --git a/aqi/integrationpluginaqi.json b/aqi/integrationpluginaqi.json index 3f85210f..85d2c9e9 100644 --- a/aqi/integrationpluginaqi.json +++ b/aqi/integrationpluginaqi.json @@ -2,15 +2,6 @@ "name": "AirQualityIndex", "displayName": "AirQualityIndex", "id": "57d69b76-4d2d-41ec-bef6-949a79ffbe6b", - "paramTypes": [ - { - "id": "a3525f8a-18be-4739-b0ea-ef3af0c7f280", - "name": "apiKey", - "displayName": "API key", - "type": "QString", - "defaultValue": "74d31bb5ad9bcdeaed48097418b55188cb56d450" - } - ], "vendors": [ { "name": "airQualityIndex", @@ -22,8 +13,23 @@ "name": "airQualityIndex", "displayName": "Air quality index", "interfaces": ["windspeedsensor", "humiditysensor", "pressuresensor", "temperaturesensor", "connectable"], - "createMethods": ["user"], + "createMethods": ["discovery", "user"], + "setupMethod": "displaypin", "paramTypes": [ + { + "id": "afd5803b-6c98-44d7-9f4a-45e91cfb062e", + "name": "latitude", + "displayName": "Latitude", + "type": "QString", + "inputType": "TextLine" + }, + { + "id": "4800d78e-a367-41f7-9bf6-7c81d40ce19a", + "name": "longitude", + "displayName": "Longitude", + "type": "QString", + "inputType": "TextLine" + } ], "stateTypes": [ { diff --git a/debian/nymea-plugin-aqi.install.in b/debian/nymea-plugin-aqi.install.in index 69ba2fda..e2bd73fd 100644 --- a/debian/nymea-plugin-aqi.install.in +++ b/debian/nymea-plugin-aqi.install.in @@ -1 +1 @@ -usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginaqi.so +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginaqi.so