File indexing completed on 2025-01-12 05:01:49
0001 /* 0002 SPDX-FileCopyrightText: 2021 Emily Ehlert 0003 0004 Based upon BBC Weather Ion and ENV Canada Ion by Shawn Starr 0005 SPDX-FileCopyrightText: 2007-2009 Shawn Starr <shawn.starr@rogers.com> 0006 0007 also 0008 0009 the wetter.com Ion by Thilo-Alexander Ginkel 0010 SPDX-FileCopyrightText: 2009 Thilo-Alexander Ginkel <thilo@ginkel.com> 0011 0012 SPDX-License-Identifier: GPL-2.0-or-later 0013 */ 0014 0015 /* Ion for weather data from Deutscher Wetterdienst (DWD) / German Weather Service */ 0016 0017 #include "ion_dwd.h" 0018 0019 #include "ion_dwddebug.h" 0020 0021 #include <KIO/TransferJob> 0022 #include <KLocalizedString> 0023 #include <KUnitConversion/Converter> 0024 0025 #include <QDateTime> 0026 #include <QJsonArray> 0027 #include <QJsonDocument> 0028 #include <QJsonObject> 0029 #include <QLocale> 0030 #include <QVariant> 0031 0032 /* 0033 * Initialization 0034 */ 0035 0036 WeatherData::WeatherData() 0037 : temperature(qQNaN()) 0038 , humidity(qQNaN()) 0039 , pressure(qQNaN()) 0040 , windSpeed(qQNaN()) 0041 , gustSpeed(qQNaN()) 0042 , dewpoint(qQNaN()) 0043 , windSpeedAlt(qQNaN()) 0044 , gustSpeedAlt(qQNaN()) 0045 { 0046 } 0047 0048 WeatherData::ForecastInfo::ForecastInfo() 0049 : tempHigh(qQNaN()) 0050 , tempLow(qQNaN()) 0051 , windSpeed(qQNaN()) 0052 { 0053 } 0054 0055 DWDIon::DWDIon(QObject *parent) 0056 : IonInterface(parent) 0057 0058 { 0059 setInitialized(true); 0060 } 0061 0062 DWDIon::~DWDIon() 0063 { 0064 deleteForecasts(); 0065 } 0066 0067 void DWDIon::reset() 0068 { 0069 deleteForecasts(); 0070 m_sourcesToReset = sources(); 0071 updateAllSources(); 0072 } 0073 0074 void DWDIon::deleteForecasts() 0075 { 0076 // Destroy each forecast stored in a QList 0077 for (auto it = m_weatherData.begin(), end = m_weatherData.end(); it != end; ++it) { 0078 qDeleteAll(it.value().forecasts); 0079 it.value().forecasts.clear(); 0080 } 0081 } 0082 0083 QMap<QString, IonInterface::ConditionIcons> DWDIon::setupDayIconMappings() const 0084 { 0085 // DWD supplies it's own icon number which we can use to determine a condition 0086 0087 return QMap<QString, ConditionIcons>{{QStringLiteral("1"), ClearDay}, 0088 {QStringLiteral("2"), PartlyCloudyDay}, 0089 {QStringLiteral("3"), PartlyCloudyDay}, 0090 {QStringLiteral("4"), Overcast}, 0091 {QStringLiteral("5"), Mist}, 0092 {QStringLiteral("6"), Mist}, 0093 {QStringLiteral("7"), LightRain}, 0094 {QStringLiteral("8"), Rain}, 0095 {QStringLiteral("9"), Rain}, 0096 {QStringLiteral("10"), LightRain}, 0097 {QStringLiteral("11"), Rain}, 0098 {QStringLiteral("12"), Flurries}, 0099 {QStringLiteral("13"), RainSnow}, 0100 {QStringLiteral("14"), LightSnow}, 0101 {QStringLiteral("15"), Snow}, 0102 {QStringLiteral("16"), Snow}, 0103 {QStringLiteral("17"), Hail}, 0104 {QStringLiteral("18"), LightRain}, 0105 {QStringLiteral("19"), Rain}, 0106 {QStringLiteral("20"), Flurries}, 0107 {QStringLiteral("21"), RainSnow}, 0108 {QStringLiteral("22"), LightSnow}, 0109 {QStringLiteral("23"), Snow}, 0110 {QStringLiteral("24"), Hail}, 0111 {QStringLiteral("25"), Hail}, 0112 {QStringLiteral("26"), Thunderstorm}, 0113 {QStringLiteral("27"), Thunderstorm}, 0114 {QStringLiteral("28"), Thunderstorm}, 0115 {QStringLiteral("29"), Thunderstorm}, 0116 {QStringLiteral("30"), Thunderstorm}, 0117 {QStringLiteral("31"), ClearWindyDay}}; 0118 } 0119 0120 QMap<QString, IonInterface::WindDirections> DWDIon::setupWindIconMappings() const 0121 { 0122 return QMap<QString, WindDirections>{ 0123 {QStringLiteral("0"), N}, {QStringLiteral("10"), N}, {QStringLiteral("20"), NNE}, {QStringLiteral("30"), NNE}, {QStringLiteral("40"), NE}, 0124 {QStringLiteral("50"), NE}, {QStringLiteral("60"), ENE}, {QStringLiteral("70"), ENE}, {QStringLiteral("80"), E}, {QStringLiteral("90"), E}, 0125 {QStringLiteral("100"), E}, {QStringLiteral("120"), ESE}, {QStringLiteral("130"), ESE}, {QStringLiteral("140"), SE}, {QStringLiteral("150"), SE}, 0126 {QStringLiteral("160"), SSE}, {QStringLiteral("170"), SSE}, {QStringLiteral("180"), S}, {QStringLiteral("190"), S}, {QStringLiteral("200"), SSW}, 0127 {QStringLiteral("210"), SSW}, {QStringLiteral("220"), SW}, {QStringLiteral("230"), SW}, {QStringLiteral("240"), WSW}, {QStringLiteral("250"), WSW}, 0128 {QStringLiteral("260"), W}, {QStringLiteral("270"), W}, {QStringLiteral("280"), W}, {QStringLiteral("290"), WNW}, {QStringLiteral("300"), WNW}, 0129 {QStringLiteral("310"), NW}, {QStringLiteral("320"), NW}, {QStringLiteral("330"), NNW}, {QStringLiteral("340"), NNW}, {QStringLiteral("350"), N}, 0130 {QStringLiteral("360"), N}, 0131 }; 0132 } 0133 0134 QMap<QString, IonInterface::ConditionIcons> const &DWDIon::dayIcons() const 0135 { 0136 static QMap<QString, ConditionIcons> const dval = setupDayIconMappings(); 0137 return dval; 0138 } 0139 0140 QMap<QString, IonInterface::WindDirections> const &DWDIon::windIcons() const 0141 { 0142 static QMap<QString, WindDirections> const wval = setupWindIconMappings(); 0143 return wval; 0144 } 0145 0146 bool DWDIon::updateIonSource(const QString &source) 0147 { 0148 // We expect the applet to send the source in the following tokenization: 0149 // ionname|validate|place_name|extra - Triggers validation (search) of place 0150 // ionname|weather|place_name|extra - Triggers receiving weather of place 0151 const QStringList sourceAction = source.split(QLatin1Char('|')); 0152 0153 if (sourceAction.size() < 3) { 0154 setData(source, QStringLiteral("validate"), QStringLiteral("dwd|malformed")); 0155 return true; 0156 } 0157 0158 if (sourceAction[1] == QLatin1String("validate") && sourceAction.size() >= 3) { 0159 // Look for places to match 0160 findPlace(sourceAction[2]); 0161 return true; 0162 } 0163 if (sourceAction[1] == QLatin1String("weather") && sourceAction.size() >= 3) { 0164 if (sourceAction.count() >= 4) { 0165 if (sourceAction[2].isEmpty()) { 0166 setData(source, QStringLiteral("validate"), QStringLiteral("dwd|malformed")); 0167 return true; 0168 } 0169 0170 // Extra data: station_id 0171 m_place[sourceAction[2]] = sourceAction[3]; 0172 0173 qCDebug(IONENGINE_dwd) << "About to retrieve forecast for source: " << sourceAction[2]; 0174 0175 fetchWeather(sourceAction[2], m_place[sourceAction[2]]); 0176 0177 return true; 0178 } 0179 0180 return false; 0181 } 0182 0183 setData(source, QStringLiteral("validate"), QStringLiteral("dwd|malformed")); 0184 return true; 0185 } 0186 0187 void DWDIon::findPlace(const QString &searchText) 0188 { 0189 // Checks if the stations have already been loaded, always contains the currently active one 0190 if (m_place.size() > 1) { 0191 setData(QStringLiteral("dwd|validate|") + searchText, Data()); 0192 searchInStationList(searchText); 0193 } else { 0194 const QUrl forecastURL(QStringLiteral(CATALOGUE_URL)); 0195 KIO::TransferJob *getJob = KIO::get(forecastURL, KIO::Reload, KIO::HideProgressInfo); 0196 getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); 0197 0198 m_searchJobList.insert(getJob, searchText); 0199 m_searchJobData.insert(getJob, QByteArray("")); 0200 0201 connect(getJob, &KIO::TransferJob::data, this, &DWDIon::setup_slotDataArrived); 0202 connect(getJob, &KJob::result, this, &DWDIon::setup_slotJobFinished); 0203 } 0204 } 0205 0206 void DWDIon::fetchWeather(QString placeName, QString placeID) 0207 { 0208 for (const QString &fetching : std::as_const(m_forecastJobList)) { 0209 if (fetching == placeName) { 0210 // already fetching! 0211 return; 0212 } 0213 } 0214 0215 // Fetch forecast data 0216 0217 const QUrl forecastURL(QStringLiteral(FORECAST_URL).arg(placeID)); 0218 KIO::TransferJob *getJob = KIO::get(forecastURL, KIO::Reload, KIO::HideProgressInfo); 0219 getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); 0220 0221 m_forecastJobList.insert(getJob, placeName); 0222 m_forecastJobJSON.insert(getJob, QByteArray("")); 0223 0224 qCDebug(IONENGINE_dwd) << "Requesting URL: " << forecastURL; 0225 0226 connect(getJob, &KIO::TransferJob::data, this, &DWDIon::forecast_slotDataArrived); 0227 connect(getJob, &KJob::result, this, &DWDIon::forecast_slotJobFinished); 0228 m_weatherData[placeName].isForecastsDataPending = true; 0229 0230 // Fetch current measurements (different url AND different API, AMAZING) 0231 0232 const QUrl measureURL(QStringLiteral(MEASURE_URL).arg(placeID)); 0233 KIO::TransferJob *getMeasureJob = KIO::get(measureURL, KIO::Reload, KIO::HideProgressInfo); 0234 getMeasureJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); 0235 0236 m_measureJobList.insert(getMeasureJob, placeName); 0237 m_measureJobJSON.insert(getMeasureJob, QByteArray("")); 0238 0239 qCDebug(IONENGINE_dwd) << "Requesting URL: " << measureURL; 0240 0241 connect(getMeasureJob, &KIO::TransferJob::data, this, &DWDIon::measure_slotDataArrived); 0242 connect(getMeasureJob, &KJob::result, this, &DWDIon::measure_slotJobFinished); 0243 m_weatherData[placeName].isMeasureDataPending = true; 0244 } 0245 0246 void DWDIon::setup_slotDataArrived(KIO::Job *job, const QByteArray &data) 0247 { 0248 QByteArray local = data; 0249 0250 if (data.isEmpty() || !m_searchJobData.contains(job)) { 0251 return; 0252 } 0253 0254 m_searchJobData[job].append(local); 0255 } 0256 0257 void DWDIon::measure_slotDataArrived(KIO::Job *job, const QByteArray &data) 0258 { 0259 QByteArray local = data; 0260 0261 if (data.isEmpty() || !m_measureJobJSON.contains(job)) { 0262 return; 0263 } 0264 0265 m_measureJobJSON[job].append(local); 0266 } 0267 0268 void DWDIon::forecast_slotDataArrived(KIO::Job *job, const QByteArray &data) 0269 { 0270 QByteArray local = data; 0271 0272 if (data.isEmpty() || !m_forecastJobJSON.contains(job)) { 0273 return; 0274 } 0275 0276 m_forecastJobJSON[job].append(local); 0277 } 0278 0279 void DWDIon::setup_slotJobFinished(KJob *job) 0280 { 0281 if (!job->error()) { 0282 const QString searchText(m_searchJobList.value(job)); 0283 setData(QStringLiteral("dwd|validate|") + searchText, Data()); 0284 0285 QByteArray catalogueData = m_searchJobData[job]; 0286 if (!catalogueData.isEmpty()) { 0287 parseStationData(catalogueData); 0288 searchInStationList(searchText); 0289 } 0290 } else { 0291 qCWarning(IONENGINE_dwd) << "error during setup" << job->errorText(); 0292 } 0293 0294 m_searchJobList.remove(job); 0295 m_searchJobData.remove(job); 0296 } 0297 0298 void DWDIon::measure_slotJobFinished(KJob *job) 0299 { 0300 const QString source(m_measureJobList.value(job)); 0301 const QByteArray &jsonData = m_measureJobJSON.value(job); 0302 0303 if (!job->error() && !jsonData.isEmpty()) { 0304 setData(source, Data()); 0305 QJsonDocument doc = QJsonDocument::fromJson(jsonData); 0306 parseMeasureData(source, doc); 0307 } else { 0308 qCWarning(IONENGINE_dwd) << "no measurements received" << job->errorText(); 0309 m_weatherData[source].isMeasureDataPending = false; 0310 updateWeather(source); 0311 } 0312 0313 m_measureJobList.remove(job); 0314 m_measureJobJSON.remove(job); 0315 } 0316 0317 void DWDIon::forecast_slotJobFinished(KJob *job) 0318 { 0319 if (!job->error()) { 0320 const QString source(m_forecastJobList.value(job)); 0321 setData(source, Data()); 0322 0323 QJsonDocument doc = QJsonDocument::fromJson(m_forecastJobJSON.value(job)); 0324 0325 if (!doc.isEmpty()) { 0326 parseForecastData(source, doc); 0327 } 0328 0329 if (m_sourcesToReset.contains(source)) { 0330 m_sourcesToReset.removeAll(source); 0331 const QString weatherSource = QStringLiteral("dwd|weather|%1|%2").arg(source, m_place[source]); 0332 0333 // so the weather engine updates it's data 0334 forceImmediateUpdateOfAllVisualizations(); 0335 0336 // update the clients of our engine 0337 Q_EMIT forceUpdate(this, weatherSource); 0338 } 0339 } else { 0340 qCWarning(IONENGINE_dwd) << "error during forecast" << job->errorText(); 0341 } 0342 0343 m_forecastJobList.remove(job); 0344 m_forecastJobJSON.remove(job); 0345 } 0346 0347 void DWDIon::calculatePositions(QStringList lines, QList<int> &namePositionalInfo, QList<int> &stationIdPositionalInfo) 0348 { 0349 QStringList stringLengths = lines[1].split(QChar::Space); 0350 QList<int> lengths; 0351 for (const QString &length : std::as_const(stringLengths)) { 0352 lengths.append(length.count()); 0353 } 0354 0355 int curpos = 0; 0356 0357 for (int labelLength : lengths) { 0358 QString label = lines[0].mid(curpos, labelLength).toLower(); 0359 0360 if (label.contains(QStringLiteral("name"))) { 0361 namePositionalInfo[0] = curpos; 0362 namePositionalInfo[1] = labelLength; 0363 } else if (label.contains(QStringLiteral("id"))) { 0364 stationIdPositionalInfo[0] = curpos; 0365 stationIdPositionalInfo[1] = labelLength; 0366 } 0367 0368 curpos += labelLength + 1; 0369 } 0370 } 0371 0372 void DWDIon::parseStationData(QByteArray data) 0373 { 0374 QString stringData = QString::fromLatin1(data); 0375 QStringList lines = stringData.split(QChar::LineFeed); 0376 0377 QList<int> namePositionalInfo(2); 0378 QList<int> stationIdPositionalInfo(2); 0379 calculatePositions(lines, namePositionalInfo, stationIdPositionalInfo); 0380 0381 // This loop parses the station file (https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_stationskatalog.cfg) 0382 // ID ICAO NAME LAT LON ELEV 0383 // ----- ---- -------------------- ----- ------- ----- 0384 // 01001 ENJA JAN MAYEN 70.56 -8.40 10 0385 // 01008 ENSB SVALBARD 78.15 15.28 29 0386 int lineIndex = 0; 0387 for (const QString &line : std::as_const(lines)) { 0388 QString name = line.mid(namePositionalInfo[0], namePositionalInfo[1]).trimmed(); 0389 QString id = line.mid(stationIdPositionalInfo[0], stationIdPositionalInfo[1]).trimmed(); 0390 0391 // This checks if this station is a station we know is working 0392 // With this we remove all non working but also a lot of working ones. 0393 if (id.startsWith(QLatin1Char('0')) || id.startsWith(QLatin1Char('1'))) { 0394 m_place.insert(camelCaseString(name), id); 0395 } else if (lineIndex > 10) { 0396 // After header is passed and some more lines for safety, abort parse if filter fails, all acceptable stations were found 0397 break; 0398 } 0399 0400 lineIndex += 1; 0401 } 0402 qCDebug(IONENGINE_dwd) << "Number of parsed stations: " << m_place.size(); 0403 } 0404 0405 void DWDIon::searchInStationList(const QString searchText) 0406 { 0407 qCDebug(IONENGINE_dwd) << searchText; 0408 0409 QMap<QString, QString>::const_iterator it = m_place.constBegin(); 0410 auto end = m_place.constEnd(); 0411 0412 while (it != end) { 0413 QString name = it.key(); 0414 if (name.contains(searchText, Qt::CaseInsensitive)) { 0415 m_locations.append(it.key()); 0416 } 0417 ++it; 0418 } 0419 0420 validate(searchText); 0421 } 0422 0423 void DWDIon::parseForecastData(const QString source, QJsonDocument doc) 0424 { 0425 QVariantMap weatherMap = doc.object().toVariantMap().first().toMap(); 0426 if (!weatherMap.isEmpty()) { 0427 // Forecast data 0428 QVariantList daysList = weatherMap[QStringLiteral("days")].toList(); 0429 0430 WeatherData &weatherData = m_weatherData[source]; 0431 QList<WeatherData::ForecastInfo *> &forecasts = weatherData.forecasts; 0432 0433 // Flush out the old forecasts when updating. 0434 forecasts.clear(); 0435 0436 WeatherData::ForecastInfo *forecast = new WeatherData::ForecastInfo; 0437 0438 int dayNumber = 0; 0439 0440 for (const QVariant &day : daysList) { 0441 QMap dayMap = day.toMap(); 0442 QString period = dayMap[QStringLiteral("dayDate")].toString(); 0443 QString cond = dayMap[QStringLiteral("icon")].toString(); 0444 0445 forecast->period = QDateTime::fromString(period, QStringLiteral("yyyy-MM-dd")); 0446 forecast->tempHigh = parseNumber(dayMap[QStringLiteral("temperatureMax")]); 0447 forecast->tempLow = parseNumber(dayMap[QStringLiteral("temperatureMin")]); 0448 forecast->precipitation = dayMap[QStringLiteral("precipitation")].toInt(); 0449 forecast->iconName = getWeatherIcon(dayIcons(), cond); 0450 ; 0451 0452 if (dayNumber == 0) { 0453 // These alternative measurements are used, when the stations doesn't have it's own measurements, uses forecast data from the current day 0454 weatherData.windSpeedAlt = parseNumber(dayMap[QStringLiteral("windSpeed")]); 0455 weatherData.gustSpeedAlt = parseNumber(dayMap[QStringLiteral("windGust")]); 0456 QString windDirection = roundWindDirections(dayMap[QStringLiteral("windDirection")].toInt()); 0457 weatherData.windDirectionAlt = getWindDirectionIcon(windIcons(), windDirection); 0458 } 0459 0460 forecasts.append(forecast); 0461 forecast = new WeatherData::ForecastInfo; 0462 0463 dayNumber++; 0464 // Only get the next 7 days (including today) 0465 if (dayNumber == 7) 0466 break; 0467 } 0468 0469 delete forecast; 0470 0471 // Warnings data 0472 QVariantList warningData = weatherMap[QStringLiteral("warnings")].toList(); 0473 0474 QList<WeatherData::WarningInfo *> &warningList = weatherData.warnings; 0475 0476 // Flush out the old forecasts when updating. 0477 warningList.clear(); 0478 0479 WeatherData::WarningInfo *warning = new WeatherData::WarningInfo; 0480 0481 for (const QVariant &warningElement : warningData) { 0482 QMap warningMap = warningElement.toMap(); 0483 0484 warning->headline = warningMap[QStringLiteral("headline")].toString(); 0485 warning->description = warningMap[QStringLiteral("description")].toString(); 0486 warning->priority = warningMap[QStringLiteral("level")].toInt(); 0487 warning->type = warningMap[QStringLiteral("event")].toString(); 0488 warning->timestamp = QDateTime::fromMSecsSinceEpoch(warningMap[QStringLiteral("start")].toLongLong()); 0489 0490 warningList.append(warning); 0491 warning = new WeatherData::WarningInfo; 0492 } 0493 0494 delete warning; 0495 0496 weatherData.isForecastsDataPending = false; 0497 0498 updateWeather(source); 0499 } 0500 } 0501 0502 void DWDIon::parseMeasureData(const QString source, QJsonDocument doc) 0503 { 0504 WeatherData &weatherData = m_weatherData[source]; 0505 QVariantMap weatherMap = doc.object().toVariantMap(); 0506 0507 if (!weatherMap.isEmpty()) { 0508 QDateTime time = QDateTime::fromMSecsSinceEpoch(weatherMap[QStringLiteral("time")].toLongLong()); 0509 weatherData.observationDateTime = time; 0510 0511 QString condIconNumber = weatherMap[QStringLiteral("icon")].toString(); 0512 if (condIconNumber != QLatin1String("")) { 0513 weatherData.conditionIcon = getWeatherIcon(dayIcons(), condIconNumber); 0514 } 0515 0516 bool windIconValid = false; 0517 const int windDirection = weatherMap[QStringLiteral("winddirection")].toInt(&windIconValid); 0518 if (windIconValid) { 0519 weatherData.windDirection = getWindDirectionIcon(windIcons(), roundWindDirections(windDirection)); 0520 } 0521 0522 weatherData.temperature = parseNumber(weatherMap[QStringLiteral("temperature")]); 0523 weatherData.humidity = parseNumber(weatherMap[QStringLiteral("humidity")]); 0524 weatherData.pressure = parseNumber(weatherMap[QStringLiteral("pressure")]); 0525 weatherData.windSpeed = parseNumber(weatherMap[QStringLiteral("meanwind")]); 0526 weatherData.gustSpeed = parseNumber(weatherMap[QStringLiteral("maxwind")]); 0527 weatherData.dewpoint = parseNumber(weatherMap[QStringLiteral("dewpoint")]); 0528 } 0529 0530 weatherData.isMeasureDataPending = false; 0531 0532 updateWeather(source); 0533 } 0534 0535 void DWDIon::validate(const QString &searchText) 0536 { 0537 const QString source(QStringLiteral("dwd|validate|") + searchText); 0538 0539 if (m_locations.isEmpty()) { 0540 const QString invalidPlace = searchText; 0541 setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("dwd|invalid|multiple|") + invalidPlace)); 0542 return; 0543 } 0544 0545 QString placeList; 0546 for (const QString &place : std::as_const(m_locations)) { 0547 placeList.append(QStringLiteral("|place|") + place + QStringLiteral("|extra|") + m_place[place]); 0548 } 0549 if (m_locations.count() > 1) { 0550 setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("dwd|valid|multiple") + placeList)); 0551 } else { 0552 placeList[7] = placeList[7].toUpper(); 0553 setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("dwd|valid|single") + placeList)); 0554 } 0555 m_locations.clear(); 0556 } 0557 0558 void DWDIon::updateWeather(const QString &source) 0559 { 0560 const WeatherData &weatherData = m_weatherData[source]; 0561 0562 if (weatherData.isForecastsDataPending || weatherData.isMeasureDataPending) { 0563 return; 0564 } 0565 0566 QString placeCode = m_place[source]; 0567 QString weatherSource = QStringLiteral("dwd|weather|%1|%2").arg(source, placeCode); 0568 0569 Plasma5Support::DataEngine::Data data; 0570 0571 data.insert(QStringLiteral("Place"), source); 0572 data.insert(QStringLiteral("Station"), source); 0573 0574 data.insert(QStringLiteral("Temperature Unit"), KUnitConversion::Celsius); 0575 data.insert(QStringLiteral("Wind Speed Unit"), KUnitConversion::KilometerPerHour); 0576 data.insert(QStringLiteral("Humidity Unit"), KUnitConversion::Percent); 0577 data.insert(QStringLiteral("Pressure Unit"), KUnitConversion::Hectopascal); 0578 0579 if (!weatherData.observationDateTime.isNull()) 0580 data.insert(QStringLiteral("Observation Timestamp"), weatherData.observationDateTime); 0581 else 0582 data.insert(QStringLiteral("Observation Timestamp"), QDateTime::currentDateTime()); 0583 0584 if (!weatherData.conditionIcon.isEmpty()) 0585 data.insert(QStringLiteral("Condition Icon"), weatherData.conditionIcon); 0586 0587 if (!qIsNaN(weatherData.temperature)) 0588 data.insert(QStringLiteral("Temperature"), weatherData.temperature); 0589 0590 if (!qIsNaN(weatherData.humidity)) 0591 data.insert(QStringLiteral("Humidity"), weatherData.humidity); 0592 0593 if (!qIsNaN(weatherData.pressure)) 0594 data.insert(QStringLiteral("Pressure"), weatherData.pressure); 0595 0596 if (!qIsNaN(weatherData.dewpoint)) 0597 data.insert(QStringLiteral("Dewpoint"), weatherData.dewpoint); 0598 0599 if (!qIsNaN(weatherData.windSpeed)) 0600 data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeed); 0601 else 0602 data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeedAlt); 0603 0604 if (!qIsNaN(weatherData.gustSpeed)) 0605 data.insert(QStringLiteral("Wind Gust Speed"), weatherData.gustSpeed); 0606 else 0607 data.insert(QStringLiteral("Wind Gust Speed"), weatherData.gustSpeedAlt); 0608 0609 if (!weatherData.windDirection.isEmpty()) { 0610 data.insert(QStringLiteral("Wind Direction"), weatherData.windDirection); 0611 } else { 0612 data.insert(QStringLiteral("Wind Direction"), weatherData.windDirectionAlt); 0613 } 0614 0615 int dayNumber = 0; 0616 for (const WeatherData::ForecastInfo *dayForecast : weatherData.forecasts) { 0617 QString period; 0618 if (dayNumber == 0) { 0619 period = i18nc("Short for Today", "Today"); 0620 } else { 0621 period = dayForecast->period.toString(QStringLiteral("dddd")); 0622 0623 period.replace(QStringLiteral("Saturday"), i18nc("Short for Saturday", "Sat")); 0624 period.replace(QStringLiteral("Sunday"), i18nc("Short for Sunday", "Sun")); 0625 period.replace(QStringLiteral("Monday"), i18nc("Short for Monday", "Mon")); 0626 period.replace(QStringLiteral("Tuesday"), i18nc("Short for Tuesday", "Tue")); 0627 period.replace(QStringLiteral("Wednesday"), i18nc("Short for Wednesday", "Wed")); 0628 period.replace(QStringLiteral("Thursday"), i18nc("Short for Thursday", "Thu")); 0629 period.replace(QStringLiteral("Friday"), i18nc("Short for Friday", "Fri")); 0630 } 0631 0632 data.insert(QStringLiteral("Short Forecast Day %1").arg(dayNumber), 0633 QStringLiteral("%1|%2|%3|%4|%5|%6") 0634 .arg(period, dayForecast->iconName, QLatin1String("")) 0635 .arg(dayForecast->tempHigh) 0636 .arg(dayForecast->tempLow) 0637 .arg(dayForecast->precipitation)); 0638 dayNumber++; 0639 } 0640 0641 int k = 0; 0642 0643 for (const WeatherData::WarningInfo *warning : weatherData.warnings) { 0644 const QString number = QString::number(k); 0645 0646 data.insert(QStringLiteral("Warning Priority ") + number, warning->priority); 0647 data.insert(QStringLiteral("Warning Description ") + number, QStringLiteral("<p><b>%1</b></p>%2").arg(warning->headline, warning->description)); 0648 data.insert(QStringLiteral("Warning Timestamp ") + number, warning->timestamp.toString(QStringLiteral("dd.MM.yyyy"))); 0649 0650 ++k; 0651 } 0652 0653 data.insert(QStringLiteral("Total Weather Days"), weatherData.forecasts.size()); 0654 data.insert(QStringLiteral("Total Warnings Issued"), weatherData.warnings.size()); 0655 data.insert(QStringLiteral("Credit"), i18nc("credit line, don't change name!", "Source: Deutscher Wetterdienst")); 0656 data.insert(QStringLiteral("Credit Url"), QStringLiteral("https://www.dwd.de/")); 0657 0658 setData(weatherSource, data); 0659 } 0660 0661 /* 0662 * Helper methods 0663 */ 0664 float DWDIon::parseNumber(QVariant number) 0665 { 0666 bool isValid = false; 0667 const int intValue = number.toInt(&isValid); 0668 if (!isValid) { 0669 return NAN; 0670 } 0671 if (intValue == 0x7fff) { // DWD uses 32767 to mark an error value 0672 return NAN; 0673 } 0674 // e.g. DWD API int 17 equals 1.7 0675 return static_cast<float>(intValue) / 10; 0676 } 0677 0678 QString DWDIon::roundWindDirections(int windDirection) 0679 { 0680 QString roundedWindDirection = QString::number(qRound(((float)windDirection) / 100) * 10); 0681 return roundedWindDirection; 0682 } 0683 0684 QString DWDIon::extractString(QByteArray array, int start, int length) 0685 { 0686 QString string; 0687 0688 for (int i = start; i < start + length; i++) { 0689 string.append(QLatin1Char(array[i])); 0690 } 0691 0692 return string; 0693 } 0694 0695 QString DWDIon::camelCaseString(const QString text) 0696 { 0697 QString result; 0698 bool nextBig = true; 0699 0700 for (QChar c : text) { 0701 if (c.isLetter()) { 0702 if (nextBig) { 0703 result.append(c.toUpper()); 0704 nextBig = false; 0705 } else { 0706 result.append(c.toLower()); 0707 } 0708 } else { 0709 if (c == QChar::Space || c == QLatin1Char('-')) { 0710 nextBig = true; 0711 } 0712 result.append(c); 0713 } 0714 } 0715 0716 return result; 0717 } 0718 0719 K_PLUGIN_CLASS_WITH_JSON(DWDIon, "ion-dwd.json") 0720 0721 #include "ion_dwd.moc"