Skip to content

Instantly share code, notes, and snippets.

@MarkTiedemann
Created January 20, 2020 17:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MarkTiedemann/767b5cdecee5ee2aed1b4dc6518c15dd to your computer and use it in GitHub Desktop.
Save MarkTiedemann/767b5cdecee5ee2aed1b4dc6518c15dd to your computer and use it in GitHub Desktop.
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