Created
January 20, 2020 17:43
-
-
Save MarkTiedemann/767b5cdecee5ee2aed1b4dc6518c15dd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fs = require("fs"); | |
const https = require("https"); | |
const jsdom = require("jsdom"); | |
const unzipper = require("unzipper"); | |
main(); | |
function main() { | |
// To get an overview, see: | |
// https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/met_verfahren_mosmix.html | |
// For a list of station names, see: | |
// https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_stationskatalog.cfg?view=nasPublication&nn=16102 | |
let station_name = "TRITTAU"; | |
let today = new Date(); | |
let dates = [today, calc_tomorrow(today)]; | |
// For a list of possible values, see: | |
// https://opendata.dwd.de/weather/lib/MetElementDefinition.xml | |
let values = [ | |
"TTT", // Temperature 2m above surface (in K) | |
"FF" // Wind speed (in m/s) | |
]; | |
query(station_name, dates, values, (err, results) => { | |
if (err) { | |
console.error(err); | |
process.exit(1); | |
} | |
console.log(`====== FORECASTS ====== | |
[Today] | |
Date: ${format_date(results[0][0])} | |
Time: ${format_date_time(results[0][0])} | |
Temperature: ${round_two_dec_places(kelvin_to_celsius(results[0][1]))} °C | |
Wind speed: ${round_two_dec_places(mps_to_kmh(results[0][2]))} km/h | |
[Tomorrow] | |
Date: ${format_date(results[1][0])} | |
Time: ${format_date_time(results[1][0])} | |
Temperature: ${round_two_dec_places(kelvin_to_celsius(results[1][1]))} °C | |
Wind speed: ${round_two_dec_places(mps_to_kmh(results[1][2]))} km/h | |
=======================`); | |
}); | |
} | |
function query(station_name, dates, units, cb) { | |
let simple_date = get_simple_date(dates[0]); | |
fetch_station_id(station_name, (err, station_id) => { | |
if (err) return cb(err); | |
fetch_latest_measurement(station_id, simple_date, err => { | |
if (err) return cb(err); | |
unzip_latest_measurement(station_id, simple_date, err => { | |
if (err) return cb(err); | |
parse_latest_measurenment(station_id, simple_date, (err, doc) => { | |
if (err) return cb(err); | |
let time_steps = get_time_steps(doc); | |
let results = dates.map(date => { | |
let closest_index = closest_date(time_steps, date.getTime()); | |
let values = units.map(unit => get_forecast(doc, unit)[closest_index]); | |
return [time_steps[closest_index], ...values]; | |
}); | |
cb(null, results); | |
}); | |
}); | |
}); | |
}); | |
} | |
function fetch_station_id(station_name, cb) { | |
ensure_file_downloaded( | |
"https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_stationskatalog.cfg?view=nasPublication&nn=16102", | |
".cache/mosmix_stationskatalog.cfg", | |
err => { | |
if (err) return cb(err); | |
fs.readFile(".cache/mosmix_stationskatalog.cfg", (err, buf) => { | |
if (err) return cb(err); | |
let lines = buf.toString("utf-8").split("\n"); | |
let station_line = lines.find(l => l.includes(station_name)); | |
if (!station_line) return cb(new Error(`Unable to find station '${station_name}'`)); | |
cb(null, station_line.substr(12, 5)); | |
}); | |
} | |
); | |
} | |
function fetch_latest_measurement(station_id, { year, month, day }, cb) { | |
ensure_file_downloaded( | |
`https://opendata.dwd.de/weather/local_forecasts/mos/MOSMIX_L/single_stations/${station_id}/kml/MOSMIX_L_LATEST_${station_id}.kmz`, | |
`.cache/${station_id}_${year}_${month}_${day}.kmz`, | |
cb | |
); | |
} | |
function unzip_latest_measurement(station_id, { year, month, day }, cb) { | |
fs.exists(`.cache/${station_id}_${year}_${month}_${day}.kml`, is_unzipped => { | |
if (!is_unzipped) { | |
fs | |
.createReadStream(`.cache/${station_id}_${year}_${month}_${day}.kmz`) | |
.pipe(unzipper.ParseOne(null, {})) | |
.pipe(fs.createWriteStream(`.cache/${station_id}_${year}_${month}_${day}.kml`)) | |
.on("error", cb) | |
.on("close", cb); | |
} else { | |
cb(); | |
} | |
}); | |
} | |
function parse_latest_measurenment(station_id, { year, month, day }, cb) { | |
fs.readFile(`.cache/${station_id}_${year}_${month}_${day}.kml`, (err, buf) => { | |
if (err) return cb(err); | |
let xml = buf.toString("utf-8"); | |
let dom = new jsdom.JSDOM(xml, { contentType: "application/xml" }); | |
let doc = dom.window.document; | |
cb(null, doc); | |
}); | |
} | |
function get_time_steps(doc) { | |
return Array.from(doc.querySelectorAll("dwd\\:TimeStep")).map(ts => new Date(ts.textContent)); | |
} | |
function get_forecast(doc, unit) { | |
return doc | |
.querySelector(`dwd\\:Forecast[dwd\\:elementName=${unit}] dwd\\:value`) | |
.textContent.trim() | |
.replace(/\s+/g, " ") | |
.split(" ") | |
.map(s => (s === "-" ? NaN : parseFloat(s))); | |
} | |
/*** UTILS ***/ | |
function ensure_file_downloaded(url, path, cb) { | |
fs.exists(path, is_cached => { | |
if (!is_cached) { | |
https.get(url, res => { | |
res | |
.pipe(fs.createWriteStream(path)) | |
.on("error", cb) | |
.on("close", cb); | |
}); | |
} else { | |
cb(); | |
} | |
}); | |
} | |
function calc_tomorrow(today) { | |
let tomorrow = new Date(today); | |
tomorrow.setDate(tomorrow.getDate() + 1); | |
return tomorrow; | |
} | |
function get_simple_date(date) { | |
let year = date.getFullYear(); | |
let month = pad_two_zeros(date.getMonth() + 1); | |
let day = pad_two_zeros(date.getDate()); | |
return { year, month, day }; | |
} | |
function pad_two_zeros(n) { | |
return n.toString().padStart(2, "0"); | |
} | |
function kelvin_to_celsius(n) { | |
return n - 273.15; | |
} | |
function mps_to_kmh(n) { | |
return n * 3.6; | |
} | |
function round_two_dec_places(n) { | |
return Math.round(n * 100 + Number.EPSILON) / 100; | |
} | |
function closest_date(dates, target_time) { | |
let closest_distance = Infinity; | |
let winner_index = -1; | |
for (let i = 0; i < dates.length; i++) { | |
let distance = Math.abs(dates[i].getTime() - target_time); | |
if (distance < closest_distance) { | |
closest_distance = distance; | |
winner_index = i; | |
} | |
} | |
return winner_index; | |
} | |
function format_date(date) { | |
let year = date.getFullYear(); | |
let month = pad_two_zeros(date.getMonth() + 1); | |
let day = pad_two_zeros(date.getDate()); | |
return `${day}.${month}.${year}`; | |
} | |
function format_date_time(date) { | |
let hours = pad_two_zeros(date.getHours()); | |
let minutes = pad_two_zeros(date.getMinutes()); | |
return `${hours}:${minutes}`; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment