// SPDX-License-Identifier: GPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-plugins. * * nymea-plugins is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nymea-plugins 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 * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nymea-plugins. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "integrationpluginopenweathermap.h" #include "plugininfo.h" #include #include #include #include #include #include #include #include #include #include #include IntegrationPluginOpenweathermap::IntegrationPluginOpenweathermap() { } IntegrationPluginOpenweathermap::~IntegrationPluginOpenweathermap() { } void IntegrationPluginOpenweathermap::init() { updateApiKey(); connect(this, &IntegrationPlugin::configValueChanged, this, &IntegrationPluginOpenweathermap::updateApiKey); connect(apiKeyStorage(), &ApiKeyStorage::keyAdded, this, &IntegrationPluginOpenweathermap::updateApiKey); connect(apiKeyStorage(), &ApiKeyStorage::keyUpdated, this, &IntegrationPluginOpenweathermap::updateApiKey); } void IntegrationPluginOpenweathermap::discoverThings(ThingDiscoveryInfo *info) { QString location = info->params().paramValue(openweathermapDiscoveryLocationParamTypeId).toString(); // if we have an empty search string, perform an autodetection of the location with the WAN ip... if (location.isEmpty()){ searchAutodetect(info); } else { search(location, info); } } void IntegrationPluginOpenweathermap::setupThing(ThingSetupInfo *info) { update(info->thing()); if (m_apiKey.isEmpty()) { info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("No API key for OpenWeatherMap available.")); return; } info->finish(Thing::ThingErrorNoError); if (!m_pluginTimer) { m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(900); connect(m_pluginTimer, &PluginTimer::timeout, this, [this](){ foreach (Thing *thing, myThings()) { update(thing); } }); } } void IntegrationPluginOpenweathermap::executeAction(ThingActionInfo *info) { update(info->thing()); info->finish(Thing::ThingErrorNoError); } void IntegrationPluginOpenweathermap::thingRemoved(Thing *thing) { Q_UNUSED(thing) if (myThings().isEmpty()) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); m_pluginTimer = nullptr; } } void IntegrationPluginOpenweathermap::updateApiKey() { if (!(m_apiKey = configValue(openWeatherMapPluginCustomApiKeyParamTypeId).toString()).isEmpty()) { qCDebug(dcOpenWeatherMap()) << "Using API key from plugin settings."; } else if (!(m_apiKey = apiKeyStorage()->requestKey("openweathermap").data("appid")).isEmpty()) { qCDebug(dcOpenWeatherMap()) << "Using API key from nymea API keys provider"; } else { qCWarning(dcOpenWeatherMap()) << "No API key set. This plugin might not work correctly."; qCWarning(dcOpenWeatherMap()) << "Either install an API key pacakge (nymea-apikeysprovider-plugin-*) or provide a key in the plugin settings."; } } void IntegrationPluginOpenweathermap::update(Thing *thing) { qCDebug(dcOpenWeatherMap()) << "Refreshing data for" << thing->name(); QUrl url("http://api.openweathermap.org/data/2.5/weather"); QUrlQuery query; query.addQueryItem("id", thing->paramValue(openweathermapThingIdParamTypeId).toString()); query.addQueryItem("mode", "json"); query.addQueryItem("units", "metric"); query.addQueryItem("appid", m_apiKey); url.setQuery(query); QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, thing, [thing, reply](){ if (reply->error() != QNetworkReply::NoError) { qCWarning(dcOpenWeatherMap) << "OpenWeatherMap reply error: " << reply->errorString(); return; } QByteArray data = reply->readAll(); QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { qCWarning(dcOpenWeatherMap()) << "failed to parse weather data for thing " << thing->name() << "\n" << data << "\n" << error.errorString(); return; } qCDebug(dcOpenWeatherMap()) << "Received" << qUtf8Printable(jsonDoc.toJson()); // http://openweathermap.org/current QVariantMap dataMap = jsonDoc.toVariant().toMap(); if (dataMap.contains("clouds")) { int cloudiness = dataMap.value("clouds").toMap().value("all").toInt(); thing->setStateValue(openweathermapCloudinessStateTypeId, cloudiness); } if (dataMap.contains("dt")) { uint lastUpdate = dataMap.value("dt").toUInt(); thing->setStateValue(openweathermapUpdateTimeStateTypeId, lastUpdate); } if (dataMap.contains("main")) { double temperatur = dataMap.value("main").toMap().value("temp").toDouble(); double temperaturMax = dataMap.value("main").toMap().value("temp_max").toDouble(); double temperaturMin = dataMap.value("main").toMap().value("temp_min").toDouble(); double pressure = dataMap.value("main").toMap().value("pressure").toDouble(); int humidity = dataMap.value("main").toMap().value("humidity").toInt(); thing->setStateValue(openweathermapTemperatureStateTypeId, temperatur); thing->setStateValue(openweathermapTemperatureMinStateTypeId, temperaturMin); thing->setStateValue(openweathermapTemperatureMaxStateTypeId, temperaturMax); thing->setStateValue(openweathermapPressureStateTypeId, pressure); thing->setStateValue(openweathermapHumidityStateTypeId, humidity); } if (dataMap.contains("sys")) { qint64 sunrise = dataMap.value("sys").toMap().value("sunrise").toLongLong(); qint64 sunset = dataMap.value("sys").toMap().value("sunset").toLongLong(); thing->setStateValue(openweathermapSunriseTimeStateTypeId, sunrise); thing->setStateValue(openweathermapSunsetTimeStateTypeId, sunset); QTimeZone tz = QTimeZone(QTimeZone::systemTimeZoneId()); QDateTime up = QDateTime::fromMSecsSinceEpoch(sunrise * 1000); QDateTime down = QDateTime::fromMSecsSinceEpoch(sunset * 1000); QDateTime now = QDateTime::currentDateTime().toTimeZone(tz); thing->setStateValue(openweathermapDaylightStateTypeId, up < now && down > now); } if (dataMap.contains("visibility")) { int visibility = dataMap.value("visibility").toInt(); thing->setStateValue(openweathermapVisibilityStateTypeId, visibility); } // http://openweathermap.org/weather-conditions if (dataMap.contains("weather") && dataMap.value("weather").toList().count() > 0) { int conditionId = dataMap.value("weather").toList().first().toMap().value("id").toInt(); if (conditionId == 800) { if (thing->stateValue(openweathermapUpdateTimeStateTypeId).toInt() > thing->stateValue(openweathermapSunriseTimeStateTypeId).toInt() && thing->stateValue(openweathermapUpdateTimeStateTypeId).toInt() < thing->stateValue(openweathermapSunsetTimeStateTypeId).toInt()) { thing->setStateValue(openweathermapWeatherConditionStateTypeId, "clear-day"); } else { thing->setStateValue(openweathermapWeatherConditionStateTypeId, "clear-night"); } } else if (conditionId == 801) { if (thing->stateValue(openweathermapUpdateTimeStateTypeId).toInt() > thing->stateValue(openweathermapSunriseTimeStateTypeId).toInt() && thing->stateValue(openweathermapUpdateTimeStateTypeId).toInt() < thing->stateValue(openweathermapSunsetTimeStateTypeId).toInt()) { thing->setStateValue(openweathermapWeatherConditionStateTypeId, "few-clouds-day"); } else { thing->setStateValue(openweathermapWeatherConditionStateTypeId, "few-clouds-night"); } } else if (conditionId == 802) { thing->setStateValue(openweathermapWeatherConditionStateTypeId, "clouds"); } else if (conditionId >= 803 && conditionId < 900) { thing->setStateValue(openweathermapWeatherConditionStateTypeId, "overcast"); } else if (conditionId >= 300 && conditionId < 400) { thing->setStateValue(openweathermapWeatherConditionStateTypeId, "light-rain"); } else if (conditionId >= 500 && conditionId < 600) { thing->setStateValue(openweathermapWeatherConditionStateTypeId, "shower-rain"); } else if (conditionId >= 200 && conditionId < 300) { thing->setStateValue(openweathermapWeatherConditionStateTypeId, "thunderstorm"); } else if (conditionId >= 600 && conditionId < 700) { thing->setStateValue(openweathermapWeatherConditionStateTypeId, "snow"); } else if (conditionId >= 700 && conditionId < 800) { thing->setStateValue(openweathermapWeatherConditionStateTypeId, "fog"); } QString description = dataMap.value("weather").toList().first().toMap().value("description").toString(); thing->setStateValue(openweathermapWeatherDescriptionStateTypeId, description); } if (dataMap.contains("wind")) { int windDirection = dataMap.value("wind").toMap().value("deg").toInt(); double windSpeed = dataMap.value("wind").toMap().value("speed").toDouble(); thing->setStateValue(openweathermapWindDirectionStateTypeId, windDirection); thing->setStateValue(openweathermapWindSpeedStateTypeId, windSpeed); } }); } void IntegrationPluginOpenweathermap::searchAutodetect(ThingDiscoveryInfo *info) { QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(QUrl("http://ip-api.com/json"))); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, info, [this, info, reply](){ if (reply->error()) { qCWarning(dcOpenWeatherMap) << "OpenWeatherMap reply error: " << reply->errorString(); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error detecting current location.")); return; } QByteArray data = reply->readAll(); QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if(error.error != QJsonParseError::NoError) { qCWarning(dcOpenWeatherMap) << "failed to parse data" << data << ":" << error.errorString(); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Received unexpected data detecting current location.")); return; } // search by geographic coordinates QVariantMap dataMap = jsonDoc.toVariant().toMap(); QString country = dataMap.value("countryCode").toString(); QString cityName = dataMap.value("city").toString(); QHostAddress wanIp = QHostAddress(dataMap.value("query").toString()); double longitude = dataMap.value("lon").toDouble(); double latitude = dataMap.value("lat").toDouble(); qCDebug(dcOpenWeatherMap) << "----------------------------------------"; qCDebug(dcOpenWeatherMap) << "Autodetection of location: "; qCDebug(dcOpenWeatherMap) << "----------------------------------------"; qCDebug(dcOpenWeatherMap) << " name:" << cityName; qCDebug(dcOpenWeatherMap) << " country:" << country; qCDebug(dcOpenWeatherMap) << " WAN IP:" << wanIp.toString(); qCDebug(dcOpenWeatherMap) << " latitude:" << latitude; qCDebug(dcOpenWeatherMap) << " longitude:" << longitude; qCDebug(dcOpenWeatherMap) << "----------------------------------------"; searchGeoLocation(latitude, longitude, country, info); }); } void IntegrationPluginOpenweathermap::search(QString searchString, ThingDiscoveryInfo *info) { QUrl url("http://api.openweathermap.org/data/2.5/find"); QUrlQuery query; query.addQueryItem("q", searchString); query.addQueryItem("type", "like"); query.addQueryItem("mode", "json"); query.addQueryItem("units", "metric"); query.addQueryItem("appid", m_apiKey); url.setQuery(query); QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, info, [this, info, reply](){ if (reply->error()) { qCWarning(dcOpenWeatherMap) << "OpenWeatherMap reply error: " << reply->errorString(); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error searching for weather stations.")); return; } QByteArray data = reply->readAll(); QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if(error.error != QJsonParseError::NoError) { qCWarning(dcOpenWeatherMap) << "failed to parse data" << data << ":" << error.errorString(); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Received unexpected data while searching for weather stations.")); return; } QVariantMap dataMap = jsonDoc.toVariant().toMap(); QList cityList; if (dataMap.contains("list")) { QVariantList list = dataMap.value("list").toList(); foreach (QVariant key, list) { QVariantMap elemant = key.toMap(); QVariantMap city; city.insert("name",elemant.value("name").toString()); city.insert("country", elemant.value("sys").toMap().value("country").toString()); city.insert("id",elemant.value("id").toString()); cityList.append(city); } } processSearchResults(cityList, info); }); } void IntegrationPluginOpenweathermap::searchGeoLocation(double lat, double lon, const QString &country, ThingDiscoveryInfo *info) { QUrl url("http://api.openweathermap.org/data/2.5/find"); QUrlQuery query; query.addQueryItem("lat", QString::number(lat)); query.addQueryItem("lon", QString::number(lon)); query.addQueryItem("cnt", QString::number(3)); // 3 km radius query.addQueryItem("type", "like"); query.addQueryItem("mode", "json"); query.addQueryItem("units", "metric"); query.addQueryItem("appid", m_apiKey); url.setQuery(query); QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, info, [this, info, reply, country](){ if (reply->error()) { qCWarning(dcOpenWeatherMap) << "OpenWeatherMap reply error: " << reply->errorString(); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error searching for weather stations in current location.")); return; } QByteArray data = reply->readAll(); QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if(error.error != QJsonParseError::NoError) { qCWarning(dcOpenWeatherMap) << "failed to parse data" << data << ":" << error.errorString(); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Received unexpected data while searching for weather stations.")); return; } QVariantMap dataMap = jsonDoc.toVariant().toMap(); QList cityList; if (dataMap.contains("list")) { QVariantList list = dataMap.value("list").toList(); foreach (QVariant key, list) { QVariantMap elemant = key.toMap(); QVariantMap city; city.insert("name",elemant.value("name").toString()); if(elemant.value("sys").toMap().value("country").toString().isEmpty()){ city.insert("country",country); }else{ city.insert("country", elemant.value("sys").toMap().value("country").toString()); } city.insert("id",elemant.value("id").toString()); cityList.append(city); } } processSearchResults(cityList, info); }); } void IntegrationPluginOpenweathermap::processSearchResults(const QList &cityList, ThingDiscoveryInfo *info) { foreach (QVariantMap element, cityList) { ThingDescriptor descriptor(openweathermapThingClassId, element.value("name").toString(), element.value("country").toString()); ParamList params; Param nameParam(openweathermapThingNameParamTypeId, element.value("name")); params.append(nameParam); Param countryParam(openweathermapThingCountryParamTypeId, element.value("country")); params.append(countryParam); Param idParam(openweathermapThingIdParamTypeId, element.value("id")); params.append(idParam); descriptor.setParams(params); foreach (Thing *existingThing, myThings()) { if (existingThing->paramValue(openweathermapThingIdParamTypeId).toString() == element.value("id")) { descriptor.setThingId(existingThing->id()); break; } } info->addThingDescriptor(descriptor); } info->finish(Thing::ThingErrorNoError); }