From 2b2cb192761ffa208fd32a8ef88606b3164d14de Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sat, 30 May 2026 12:19:25 +0200 Subject: [PATCH] Add openmeteo integration plugin (site + PV plane) - Weather/solar current conditions from Open-Meteo (Meteo-France model) - Satellite-observed GHI + clear-sky index (MTG) - Per-plane GTI with geographic azimuth convention (180=South) - Self-hosted ready via baseUrl/satelliteBaseUrl plugin settings --- nymea-plugins.pro | 1 + .../CLAUDE_CODE_PROMPT_build_test_deb.md | 66 ++++ openmeteo/README.md | 49 +++ openmeteo/integrationpluginopenmeteo.cpp | 367 ++++++++++++++++++ openmeteo/integrationpluginopenmeteo.h | 65 ++++ openmeteo/integrationpluginopenmeteo.json | 353 +++++++++++++++++ openmeteo/meta.json | 14 + openmeteo/openmeteo.png | Bin 0 -> 4566 bytes openmeteo/openmeteo.pro | 9 + 9 files changed, 924 insertions(+) create mode 100644 openmeteo/CLAUDE_CODE_PROMPT_build_test_deb.md create mode 100644 openmeteo/README.md create mode 100644 openmeteo/integrationpluginopenmeteo.cpp create mode 100644 openmeteo/integrationpluginopenmeteo.h create mode 100644 openmeteo/integrationpluginopenmeteo.json create mode 100644 openmeteo/meta.json create mode 100644 openmeteo/openmeteo.png create mode 100644 openmeteo/openmeteo.pro diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 01b19cd3..b1e82f04 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -47,6 +47,7 @@ PLUGIN_DIRS = \ notifyevents \ nuki \ onewire \ + openmeteo \ openuv \ openweathermap \ osdomotics \ diff --git a/openmeteo/CLAUDE_CODE_PROMPT_build_test_deb.md b/openmeteo/CLAUDE_CODE_PROMPT_build_test_deb.md new file mode 100644 index 00000000..a3681575 --- /dev/null +++ b/openmeteo/CLAUDE_CODE_PROMPT_build_test_deb.md @@ -0,0 +1,66 @@ +# Prompt Claude Code — build, test et packaging du module `openmeteo` + +> À coller dans Claude Code, à la racine du dépôt `etm-powersync-plugins`. +> Le module `openmeteo/` existe déjà (`.json`, `.h`, `.cpp`, `.pro`, `meta.json`, `README.md`, `openmeteo.png`). +> Lis d'abord `CLAUDE.md` (conventions du repo) et le code existant **avant** de modifier quoi que ce soit. + +## Objectif + +Faire compiler, tester manuellement, puis packager en `.deb` le plugin d'intégration nymea `openmeteo`, sans en changer le comportement fonctionnel décrit dans `integrationpluginopenmeteo.cpp`. + +## Étape 1 — Intégration au build + +1. Ajouter `openmeteo` à la liste `SUBDIRS` de `nymea-plugins.pro` (ordre alphabétique, comme les autres modules). +2. Vérifier que `openmeteo/openmeteo.pro` inclut bien `../plugins.pri` et `QT *= network`. + +## Étape 2 — Compilation et correction des constantes générées + +1. Compiler le module (QMake, dans l'arbre, Qt5 d'abord) : + ``` + qmake && make -j$(nproc) + ``` +2. `plugininfo.h` est **généré** à partir de `integrationpluginopenmeteo.json` par l'outil nymea. Ne pas l'éditer. +3. Le `.cpp` référence des constantes (`openMeteoSite*StateTypeId`, `openMeteoPlugin*ParamTypeId`, `openMeteoPvPlane*`, `dcOpenMeteo`, etc.) déduites de la convention nymea. **Comparer ces noms au `plugininfo.h` réellement généré** et corriger toute divergence **dans le `.cpp`** (jamais dans le header généré). Lister les corrections faites. +4. Corriger les éventuels warnings de compilation (variables inutilisées, signedness, etc.). +5. Rejouer le build en **Qt6** et corriger les différences éventuelles. + +## Étape 3 — Vérification du point satellite + +Le `.cpp` contient un `TODO(verify)` dans `updateSiteSatellite()` : l'endpoint satellite (`{satelliteBaseUrl}/v1/archive`, modèle `satellite_radiation_seamless`) doit accepter `current=shortwave_radiation,shortwave_radiation_clear_sky`. + +1. Tester l'URL réellement construite (avec une vraie lat/lon, p.ex. 48.9 / 7.85) avec `curl`. +2. Si `current=` n'est pas supporté : basculer sur `hourly=` et prendre le dernier échantillon **non nul** (garder le calcul `clearSkyIndex = ghiObs/clearSky`, garde `clearSky > 20`). +3. Documenter le comportement retenu en commentaire. + +## Étape 4 — Test manuel (fonctionnel) + +Installer le plugin localement et, via `nymea-cli` ou l'app : + +1. Ajouter un thing **openMeteoSite** (latitude 48.9, longitude 7.85). Vérifier qu'au premier cycle se remplissent : `temperature`, `windSpeed`, `cloudiness`, `humidity`, `pressure`, `weatherCondition`, `ghi`, `dni`, `dhi`, `terrestrialRadiation`, `sunriseTime`, `sunsetTime`, `daylight`, et les daily (`temperatureMin/Max`, `shortwaveRadiationSumToday`). +2. Vérifier les states satellite : `ghiObserved`, `clearSkyGhi`, `clearSkyIndex`. +3. Ajouter un thing **openMeteoPvPlane** enfant du site (tilt 30, azimuth 180 = plein sud). Vérifier `gtiNow` cohérent (proche du GHI à midi pour un plan ~sud). +4. Vérifier la conversion d'azimut : un pan azimuth 90 (Est) doit produire un GTI matinal supérieur à un pan azimuth 270 (Ouest) le matin. +5. Contrôle **de nuit** : `daylight=false`, `ghi≈0`, `clearSkyIndex=0`, pas de division par zéro, pas de crash. +6. Déclencher l'action `refreshWeather` sur le site et vérifier que site + pans se mettent à jour immédiatement. + +## Étape 5 — Packaging `.deb` + +1. Utiliser les dossiers `debian-qt5` / `debian-qt6` existants du dépôt (le lien `debian -> debian-qt5` est en place). +2. Vérifier/compléter le `debian/control` et les fichiers d'install pour inclure le paquet `nymea-plugin-openmeteo` (binaire du plugin + `integrationpluginopenmeteo.json` + ressources). +3. Construire le paquet (p.ex. `dpkg-buildpackage -b -us -uc` ou la cible utilisée par le repo) et vérifier que le `.deb` contient bien la librairie du plugin et son métadonnée. +4. Indiquer le nom exact du `.deb` produit et son emplacement. + +## Contraintes + +- **Ne pas modifier les autres plugins** de l'arbre. +- **Aucune logique propriétaire** (cf. `CLAUDE.md` / frontière de licence) : ce plugin reste une source de données. +- Garder le code et les commentaires en anglais (convention nymea-plugins). +- Montrer chaque diff avant de l'appliquer ; procéder étape par étape. + +## Livrables attendus + +- Module compilé en Qt5 et Qt6, sans warning. +- Liste des corrections de constantes (le cas échéant). +- Comportement satellite confirmé/ajusté. +- Compte rendu du test manuel (states remplis jour/nuit, GTI cohérent par orientation). +- `.deb` `nymea-plugin-openmeteo` produit, nom et emplacement indiqués. diff --git a/openmeteo/README.md b/openmeteo/README.md new file mode 100644 index 00000000..56e30e0a --- /dev/null +++ b/openmeteo/README.md @@ -0,0 +1,49 @@ +# OpenMeteo + +This plugin allows to get current weather and solar irradiance data from [Open-Meteo](https://open-meteo.com), including plane-of-array irradiance (GTI) for photovoltaic surfaces and satellite-observed clear-sky data. + +## Usage + +The data is refreshed every 15 minutes automatically, and can also be refreshed manually. + +A **site** is added by entering its latitude and longitude (no discovery). It provides the shared weather and solar data for the location, including a satellite-observed GHI and clear-sky index. + +One or more **PV planes** can be added as children of a site. Each plane has its own tilt, azimuth and peak power, and reports the global tilted irradiance (GTI) for that orientation. Multiple orientations are handled by adding multiple planes. + +> Note: the azimuth is entered in geographic convention (180 = South, 90 = East, 270 = West). + +By default the plugin queries the public Open-Meteo API. For commercial deployments, the `Base URL` and `Base URL (satellite radiation)` plugin settings can be pointed to a self-hosted Open-Meteo instance (the public API is for non-commercial use only). + +## Supported Things + +* Open-Meteo site (interfaces: weather, daylightsensor) + * Weather condition + * Temperature (current, daily min and max) + * Humidity + * Pressure + * Wind speed and direction + * Cloudiness + * Snowfall + * Global, direct normal and diffuse irradiance (GHI / DNI / DHI) + * Terrestrial (top-of-atmosphere) radiation + * Daily shortwave radiation sum + * Satellite-observed GHI, clear-sky GHI and clear-sky index + * Sunrise and sunset time + * Daylight + +* Open-Meteo PV plane + * Global tilted irradiance (GTI) for the configured tilt and azimuth + +## Requirements + +* Internet connection, or a reachable self-hosted Open-Meteo instance +* The package 'nymea-plugin-openmeteo' must be installed +* No API key is required. For commercial use, a self-hosted instance is required (the public Open-Meteo API is non-commercial only). + +## More + +Open-Meteo https://open-meteo.com + +Weather data by Météo-France, satellite radiation by EUMETSAT, provided through Open-Meteo (CC-BY 4.0). + +For the overall architecture (role of this plugin, conventions, license boundary), see the `etm-powersync-docs` repository. diff --git a/openmeteo/integrationpluginopenmeteo.cpp b/openmeteo/integrationpluginopenmeteo.cpp new file mode 100644 index 00000000..4165b97c --- /dev/null +++ b/openmeteo/integrationpluginopenmeteo.cpp @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2026, ETM-Schurig SARL +* +* This file is part of etm-powersync-plugins. +* +* This program 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. +* +* This program 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 this program. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "integrationpluginopenmeteo.h" +#include "plugininfo.h" + +#include +#include +#include +#include +#include + +#include +#include + +// NOTE: the *StateTypeId / *ParamTypeId / *ThingClassId / *ActionTypeId / dcOpenMeteo +// identifiers below are generated into plugininfo.h from integrationpluginopenmeteo.json. +// If a name does not match, align it with the generated header (do not edit plugininfo.h). + +IntegrationPluginOpenMeteo::IntegrationPluginOpenMeteo() +{ +} + +void IntegrationPluginOpenMeteo::init() +{ + // No API key required. Plugin parameters (baseUrl, satelliteBaseUrl, weatherModel) + // are read at request time via configValue(). +} + +void IntegrationPluginOpenMeteo::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + if (thing->thingClassId() == openMeteoSiteThingClassId) { + double lat, lon; + if (!resolveCoordinates(thing, lat, lon)) { + info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("Missing coordinates for this site.")); + return; + } + updateSiteForecast(thing); + updateSiteSatellite(thing); + } else if (thing->thingClassId() == openMeteoPvPlaneThingClassId) { + double lat, lon; + if (!resolveCoordinates(thing, lat, lon)) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Parent site not available.")); + return; + } + updatePlane(thing); + } + + info->finish(Thing::ThingErrorNoError); + + if (!m_pluginTimer) { + // 900 s = 15 min, aligned with the OpenWeatherMap reference plugin. + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(900); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() { + foreach (Thing *thing, myThings()) { + refresh(thing); + } + }); + } +} + +void IntegrationPluginOpenMeteo::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + + if (thing->thingClassId() == openMeteoSiteThingClassId + && info->action().actionTypeId() == openMeteoSiteRefreshWeatherActionTypeId) { + refresh(thing); + // Also refresh the child PV planes of this site. + foreach (Thing *child, myThings()) { + if (child->thingClassId() == openMeteoPvPlaneThingClassId && child->parentId() == thing->id()) + updatePlane(child); + } + } + + info->finish(Thing::ThingErrorNoError); +} + +void IntegrationPluginOpenMeteo::thingRemoved(Thing *thing) +{ + Q_UNUSED(thing) + if (myThings().isEmpty() && m_pluginTimer) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + m_pluginTimer = nullptr; + } +} + +void IntegrationPluginOpenMeteo::refresh(Thing *thing) +{ + if (thing->thingClassId() == openMeteoSiteThingClassId) { + updateSiteForecast(thing); + updateSiteSatellite(thing); + } else if (thing->thingClassId() == openMeteoPvPlaneThingClassId) { + updatePlane(thing); + } +} + +bool IntegrationPluginOpenMeteo::resolveCoordinates(Thing *thing, double &latitude, double &longitude) const +{ + Thing *site = nullptr; + if (thing->thingClassId() == openMeteoSiteThingClassId) { + site = thing; + } else if (thing->thingClassId() == openMeteoPvPlaneThingClassId) { + site = myThings().findById(thing->parentId()); + } + if (!site) + return false; + + latitude = site->paramValue(openMeteoSiteLatitudeParamTypeId).toDouble(); + longitude = site->paramValue(openMeteoSiteLongitudeParamTypeId).toDouble(); + return true; +} + +void IntegrationPluginOpenMeteo::updateSiteForecast(Thing *thing) +{ + double lat, lon; + if (!resolveCoordinates(thing, lat, lon)) + return; + + const QString baseUrl = configValue(openMeteoPluginBaseUrlParamTypeId).toString(); + const QString model = configValue(openMeteoPluginWeatherModelParamTypeId).toString(); + + QUrl url(baseUrl + "/v1/forecast"); + QUrlQuery query; + query.addQueryItem("latitude", QString::number(lat)); + query.addQueryItem("longitude", QString::number(lon)); + const double elevation = thing->paramValue(openMeteoSiteElevationParamTypeId).toDouble(); + if (elevation != 0.0) + query.addQueryItem("elevation", QString::number(elevation)); + query.addQueryItem("models", model); + query.addQueryItem("wind_speed_unit", "ms"); + query.addQueryItem("timezone", "Europe/Paris"); + query.addQueryItem("timeformat", "unixtime"); + query.addQueryItem("current", "temperature_2m,wind_speed_10m,wind_direction_10m,cloud_cover," + "relative_humidity_2m,surface_pressure,weather_code,snowfall,is_day," + "shortwave_radiation,direct_normal_irradiance,diffuse_radiation," + "terrestrial_radiation"); + query.addQueryItem("daily", "temperature_2m_max,temperature_2m_min,sunrise,sunset,shortwave_radiation_sum"); + url.setQuery(query); + + qCDebug(dcOpenMeteo()) << "Requesting forecast for" << thing->name() << url.toString(); + + QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [this, thing, reply]() { + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcOpenMeteo()) << "Forecast reply error:" << reply->errorString(); + return; + } + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcOpenMeteo()) << "Forecast parse error:" << error.errorString(); + return; + } + + const QVariantMap map = doc.toVariant().toMap(); + const QVariantMap current = map.value("current").toMap(); + const QVariantMap daily = map.value("daily").toMap(); + + if (current.contains("time")) + thing->setStateValue(openMeteoSiteUpdateTimeStateTypeId, current.value("time").toUInt()); + if (current.contains("temperature_2m")) + thing->setStateValue(openMeteoSiteTemperatureStateTypeId, current.value("temperature_2m").toDouble()); + if (current.contains("wind_speed_10m")) + thing->setStateValue(openMeteoSiteWindSpeedStateTypeId, current.value("wind_speed_10m").toDouble()); + if (current.contains("wind_direction_10m")) + thing->setStateValue(openMeteoSiteWindDirectionStateTypeId, current.value("wind_direction_10m").toInt()); + if (current.contains("cloud_cover")) + thing->setStateValue(openMeteoSiteCloudinessStateTypeId, current.value("cloud_cover").toInt()); + if (current.contains("relative_humidity_2m")) + thing->setStateValue(openMeteoSiteHumidityStateTypeId, current.value("relative_humidity_2m").toInt()); + if (current.contains("surface_pressure")) + thing->setStateValue(openMeteoSitePressureStateTypeId, current.value("surface_pressure").toDouble()); + if (current.contains("snowfall")) + thing->setStateValue(openMeteoSiteSnowfallStateTypeId, current.value("snowfall").toDouble()); + if (current.contains("shortwave_radiation")) + thing->setStateValue(openMeteoSiteGhiStateTypeId, current.value("shortwave_radiation").toDouble()); + if (current.contains("direct_normal_irradiance")) + thing->setStateValue(openMeteoSiteDniStateTypeId, current.value("direct_normal_irradiance").toDouble()); + if (current.contains("diffuse_radiation")) + thing->setStateValue(openMeteoSiteDhiStateTypeId, current.value("diffuse_radiation").toDouble()); + if (current.contains("terrestrial_radiation")) + thing->setStateValue(openMeteoSiteTerrestrialRadiationStateTypeId, current.value("terrestrial_radiation").toDouble()); + + bool isDay = current.value("is_day").toInt() == 1; + if (current.contains("is_day")) + thing->setStateValue(openMeteoSiteDaylightStateTypeId, isDay); + if (current.contains("weather_code")) + thing->setStateValue(openMeteoSiteWeatherConditionStateTypeId, + weatherConditionFromCode(current.value("weather_code").toInt(), isDay)); + + // Daily arrays: take index 0 (today). + const QVariantList tmax = daily.value("temperature_2m_max").toList(); + const QVariantList tmin = daily.value("temperature_2m_min").toList(); + const QVariantList sunrise = daily.value("sunrise").toList(); + const QVariantList sunset = daily.value("sunset").toList(); + const QVariantList swSum = daily.value("shortwave_radiation_sum").toList(); + if (!tmax.isEmpty()) + thing->setStateValue(openMeteoSiteTemperatureMaxStateTypeId, tmax.first().toDouble()); + if (!tmin.isEmpty()) + thing->setStateValue(openMeteoSiteTemperatureMinStateTypeId, tmin.first().toDouble()); + if (!sunrise.isEmpty()) + thing->setStateValue(openMeteoSiteSunriseTimeStateTypeId, sunrise.first().toUInt()); + if (!sunset.isEmpty()) + thing->setStateValue(openMeteoSiteSunsetTimeStateTypeId, sunset.first().toUInt()); + if (!swSum.isEmpty()) + // Open-Meteo shortwave_radiation_sum is in MJ/m^2; expose kWh/m^2 (/ 3.6). + thing->setStateValue(openMeteoSiteShortwaveRadiationSumTodayStateTypeId, swSum.first().toDouble() / 3.6); + }); +} + +void IntegrationPluginOpenMeteo::updateSiteSatellite(Thing *thing) +{ + double lat, lon; + if (!resolveCoordinates(thing, lat, lon)) + return; + + const QString satelliteBaseUrl = configValue(openMeteoPluginSatelliteBaseUrlParamTypeId).toString(); + + // Observation product (geostationary satellite). Used for the observed GHI and the + // native clear-sky -> clear-sky index (kt). Observation only, no forward forecast. + // TODO(verify): confirm the satellite endpoint supports "current="; otherwise switch to + // "hourly=" and take the latest non-null sample. + QUrl url(satelliteBaseUrl + "/v1/archive"); + QUrlQuery query; + query.addQueryItem("latitude", QString::number(lat)); + query.addQueryItem("longitude", QString::number(lon)); + query.addQueryItem("models", "satellite_radiation_seamless"); + query.addQueryItem("timezone", "Europe/Paris"); + query.addQueryItem("timeformat", "unixtime"); + query.addQueryItem("current", "shortwave_radiation,shortwave_radiation_clear_sky"); + url.setQuery(query); + + qCDebug(dcOpenMeteo()) << "Requesting satellite observation for" << thing->name() << url.toString(); + + QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [this, thing, reply]() { + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcOpenMeteo()) << "Satellite reply error:" << reply->errorString(); + return; + } + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcOpenMeteo()) << "Satellite parse error:" << error.errorString(); + return; + } + + const QVariantMap current = doc.toVariant().toMap().value("current").toMap(); + double ghiObs = current.value("shortwave_radiation").toDouble(); + double clearSky = current.value("shortwave_radiation_clear_sky").toDouble(); + + if (current.contains("shortwave_radiation")) + thing->setStateValue(openMeteoSiteGhiObservedStateTypeId, ghiObs); + if (current.contains("shortwave_radiation_clear_sky")) + thing->setStateValue(openMeteoSiteClearSkyGhiStateTypeId, clearSky); + + // Clear-sky index kt = GHI_observed / GHI_clearsky, with a night guard. + double kt = 0.0; + if (clearSky > 20.0) + kt = qBound(0.0, ghiObs / clearSky, 1.2); + thing->setStateValue(openMeteoSiteClearSkyIndexStateTypeId, kt); + }); +} + +void IntegrationPluginOpenMeteo::updatePlane(Thing *thing) +{ + double lat, lon; + if (!resolveCoordinates(thing, lat, lon)) + return; + + const QString baseUrl = configValue(openMeteoPluginBaseUrlParamTypeId).toString(); + const QString model = configValue(openMeteoPluginWeatherModelParamTypeId).toString(); + + const double tilt = thing->paramValue(openMeteoPvPlaneTiltParamTypeId).toDouble(); + // Stored azimuth is geographic (180 = South). Open-Meteo expects 0 = South, + // -90 = East, +90 = West. Convert here (see ARCHITECTURE.md, conventions). + const double azimuthOm = thing->paramValue(openMeteoPvPlaneAzimuthParamTypeId).toDouble() - 180.0; + + QUrl url(baseUrl + "/v1/forecast"); + QUrlQuery query; + query.addQueryItem("latitude", QString::number(lat)); + query.addQueryItem("longitude", QString::number(lon)); + query.addQueryItem("models", model); + query.addQueryItem("timezone", "Europe/Paris"); + query.addQueryItem("timeformat", "unixtime"); + query.addQueryItem("tilt", QString::number(tilt)); + query.addQueryItem("azimuth", QString::number(azimuthOm)); + query.addQueryItem("current", "global_tilted_irradiance"); + url.setQuery(query); + + qCDebug(dcOpenMeteo()) << "Requesting GTI for plane" << thing->name() << url.toString(); + + QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, thing, [this, thing, reply]() { + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcOpenMeteo()) << "GTI reply error:" << reply->errorString(); + return; + } + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcOpenMeteo()) << "GTI parse error:" << error.errorString(); + return; + } + + const QVariantMap current = doc.toVariant().toMap().value("current").toMap(); + if (current.contains("global_tilted_irradiance")) + thing->setStateValue(openMeteoPvPlaneGtiNowStateTypeId, current.value("global_tilted_irradiance").toDouble()); + if (current.contains("time")) + thing->setStateValue(openMeteoPvPlaneUpdateTimeStateTypeId, current.value("time").toUInt()); + }); +} + +QString IntegrationPluginOpenMeteo::weatherConditionFromCode(int code, bool isDay) const +{ + // WMO weather interpretation codes -> weatherCondition enum (see metadata possibleValues). + switch (code) { + case 0: + return isDay ? "clear-day" : "clear-night"; + case 1: + case 2: + return isDay ? "few-clouds-day" : "few-clouds-night"; + case 3: + return "overcast"; + case 45: + case 48: + return "fog"; + case 51: case 53: case 55: case 56: case 57: + return "light-rain"; + case 61: case 63: case 65: case 66: case 67: + case 80: case 81: case 82: + return "shower-rain"; + case 71: case 73: case 75: case 77: + case 85: case 86: + return "snow"; + case 95: case 96: case 99: + return "thunderstorm"; + default: + return "clouds"; + } +} diff --git a/openmeteo/integrationpluginopenmeteo.h b/openmeteo/integrationpluginopenmeteo.h new file mode 100644 index 00000000..5d3273e7 --- /dev/null +++ b/openmeteo/integrationpluginopenmeteo.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright (C) 2026, ETM-Schurig SARL +* +* This file is part of etm-powersync-plugins. +* +* This program 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. +* +* This program 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 this program. If not, see . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINOPENMETEO_H +#define INTEGRATIONPLUGINOPENMETEO_H + +#include +#include + +class IntegrationPluginOpenMeteo : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginopenmeteo.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginOpenMeteo(); + + void init() override; + void setupThing(ThingSetupInfo *info) override; + void executeAction(ThingActionInfo *info) override; + void thingRemoved(Thing *thing) override; + +private: + PluginTimer *m_pluginTimer = nullptr; + + // Refresh dispatch + void refresh(Thing *thing); + + // Site (parent): two requests — forecast (Meteo-France) + satellite observation (MTG) + void updateSiteForecast(Thing *thing); + void updateSiteSatellite(Thing *thing); + + // PV plane (child): one GTI request, using the parent site coordinates + void updatePlane(Thing *thing); + + // WMO weather_code -> weatherCondition enum value (with -day/-night via isDay) + QString weatherConditionFromCode(int code, bool isDay) const; + + // Helper: resolve the site coordinates for a thing (site itself, or a plane's parent) + bool resolveCoordinates(Thing *thing, double &latitude, double &longitude) const; +}; + +#endif // INTEGRATIONPLUGINOPENMETEO_H diff --git a/openmeteo/integrationpluginopenmeteo.json b/openmeteo/integrationpluginopenmeteo.json new file mode 100644 index 00000000..7688f925 --- /dev/null +++ b/openmeteo/integrationpluginopenmeteo.json @@ -0,0 +1,353 @@ +{ + "name": "OpenMeteo", + "displayName": "Open-Meteo", + "id": "d1ed979d-b802-463e-a51f-fef79245cfdc", + "paramTypes": [ + { + "id": "590cf57d-c1ca-4645-a443-7c5a6c97db93", + "name": "baseUrl", + "displayName": "Base URL (forecast)", + "type": "QString", + "defaultValue": "https://api.open-meteo.com" + }, + { + "id": "6cd410dc-7f1c-4681-8e47-8df525252d2d", + "name": "satelliteBaseUrl", + "displayName": "Base URL (satellite radiation)", + "type": "QString", + "defaultValue": "https://satellite-api.open-meteo.com" + }, + { + "id": "f904db60-527a-47b2-9dc5-371ac5f7e164", + "name": "weatherModel", + "displayName": "Forecast model", + "type": "QString", + "defaultValue": "meteofrance_seamless" + } + ], + "vendors": [ + { + "name": "openMeteo", + "displayName": "Open-Meteo", + "id": "b1f520ea-dcac-4b16-8cab-89204a225c1b", + "thingClasses": [ + { + "id": "59e23cb4-4820-45d4-9c2e-e7bc7b8c7ca6", + "name": "openMeteoSite", + "displayName": "Open-Meteo site", + "interfaces": [ + "weather", + "daylightsensor" + ], + "createMethods": [ + "user" + ], + "paramTypes": [ + { + "id": "cc7f6cde-c3dd-4924-be5e-a014e20df624", + "name": "name", + "displayName": "Name", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "" + }, + { + "id": "28b4574a-19ea-41d2-9e58-51f0de11165b", + "name": "latitude", + "displayName": "Latitude", + "type": "double", + "defaultValue": 48.9 + }, + { + "id": "8bb8ca04-7a28-4cf5-8a4d-1e1081e75c4b", + "name": "longitude", + "displayName": "Longitude", + "type": "double", + "defaultValue": 7.85 + }, + { + "id": "eb16b64a-588f-4d45-b50a-3ca6bc3decc2", + "name": "elevation", + "displayName": "Elevation (m, optional)", + "type": "double", + "defaultValue": 0 + } + ], + "actionTypes": [ + { + "id": "d31b0484-0149-4dcd-92dc-408392cc5ec3", + "name": "refreshWeather", + "displayName": "Refresh" + } + ], + "stateTypes": [ + { + "id": "3ac82ea4-b824-4c0c-8ca9-216a1fc9c587", + "name": "weatherCondition", + "displayName": "Weather condition", + "displayNameEvent": "Weather condition changed", + "type": "QString", + "defaultValue": "clear-day", + "possibleValues": [ + "clear-day", + "clear-night", + "few-clouds-day", + "few-clouds-night", + "clouds", + "overcast", + "light-rain", + "shower-rain", + "thunderstorm", + "snow", + "fog" + ] + }, + { + "id": "ed48e3ed-f3fd-4401-9b62-0766d7f5d315", + "name": "updateTime", + "displayName": "Last update", + "displayNameEvent": "Last update changed", + "type": "int", + "defaultValue": 0, + "unit": "UnixTime" + }, + { + "id": "b0089fa5-fe4b-4dfc-a1ed-dbd85ca75b20", + "name": "temperature", + "displayName": "Temperature", + "displayNameEvent": "Temperature changed", + "type": "double", + "defaultValue": 0, + "unit": "DegreeCelsius" + }, + { + "id": "348879d1-0433-416f-a9ce-a05d522a2d26", + "name": "temperatureMin", + "displayName": "Minimum temperature (today)", + "displayNameEvent": "Minimum temperature (today) changed", + "type": "double", + "defaultValue": 0, + "unit": "DegreeCelsius" + }, + { + "id": "6530fa30-d1bf-4f84-9a2f-30f1aa65e130", + "name": "temperatureMax", + "displayName": "Maximum temperature (today)", + "displayNameEvent": "Maximum temperature (today) changed", + "type": "double", + "defaultValue": 0, + "unit": "DegreeCelsius" + }, + { + "id": "36413dbc-245b-4067-b5dc-41e71b37c0e9", + "name": "humidity", + "displayName": "Humidity", + "displayNameEvent": "Humidity changed", + "type": "int", + "defaultValue": 0, + "unit": "Percentage" + }, + { + "id": "6bf903e5-6c04-492b-bc43-00db60d2edf4", + "name": "pressure", + "displayName": "Pressure", + "displayNameEvent": "Pressure changed", + "type": "double", + "defaultValue": 0, + "unit": "HectoPascal" + }, + { + "id": "41ec4108-3651-4162-b782-c957e14355b9", + "name": "windSpeed", + "displayName": "Wind speed", + "displayNameEvent": "Wind speed changed", + "type": "double", + "defaultValue": 0, + "unit": "MeterPerSecond" + }, + { + "id": "d64aa5ab-0b1a-477d-95a6-11b755154d88", + "name": "windDirection", + "displayName": "Wind direction", + "displayNameEvent": "Wind direction changed", + "type": "int", + "defaultValue": 0, + "unit": "Degree" + }, + { + "id": "758af353-7e2b-4073-961f-77c5f8bd3b32", + "name": "cloudiness", + "displayName": "Cloudiness", + "displayNameEvent": "Cloudiness changed", + "type": "int", + "defaultValue": 0, + "unit": "Percentage" + }, + { + "id": "175bf804-9881-4272-a83f-fb81a9a029b3", + "name": "snowfall", + "displayName": "Snowfall (preceding hour)", + "displayNameEvent": "Snowfall (preceding hour) changed", + "type": "double", + "defaultValue": 0, + "unit": "CentiMeter" + }, + { + "id": "c8325cc4-de67-4581-a3d7-8248ecbd70e9", + "name": "ghi", + "displayName": "GHI forecast (W/m2)", + "displayNameEvent": "GHI forecast (W/m2) changed", + "type": "double", + "defaultValue": 0 + }, + { + "id": "0b0c5565-e5c5-4927-8a89-1e6b05c19250", + "name": "dni", + "displayName": "DNI forecast (W/m2)", + "displayNameEvent": "DNI forecast (W/m2) changed", + "type": "double", + "defaultValue": 0 + }, + { + "id": "67e64a1f-3dfa-46fa-8438-89df0f7f5c2e", + "name": "dhi", + "displayName": "DHI forecast (W/m2)", + "displayNameEvent": "DHI forecast (W/m2) changed", + "type": "double", + "defaultValue": 0 + }, + { + "id": "c9681fca-1020-4138-8d6e-d6d73d5a002e", + "name": "terrestrialRadiation", + "displayName": "TOA radiation (W/m2)", + "displayNameEvent": "TOA radiation (W/m2) changed", + "type": "double", + "defaultValue": 0 + }, + { + "id": "a25a9740-749e-455a-b4e7-60fff3d402f3", + "name": "shortwaveRadiationSumToday", + "displayName": "GHI sum today (kWh/m2)", + "displayNameEvent": "GHI sum today (kWh/m2) changed", + "type": "double", + "defaultValue": 0 + }, + { + "id": "de0f16e8-8e88-4aa7-b00f-c744e1e13072", + "name": "ghiObserved", + "displayName": "GHI observed sat (W/m2)", + "displayNameEvent": "GHI observed sat (W/m2) changed", + "type": "double", + "defaultValue": 0 + }, + { + "id": "a8d0074b-5696-464b-a9c6-7da16f3cd021", + "name": "clearSkyGhi", + "displayName": "Clear-sky GHI sat (W/m2)", + "displayNameEvent": "Clear-sky GHI sat (W/m2) changed", + "type": "double", + "defaultValue": 0 + }, + { + "id": "c6b6ccb6-a5cb-458a-bb7d-9dd6f818a962", + "name": "clearSkyIndex", + "displayName": "Clear-sky index kt", + "displayNameEvent": "Clear-sky index kt changed", + "type": "double", + "defaultValue": 0 + }, + { + "id": "2c6918d0-9758-4c71-b75b-1f08c3701ddf", + "name": "sunriseTime", + "displayName": "Sunrise time", + "displayNameEvent": "Sunrise time changed", + "type": "int", + "defaultValue": 0, + "unit": "UnixTime" + }, + { + "id": "c005cf92-1e29-4033-a65b-578d5d541240", + "name": "sunsetTime", + "displayName": "Sunset time", + "displayNameEvent": "Sunset time changed", + "type": "int", + "defaultValue": 0, + "unit": "UnixTime" + }, + { + "id": "291eea88-0a6d-4ca2-be1c-8a291cfbbdef", + "name": "daylight", + "displayName": "Daylight", + "displayNameEvent": "Daylight changed", + "type": "bool", + "defaultValue": false + } + ] + }, + { + "id": "fdfb5cb3-e5da-49d1-8d4e-1a316a68061a", + "name": "openMeteoPvPlane", + "displayName": "Open-Meteo PV plane", + "interfaces": [], + "createMethods": [ + "user" + ], + "paramTypes": [ + { + "id": "f5c6d83f-32a2-4cf8-85ae-137eb9c60635", + "name": "name", + "displayName": "Name", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "" + }, + { + "id": "e2914db5-d5d6-4c7b-9da6-5fba8a49e023", + "name": "tilt", + "displayName": "Tilt (deg, 0=horizontal)", + "type": "double", + "minValue": 0, + "maxValue": 90, + "defaultValue": 30 + }, + { + "id": "ecba346a-e5f5-4264-92ab-a92a8fb2cf38", + "name": "azimuth", + "displayName": "Azimuth (deg geographic, 180=South)", + "type": "double", + "minValue": 0, + "maxValue": 360, + "defaultValue": 180 + }, + { + "id": "a8291abd-8106-49a1-868e-8907e16bba8a", + "name": "kwp", + "displayName": "Peak power kWp", + "type": "double", + "defaultValue": 0 + } + ], + "stateTypes": [ + { + "id": "e0efeb24-d63c-48d7-93d7-34174c9f4e15", + "name": "gtiNow", + "displayName": "GTI current (W/m2)", + "displayNameEvent": "GTI current (W/m2) changed", + "type": "double", + "defaultValue": 0 + }, + { + "id": "95ed4db9-a231-41bb-b313-78735b63fe9c", + "name": "updateTime", + "displayName": "Last update", + "displayNameEvent": "Last update changed", + "type": "int", + "defaultValue": 0, + "unit": "UnixTime" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/openmeteo/meta.json b/openmeteo/meta.json new file mode 100644 index 00000000..846c8516 --- /dev/null +++ b/openmeteo/meta.json @@ -0,0 +1,14 @@ +{ + "title": "OpenMeteo", + "tagline": "Get current weather data for your location.", + "icon": "openmeteo.png", + "stability": "consumer", + "offline": false, + "technologies": [ + "cloud" + ], + "categories": [ + "weather", + "online-service" + ] +} diff --git a/openmeteo/openmeteo.png b/openmeteo/openmeteo.png new file mode 100644 index 0000000000000000000000000000000000000000..029a30bf974aad20ba2e128e465b1cdd89e86a16 GIT binary patch literal 4566 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 literal 0 HcmV?d00001 diff --git a/openmeteo/openmeteo.pro b/openmeteo/openmeteo.pro new file mode 100644 index 00000000..1ff8fc73 --- /dev/null +++ b/openmeteo/openmeteo.pro @@ -0,0 +1,9 @@ +include(../plugins.pri) + +QT *= network + +SOURCES += \ + integrationpluginopenmeteo.cpp + +HEADERS += \ + integrationpluginopenmeteo.h