631 lines
22 KiB
JavaScript
Raw Normal View History

New weather provider: Yr.no (#2948) # Added Yr.no as a weather provider Yr.no is a free Norwegian weather service. The configuration is quite simple: ```js { weatherProvider: "yr", lat: 59.9171, lon: 10.7276, altitude: 30 } ``` The latitude and longitude cannot have more than 4 decimals, but that should be plenty. To quote yr: "There is no need to ask for weather forecasts with nanometer precision!". The altitude should be meters above sea level and defaults to 0. If `type` is set to `current` the symbol can display the next 1, 6 or 12 hours by setting `currentForecastHours` (default is 1). It states in [Getting started-guide](https://developer.yr.no/doc/GettingStarted/) that users of the API should cache the results and use the `Expires`-header to know when to ask for new data. By using the `If-Modified-Since`-header we can avoid downloading the same data over and over again. I chose not to override the `User-Agent`-header set in [`server.js`](https://github.com/MichMich/MagicMirror/blob/a328ce5/js/server.js#L97) even though it does not comply with [the terms of service](https://developer.yr.no/doc/TermsOfService/). It currently works with the default header, and by searching the web for MagicMirror the GitHub-repo should be easy to find without an explicit link. I also had to make some minor changes to `server.js` and `weatherprovider.js` to be able to send and return HTTP headers. To handle the HTTP 304 response without body I chose to return `undefined` so we easily can use the response as a condition: `if (response) ...`. The documentation for the API is available here: - [API Reference overview](https://api.met.no/weatherapi/) - [Locationforecast](https://api.met.no/weatherapi/locationforecast/2.0/) - Used to get the weather forecast - [Sunrise](https://api.met.no/weatherapi/sunrise/2.0/documentation) - used to find sunrise and sunset times Co-authored-by: Veeck <github@veeck.de>
2022-11-08 06:18:47 +01:00
/* global WeatherProvider, WeatherObject */
/* MagicMirror²
* Module: Weather
* Provider: Yr.no
*
* By Magnus Marthinsen
* MIT Licensed
*
* This class is a provider for Yr.no, a norwegian weather service.
New weather provider: Yr.no (#2948) # Added Yr.no as a weather provider Yr.no is a free Norwegian weather service. The configuration is quite simple: ```js { weatherProvider: "yr", lat: 59.9171, lon: 10.7276, altitude: 30 } ``` The latitude and longitude cannot have more than 4 decimals, but that should be plenty. To quote yr: "There is no need to ask for weather forecasts with nanometer precision!". The altitude should be meters above sea level and defaults to 0. If `type` is set to `current` the symbol can display the next 1, 6 or 12 hours by setting `currentForecastHours` (default is 1). It states in [Getting started-guide](https://developer.yr.no/doc/GettingStarted/) that users of the API should cache the results and use the `Expires`-header to know when to ask for new data. By using the `If-Modified-Since`-header we can avoid downloading the same data over and over again. I chose not to override the `User-Agent`-header set in [`server.js`](https://github.com/MichMich/MagicMirror/blob/a328ce5/js/server.js#L97) even though it does not comply with [the terms of service](https://developer.yr.no/doc/TermsOfService/). It currently works with the default header, and by searching the web for MagicMirror the GitHub-repo should be easy to find without an explicit link. I also had to make some minor changes to `server.js` and `weatherprovider.js` to be able to send and return HTTP headers. To handle the HTTP 304 response without body I chose to return `undefined` so we easily can use the response as a condition: `if (response) ...`. The documentation for the API is available here: - [API Reference overview](https://api.met.no/weatherapi/) - [Locationforecast](https://api.met.no/weatherapi/locationforecast/2.0/) - Used to get the weather forecast - [Sunrise](https://api.met.no/weatherapi/sunrise/2.0/documentation) - used to find sunrise and sunset times Co-authored-by: Veeck <github@veeck.de>
2022-11-08 06:18:47 +01:00
*
* Terms of service: https://developer.yr.no/doc/TermsOfService/
*/
WeatherProvider.register("yr", {
providerName: "Yr",
// Set the default config properties that is specific to this provider
defaults: {
useCorsProxy: true,
apiBase: "https://api.met.no/weatherapi",
altitude: 0,
currentForecastHours: 1 //1, 6 or 12
},
start() {
if (typeof Storage === "undefined") {
//local storage unavailable
Log.error("The Yr weather provider requires local storage.");
throw new Error("Local storage not available");
}
Log.info(`Weather provider: ${this.providerName} started.`);
},
fetchCurrentWeather() {
this.getCurrentWeather()
.then((currentWeather) => {
this.setCurrentWeather(currentWeather);
this.updateAvailable();
})
.catch((error) => {
Log.error(error);
throw new Error(error);
});
},
async getCurrentWeather() {
const getRequests = [this.getWeatherData(), this.getStellarData()];
const [weatherData, stellarData] = await Promise.all(getRequests);
if (!stellarData) {
Log.warn("No stellar data available.");
New weather provider: Yr.no (#2948) # Added Yr.no as a weather provider Yr.no is a free Norwegian weather service. The configuration is quite simple: ```js { weatherProvider: "yr", lat: 59.9171, lon: 10.7276, altitude: 30 } ``` The latitude and longitude cannot have more than 4 decimals, but that should be plenty. To quote yr: "There is no need to ask for weather forecasts with nanometer precision!". The altitude should be meters above sea level and defaults to 0. If `type` is set to `current` the symbol can display the next 1, 6 or 12 hours by setting `currentForecastHours` (default is 1). It states in [Getting started-guide](https://developer.yr.no/doc/GettingStarted/) that users of the API should cache the results and use the `Expires`-header to know when to ask for new data. By using the `If-Modified-Since`-header we can avoid downloading the same data over and over again. I chose not to override the `User-Agent`-header set in [`server.js`](https://github.com/MichMich/MagicMirror/blob/a328ce5/js/server.js#L97) even though it does not comply with [the terms of service](https://developer.yr.no/doc/TermsOfService/). It currently works with the default header, and by searching the web for MagicMirror the GitHub-repo should be easy to find without an explicit link. I also had to make some minor changes to `server.js` and `weatherprovider.js` to be able to send and return HTTP headers. To handle the HTTP 304 response without body I chose to return `undefined` so we easily can use the response as a condition: `if (response) ...`. The documentation for the API is available here: - [API Reference overview](https://api.met.no/weatherapi/) - [Locationforecast](https://api.met.no/weatherapi/locationforecast/2.0/) - Used to get the weather forecast - [Sunrise](https://api.met.no/weatherapi/sunrise/2.0/documentation) - used to find sunrise and sunset times Co-authored-by: Veeck <github@veeck.de>
2022-11-08 06:18:47 +01:00
}
if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) {
Log.error("No weather data available.");
return;
}
const currentTime = moment();
let forecast = weatherData.properties.timeseries[0];
let closestTimeInPast = currentTime.diff(moment(forecast.time));
for (const forecastTime of weatherData.properties.timeseries) {
const comparison = currentTime.diff(moment(forecastTime.time));
if (0 < comparison && comparison < closestTimeInPast) {
closestTimeInPast = comparison;
forecast = forecastTime;
}
}
const forecastXHours = this.getForecastForXHoursFrom(forecast.data);
forecast.weatherType = this.convertWeatherType(forecastXHours.summary.symbol_code, forecast.time);
forecast.precipitationAmount = forecastXHours.details?.precipitation_amount;
forecast.precipitationProbability = forecastXHours.details?.probability_of_precipitation;
New weather provider: Yr.no (#2948) # Added Yr.no as a weather provider Yr.no is a free Norwegian weather service. The configuration is quite simple: ```js { weatherProvider: "yr", lat: 59.9171, lon: 10.7276, altitude: 30 } ``` The latitude and longitude cannot have more than 4 decimals, but that should be plenty. To quote yr: "There is no need to ask for weather forecasts with nanometer precision!". The altitude should be meters above sea level and defaults to 0. If `type` is set to `current` the symbol can display the next 1, 6 or 12 hours by setting `currentForecastHours` (default is 1). It states in [Getting started-guide](https://developer.yr.no/doc/GettingStarted/) that users of the API should cache the results and use the `Expires`-header to know when to ask for new data. By using the `If-Modified-Since`-header we can avoid downloading the same data over and over again. I chose not to override the `User-Agent`-header set in [`server.js`](https://github.com/MichMich/MagicMirror/blob/a328ce5/js/server.js#L97) even though it does not comply with [the terms of service](https://developer.yr.no/doc/TermsOfService/). It currently works with the default header, and by searching the web for MagicMirror the GitHub-repo should be easy to find without an explicit link. I also had to make some minor changes to `server.js` and `weatherprovider.js` to be able to send and return HTTP headers. To handle the HTTP 304 response without body I chose to return `undefined` so we easily can use the response as a condition: `if (response) ...`. The documentation for the API is available here: - [API Reference overview](https://api.met.no/weatherapi/) - [Locationforecast](https://api.met.no/weatherapi/locationforecast/2.0/) - Used to get the weather forecast - [Sunrise](https://api.met.no/weatherapi/sunrise/2.0/documentation) - used to find sunrise and sunset times Co-authored-by: Veeck <github@veeck.de>
2022-11-08 06:18:47 +01:00
forecast.minTemperature = forecastXHours.details?.air_temperature_min;
forecast.maxTemperature = forecastXHours.details?.air_temperature_max;
return this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units);
},
getWeatherData() {
return new Promise((resolve, reject) => {
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
// This is to avoid multiple similar calls to the API.
let shouldWait = localStorage.getItem("yrIsFetchingWeatherData");
if (shouldWait) {
const checkForGo = setInterval(function () {
shouldWait = localStorage.getItem("yrIsFetchingWeatherData");
}, 100);
setTimeout(function () {
clearInterval(checkForGo);
shouldWait = false;
}, 5000); //Assume other fetch finished but failed to remove lock
const attemptFetchWeather = setInterval(() => {
if (!shouldWait) {
clearInterval(checkForGo);
clearInterval(attemptFetchWeather);
this.getWeatherDataFromYrOrCache(resolve, reject);
}
}, 100);
} else {
this.getWeatherDataFromYrOrCache(resolve, reject);
}
});
},
getWeatherDataFromYrOrCache(resolve, reject) {
localStorage.setItem("yrIsFetchingWeatherData", "true");
let weatherData = this.getWeatherDataFromCache();
if (this.weatherDataIsValid(weatherData)) {
localStorage.removeItem("yrIsFetchingWeatherData");
Log.debug("Weather data found in cache.");
resolve(weatherData);
} else {
this.getWeatherDataFromYr(weatherData?.downloadedAt)
.then((weatherData) => {
Log.debug("Got weather data from yr.");
if (weatherData) {
this.cacheWeatherData(weatherData);
} else {
//Undefined if unchanged
weatherData = this.getWeatherDataFromCache();
}
resolve(weatherData);
})
.catch((err) => {
Log.error(err);
reject("Unable to get weather data from Yr.");
})
.finally(() => {
localStorage.removeItem("yrIsFetchingWeatherData");
});
}
},
weatherDataIsValid(weatherData) {
return (
weatherData &&
weatherData.timeout &&
0 < moment(weatherData.timeout).diff(moment()) &&
(!weatherData.geometry || !weatherData.geometry.coordinates || !weatherData.geometry.coordinates.length < 2 || (weatherData.geometry.coordinates[0] === this.config.lat && weatherData.geometry.coordinates[1] === this.config.lon))
);
},
getWeatherDataFromCache() {
const weatherData = localStorage.getItem("weatherData");
if (weatherData) {
return JSON.parse(weatherData);
} else {
return undefined;
}
},
getWeatherDataFromYr(currentDataFetchedAt) {
const requestHeaders = [{ name: "Accept", value: "application/json" }];
if (currentDataFetchedAt) {
requestHeaders.push({ name: "If-Modified-Since", value: currentDataFetchedAt });
}
const expectedResponseHeaders = ["expires", "date"];
return this.fetchData(this.getForecastUrl(), "json", requestHeaders, expectedResponseHeaders)
.then((data) => {
if (!data || !data.headers) return data;
data.timeout = data.headers.find((header) => header.name === "expires").value;
data.downloadedAt = data.headers.find((header) => header.name === "date").value;
data.headers = undefined;
return data;
})
.catch((err) => {
Log.error("Could not load weather data.", err);
throw new Error(err);
});
},
getForecastUrl() {
if (!this.config.lat) {
Log.error("Latitude not provided.");
throw new Error("Latitude not provided.");
}
if (!this.config.lon) {
Log.error("Longitude not provided.");
throw new Error("Longitude not provided.");
}
let lat = this.config.lat.toString();
let lon = this.config.lon.toString();
const altitude = this.config.altitude ?? 0;
if (lat.includes(".") && lat.split(".")[1].length > 4) {
Log.warn("Latitude is too specific for weather data. Do not use more than four decimals. Trimming to maximum length.");
const latParts = lat.split(".");
lat = `${latParts[0]}.${latParts[1].substring(0, 4)}`;
}
if (lon.includes(".") && lon.split(".")[1].length > 4) {
Log.warn("Longitude is too specific for weather data. Do not use more than four decimals. Trimming to maximum length.");
const lonParts = lon.split(".");
lon = `${lonParts[0]}.${lonParts[1].substring(0, 4)}`;
}
return `${this.config.apiBase}/locationforecast/2.0/complete?&altitude=${altitude}&lat=${lat}&lon=${lon}`;
},
cacheWeatherData(weatherData) {
localStorage.setItem("weatherData", JSON.stringify(weatherData));
},
getAuthenticationString() {
if (!this.config.authenticationEmail) throw new Error("Authentication email not provided.");
return `${this.config.applicaitionName} ${this.config.authenticationEmail}`;
},
getStellarData() {
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
// This is to avoid multiple similar calls to the API.
return new Promise((resolve, reject) => {
let shouldWait = localStorage.getItem("yrIsFetchingStellarData");
if (shouldWait) {
const checkForGo = setInterval(function () {
shouldWait = localStorage.getItem("yrIsFetchingStellarData");
}, 100);
setTimeout(function () {
clearInterval(checkForGo);
shouldWait = false;
}, 5000); //Assume other fetch finished but failed to remove lock
const attemptFetchWeather = setInterval(() => {
if (!shouldWait) {
clearInterval(checkForGo);
clearInterval(attemptFetchWeather);
this.getStellarDataFromYrOrCache(resolve, reject);
}
}, 100);
} else {
this.getStellarDataFromYrOrCache(resolve, reject);
}
});
},
getStellarDataFromYrOrCache(resolve, reject) {
localStorage.setItem("yrIsFetchingStellarData", "true");
let stellarData = this.getStellarDataFromCache();
const today = moment().format("YYYY-MM-DD");
const tomorrow = moment().add(1, "days").format("YYYY-MM-DD");
if (stellarData && stellarData.today && stellarData.today.date === today && stellarData.tomorrow && stellarData.tomorrow.date === tomorrow) {
Log.debug("Stellar data found in cache.");
localStorage.removeItem("yrIsFetchingStellarData");
resolve(stellarData);
} else if (stellarData && stellarData.tomorrow && stellarData.tomorrow.date === today) {
Log.debug("stellar data for today found in cache, but not for tomorrow.");
stellarData.today = stellarData.tomorrow;
this.getStellarDataFromYr(tomorrow)
.then((data) => {
if (data) {
data.date = tomorrow;
stellarData.tomorrow = data;
this.cacheStellarData(stellarData);
resolve(stellarData);
} else {
reject("No stellar data returned from Yr for " + tomorrow);
}
})
.catch((err) => {
Log.error(err);
reject("Unable to get stellar data from Yr for " + tomorrow);
})
.finally(() => {
localStorage.removeItem("yrIsFetchingStellarData");
});
} else {
this.getStellarDataFromYr(today, 2)
.then((stellarData) => {
if (stellarData) {
stellarData = {
today: stellarData
};
stellarData.tomorrow = Object.assign({}, stellarData.today);
stellarData.today.date = today;
stellarData.tomorrow.date = tomorrow;
this.cacheStellarData(stellarData);
resolve(stellarData);
} else {
Log.error("Something went wrong when fetching stellar data. Responses: " + stellarData);
reject(stellarData);
}
})
.catch((err) => {
Log.error(err);
reject("Unable to get stellar data from Yr.");
})
.finally(() => {
localStorage.removeItem("yrIsFetchingStellarData");
});
}
},
getStellarDataFromCache() {
const stellarData = localStorage.getItem("stellarData");
if (stellarData) {
return JSON.parse(stellarData);
} else {
return undefined;
}
},
getStellarDataFromYr(date, days = 1) {
const requestHeaders = [{ name: "Accept", value: "application/json" }];
return this.fetchData(this.getStellarDatatUrl(date, days), "json", requestHeaders)
.then((data) => {
Log.debug("Got stellar data from yr.");
return data;
})
.catch((err) => {
Log.error("Could not load weather data.", err);
throw new Error(err);
});
},
getStellarDatatUrl(date, days) {
if (!this.config.lat) {
Log.error("Latitude not provided.");
throw new Error("Latitude not provided.");
}
if (!this.config.lon) {
Log.error("Longitude not provided.");
throw new Error("Longitude not provided.");
}
let lat = this.config.lat.toString();
let lon = this.config.lon.toString();
const altitude = this.config.altitude ?? 0;
if (lat.includes(".") && lat.split(".")[1].length > 4) {
Log.warn("Latitude is too specific for stellar data. Do not use more than four decimals. Trimming to maximum length.");
const latParts = lat.split(".");
lat = `${latParts[0]}.${latParts[1].substring(0, 4)}`;
}
if (lon.includes(".") && lon.split(".")[1].length > 4) {
Log.warn("Longitude is too specific for stellar data. Do not use more than four decimals. Trimming to maximum length.");
const lonParts = lon.split(".");
lon = `${lonParts[0]}.${lonParts[1].substring(0, 4)}`;
}
let utcOffset = moment().utcOffset() / 60;
let utcOffsetPrefix = "%2B";
if (utcOffset < 0) {
utcOffsetPrefix = "-";
}
utcOffset = Math.abs(utcOffset);
let minutes = "00";
if (utcOffset % 1 !== 0) {
minutes = "30";
}
let hours = Math.floor(utcOffset).toString();
if (hours.length < 2) {
hours = `0${hours}`;
}
return `${this.config.apiBase}/sunrise/2.0/.json?date=${date}&days=${days}&height=${altitude}&lat=${lat}&lon=${lon}&offset=${utcOffsetPrefix}${hours}%3A${minutes}`;
},
cacheStellarData(data) {
localStorage.setItem("stellarData", JSON.stringify(data));
},
getWeatherDataFrom(forecast, stellarData, units) {
const weather = new WeatherObject();
New weather provider: Yr.no (#2948) # Added Yr.no as a weather provider Yr.no is a free Norwegian weather service. The configuration is quite simple: ```js { weatherProvider: "yr", lat: 59.9171, lon: 10.7276, altitude: 30 } ``` The latitude and longitude cannot have more than 4 decimals, but that should be plenty. To quote yr: "There is no need to ask for weather forecasts with nanometer precision!". The altitude should be meters above sea level and defaults to 0. If `type` is set to `current` the symbol can display the next 1, 6 or 12 hours by setting `currentForecastHours` (default is 1). It states in [Getting started-guide](https://developer.yr.no/doc/GettingStarted/) that users of the API should cache the results and use the `Expires`-header to know when to ask for new data. By using the `If-Modified-Since`-header we can avoid downloading the same data over and over again. I chose not to override the `User-Agent`-header set in [`server.js`](https://github.com/MichMich/MagicMirror/blob/a328ce5/js/server.js#L97) even though it does not comply with [the terms of service](https://developer.yr.no/doc/TermsOfService/). It currently works with the default header, and by searching the web for MagicMirror the GitHub-repo should be easy to find without an explicit link. I also had to make some minor changes to `server.js` and `weatherprovider.js` to be able to send and return HTTP headers. To handle the HTTP 304 response without body I chose to return `undefined` so we easily can use the response as a condition: `if (response) ...`. The documentation for the API is available here: - [API Reference overview](https://api.met.no/weatherapi/) - [Locationforecast](https://api.met.no/weatherapi/locationforecast/2.0/) - Used to get the weather forecast - [Sunrise](https://api.met.no/weatherapi/sunrise/2.0/documentation) - used to find sunrise and sunset times Co-authored-by: Veeck <github@veeck.de>
2022-11-08 06:18:47 +01:00
const stellarTimesToday = stellarData?.today ? this.getStellarTimesFrom(stellarData.today, moment().format("YYYY-MM-DD")) : undefined;
const stellarTimesTomorrow = stellarData?.tomorrow ? this.getStellarTimesFrom(stellarData.tomorrow, moment().add(1, "days").format("YYYY-MM-DD")) : undefined;
weather.date = moment(forecast.time);
weather.windSpeed = forecast.data.instant.details.wind_speed;
weather.windFromDirection = forecast.data.instant.details.wind_from_direction;
New weather provider: Yr.no (#2948) # Added Yr.no as a weather provider Yr.no is a free Norwegian weather service. The configuration is quite simple: ```js { weatherProvider: "yr", lat: 59.9171, lon: 10.7276, altitude: 30 } ``` The latitude and longitude cannot have more than 4 decimals, but that should be plenty. To quote yr: "There is no need to ask for weather forecasts with nanometer precision!". The altitude should be meters above sea level and defaults to 0. If `type` is set to `current` the symbol can display the next 1, 6 or 12 hours by setting `currentForecastHours` (default is 1). It states in [Getting started-guide](https://developer.yr.no/doc/GettingStarted/) that users of the API should cache the results and use the `Expires`-header to know when to ask for new data. By using the `If-Modified-Since`-header we can avoid downloading the same data over and over again. I chose not to override the `User-Agent`-header set in [`server.js`](https://github.com/MichMich/MagicMirror/blob/a328ce5/js/server.js#L97) even though it does not comply with [the terms of service](https://developer.yr.no/doc/TermsOfService/). It currently works with the default header, and by searching the web for MagicMirror the GitHub-repo should be easy to find without an explicit link. I also had to make some minor changes to `server.js` and `weatherprovider.js` to be able to send and return HTTP headers. To handle the HTTP 304 response without body I chose to return `undefined` so we easily can use the response as a condition: `if (response) ...`. The documentation for the API is available here: - [API Reference overview](https://api.met.no/weatherapi/) - [Locationforecast](https://api.met.no/weatherapi/locationforecast/2.0/) - Used to get the weather forecast - [Sunrise](https://api.met.no/weatherapi/sunrise/2.0/documentation) - used to find sunrise and sunset times Co-authored-by: Veeck <github@veeck.de>
2022-11-08 06:18:47 +01:00
weather.temperature = forecast.data.instant.details.air_temperature;
weather.minTemperature = forecast.minTemperature;
weather.maxTemperature = forecast.maxTemperature;
weather.weatherType = forecast.weatherType;
weather.humidity = forecast.data.instant.details.relative_humidity;
weather.precipitationAmount = forecast.precipitationAmount;
weather.precipitationProbability = forecast.precipitationProbability;
New weather provider: Yr.no (#2948) # Added Yr.no as a weather provider Yr.no is a free Norwegian weather service. The configuration is quite simple: ```js { weatherProvider: "yr", lat: 59.9171, lon: 10.7276, altitude: 30 } ``` The latitude and longitude cannot have more than 4 decimals, but that should be plenty. To quote yr: "There is no need to ask for weather forecasts with nanometer precision!". The altitude should be meters above sea level and defaults to 0. If `type` is set to `current` the symbol can display the next 1, 6 or 12 hours by setting `currentForecastHours` (default is 1). It states in [Getting started-guide](https://developer.yr.no/doc/GettingStarted/) that users of the API should cache the results and use the `Expires`-header to know when to ask for new data. By using the `If-Modified-Since`-header we can avoid downloading the same data over and over again. I chose not to override the `User-Agent`-header set in [`server.js`](https://github.com/MichMich/MagicMirror/blob/a328ce5/js/server.js#L97) even though it does not comply with [the terms of service](https://developer.yr.no/doc/TermsOfService/). It currently works with the default header, and by searching the web for MagicMirror the GitHub-repo should be easy to find without an explicit link. I also had to make some minor changes to `server.js` and `weatherprovider.js` to be able to send and return HTTP headers. To handle the HTTP 304 response without body I chose to return `undefined` so we easily can use the response as a condition: `if (response) ...`. The documentation for the API is available here: - [API Reference overview](https://api.met.no/weatherapi/) - [Locationforecast](https://api.met.no/weatherapi/locationforecast/2.0/) - Used to get the weather forecast - [Sunrise](https://api.met.no/weatherapi/sunrise/2.0/documentation) - used to find sunrise and sunset times Co-authored-by: Veeck <github@veeck.de>
2022-11-08 06:18:47 +01:00
weather.precipitationUnits = units.precipitation_amount;
if (stellarTimesToday) {
weather.sunset = moment(stellarTimesToday.sunset.time);
weather.sunrise = weather.sunset < moment() && stellarTimesTomorrow ? moment(stellarTimesTomorrow.sunrise.time) : moment(stellarTimesToday.sunrise.time);
}
return weather;
},
convertWeatherType(weatherType, weatherTime) {
const weatherHour = moment(weatherTime).format("HH");
const weatherTypes = {
clearsky_day: "day-sunny",
clearsky_night: "night-clear",
clearsky_polartwilight: weatherHour < 14 ? "sunrise" : "sunset",
cloudy: "cloudy",
fair_day: "day-sunny-overcast",
fair_night: "night-alt-partly-cloudy",
fair_polartwilight: "day-sunny-overcast",
fog: "fog",
heavyrain: "rain", // Possibly raindrops or raindrop
heavyrainandthunder: "thunderstorm",
heavyrainshowers_day: "day-rain",
heavyrainshowers_night: "night-alt-rain",
heavyrainshowers_polartwilight: "day-rain",
heavyrainshowersandthunder_day: "day-thunderstorm",
heavyrainshowersandthunder_night: "night-alt-thunderstorm",
heavyrainshowersandthunder_polartwilight: "day-thunderstorm",
heavysleet: "sleet",
heavysleetandthunder: "day-sleet-storm",
heavysleetshowers_day: "day-sleet",
heavysleetshowers_night: "night-alt-sleet",
heavysleetshowers_polartwilight: "day-sleet",
heavysleetshowersandthunder_day: "day-sleet-storm",
heavysleetshowersandthunder_night: "night-alt-sleet-storm",
heavysleetshowersandthunder_polartwilight: "day-sleet-storm",
heavysnow: "snow-wind",
heavysnowandthunder: "day-snow-thunderstorm",
heavysnowshowers_day: "day-snow-wind",
heavysnowshowers_night: "night-alt-snow-wind",
heavysnowshowers_polartwilight: "day-snow-wind",
heavysnowshowersandthunder_day: "day-snow-thunderstorm",
heavysnowshowersandthunder_night: "night-alt-snow-thunderstorm",
heavysnowshowersandthunder_polartwilight: "day-snow-thunderstorm",
lightrain: "rain-mix",
lightrainandthunder: "thunderstorm",
lightrainshowers_day: "day-rain-mix",
lightrainshowers_night: "night-alt-rain-mix",
lightrainshowers_polartwilight: "day-rain-mix",
lightrainshowersandthunder_day: "thunderstorm",
lightrainshowersandthunder_night: "thunderstorm",
lightrainshowersandthunder_polartwilight: "thunderstorm",
lightsleet: "day-sleet",
lightsleetandthunder: "day-sleet-storm",
lightsleetshowers_day: "day-sleet",
lightsleetshowers_night: "night-alt-sleet",
lightsleetshowers_polartwilight: "day-sleet",
lightsnow: "snowflake-cold",
lightsnowandthunder: "day-snow-thunderstorm",
lightsnowshowers_day: "day-snow-wind",
lightsnowshowers_night: "night-alt-snow-wind",
lightsnowshowers_polartwilight: "day-snow-wind",
lightssleetshowersandthunder_day: "day-sleet-storm",
lightssleetshowersandthunder_night: "night-alt-sleet-storm",
lightssleetshowersandthunder_polartwilight: "day-sleet-storm",
lightssnowshowersandthunder_day: "day-snow-thunderstorm",
lightssnowshowersandthunder_night: "night-alt-snow-thunderstorm",
lightssnowshowersandthunder_polartwilight: "day-snow-thunderstorm",
partlycloudy_day: "day-cloudy",
partlycloudy_night: "night-alt-cloudy",
partlycloudy_polartwilight: "day-cloudy",
rain: "rain",
rainandthunder: "thunderstorm",
rainshowers_day: "day-rain",
rainshowers_night: "night-alt-rain",
rainshowers_polartwilight: "day-rain",
rainshowersandthunder_day: "thunderstorm",
rainshowersandthunder_night: "lightning",
rainshowersandthunder_polartwilight: "thunderstorm",
sleet: "sleet",
sleetandthunder: "day-sleet-storm",
sleetshowers_day: "day-sleet",
sleetshowers_night: "night-alt-sleet",
sleetshowers_polartwilight: "day-sleet",
sleetshowersandthunder_day: "day-sleet-storm",
sleetshowersandthunder_night: "night-alt-sleet-storm",
sleetshowersandthunder_polartwilight: "day-sleet-storm",
snow: "snowflake-cold",
snowandthunder: "lightning",
snowshowers_day: "day-snow-wind",
snowshowers_night: "night-alt-snow-wind",
snowshowers_polartwilight: "day-snow-wind",
snowshowersandthunder_day: "day-snow-thunderstorm",
snowshowersandthunder_night: "night-alt-snow-thunderstorm",
snowshowersandthunder_polartwilight: "day-snow-thunderstorm"
};
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
},
getStellarTimesFrom(stellarData, date) {
for (const time of stellarData.location.time) {
if (time.date === date) {
return time;
}
}
return undefined;
},
getForecastForXHoursFrom(weather) {
if (this.config.currentForecastHours === 1) {
if (weather.next_1_hours) {
return weather.next_1_hours;
} else if (weather.next_6_hours) {
return weather.next_6_hours;
} else {
return weather.next_12_hours;
}
} else if (this.config.currentForecastHours === 6) {
if (weather.next_6_hours) {
return weather.next_6_hours;
} else if (weather.next_12_hours) {
return weather.next_12_hours;
} else {
return weather.next_1_hours;
}
} else {
if (weather.next_12_hours) {
return weather.next_12_hours;
} else if (weather.next_6_hours) {
return weather.next_6_hours;
} else {
return weather.next_1_hours;
}
}
},
fetchWeatherHourly() {
this.getWeatherForecast("hourly")
.then((forecast) => {
this.setWeatherHourly(forecast);
this.updateAvailable();
})
.catch((error) => {
Log.error(error);
throw new Error(error);
});
},
async getWeatherForecast(type) {
const getRequests = [this.getWeatherData(), this.getStellarData()];
const [weatherData, stellarData] = await Promise.all(getRequests);
if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) {
Log.error("No weather data available.");
return;
}
if (!stellarData) {
Log.warn("No stellar data available.");
New weather provider: Yr.no (#2948) # Added Yr.no as a weather provider Yr.no is a free Norwegian weather service. The configuration is quite simple: ```js { weatherProvider: "yr", lat: 59.9171, lon: 10.7276, altitude: 30 } ``` The latitude and longitude cannot have more than 4 decimals, but that should be plenty. To quote yr: "There is no need to ask for weather forecasts with nanometer precision!". The altitude should be meters above sea level and defaults to 0. If `type` is set to `current` the symbol can display the next 1, 6 or 12 hours by setting `currentForecastHours` (default is 1). It states in [Getting started-guide](https://developer.yr.no/doc/GettingStarted/) that users of the API should cache the results and use the `Expires`-header to know when to ask for new data. By using the `If-Modified-Since`-header we can avoid downloading the same data over and over again. I chose not to override the `User-Agent`-header set in [`server.js`](https://github.com/MichMich/MagicMirror/blob/a328ce5/js/server.js#L97) even though it does not comply with [the terms of service](https://developer.yr.no/doc/TermsOfService/). It currently works with the default header, and by searching the web for MagicMirror the GitHub-repo should be easy to find without an explicit link. I also had to make some minor changes to `server.js` and `weatherprovider.js` to be able to send and return HTTP headers. To handle the HTTP 304 response without body I chose to return `undefined` so we easily can use the response as a condition: `if (response) ...`. The documentation for the API is available here: - [API Reference overview](https://api.met.no/weatherapi/) - [Locationforecast](https://api.met.no/weatherapi/locationforecast/2.0/) - Used to get the weather forecast - [Sunrise](https://api.met.no/weatherapi/sunrise/2.0/documentation) - used to find sunrise and sunset times Co-authored-by: Veeck <github@veeck.de>
2022-11-08 06:18:47 +01:00
}
let forecasts;
switch (type) {
case "hourly":
forecasts = this.getHourlyForecastFrom(weatherData);
break;
case "daily":
default:
forecasts = this.getDailyForecastFrom(weatherData);
break;
}
const series = [];
for (const forecast of forecasts) {
series.push(this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units));
}
return series;
},
getHourlyForecastFrom(weatherData) {
const series = [];
for (const forecast of weatherData.properties.timeseries) {
forecast.symbol = forecast.data.next_1_hours?.summary?.symbol_code;
forecast.precipitationAmount = forecast.data.next_1_hours?.details?.precipitation_amount;
forecast.precipitationProbability = forecast.data.next_1_hours?.details?.probability_of_precipitation;
New weather provider: Yr.no (#2948) # Added Yr.no as a weather provider Yr.no is a free Norwegian weather service. The configuration is quite simple: ```js { weatherProvider: "yr", lat: 59.9171, lon: 10.7276, altitude: 30 } ``` The latitude and longitude cannot have more than 4 decimals, but that should be plenty. To quote yr: "There is no need to ask for weather forecasts with nanometer precision!". The altitude should be meters above sea level and defaults to 0. If `type` is set to `current` the symbol can display the next 1, 6 or 12 hours by setting `currentForecastHours` (default is 1). It states in [Getting started-guide](https://developer.yr.no/doc/GettingStarted/) that users of the API should cache the results and use the `Expires`-header to know when to ask for new data. By using the `If-Modified-Since`-header we can avoid downloading the same data over and over again. I chose not to override the `User-Agent`-header set in [`server.js`](https://github.com/MichMich/MagicMirror/blob/a328ce5/js/server.js#L97) even though it does not comply with [the terms of service](https://developer.yr.no/doc/TermsOfService/). It currently works with the default header, and by searching the web for MagicMirror the GitHub-repo should be easy to find without an explicit link. I also had to make some minor changes to `server.js` and `weatherprovider.js` to be able to send and return HTTP headers. To handle the HTTP 304 response without body I chose to return `undefined` so we easily can use the response as a condition: `if (response) ...`. The documentation for the API is available here: - [API Reference overview](https://api.met.no/weatherapi/) - [Locationforecast](https://api.met.no/weatherapi/locationforecast/2.0/) - Used to get the weather forecast - [Sunrise](https://api.met.no/weatherapi/sunrise/2.0/documentation) - used to find sunrise and sunset times Co-authored-by: Veeck <github@veeck.de>
2022-11-08 06:18:47 +01:00
forecast.minTemperature = forecast.data.next_1_hours?.details?.air_temperature_min;
forecast.maxTemperature = forecast.data.next_1_hours?.details?.air_temperature_max;
forecast.weatherType = this.convertWeatherType(forecast.symbol, forecast.time);
series.push(forecast);
}
return series;
},
getDailyForecastFrom(weatherData) {
const series = [];
const days = weatherData.properties.timeseries.reduce(function (days, forecast) {
const date = moment(forecast.time).format("YYYY-MM-DD");
days[date] = days[date] || [];
days[date].push(forecast);
return days;
}, Object.create(null));
Object.keys(days).forEach(function (time, index) {
let minTemperature = undefined;
let maxTemperature = undefined;
//Default to first entry
let forecast = days[time][0];
forecast.symbol = forecast.data.next_12_hours?.summary?.symbol_code;
forecast.precipitation = forecast.data.next_12_hours?.details?.precipitation_amount;
//Coming days
let forecastDiffToEight = undefined;
for (const timeseries of days[time]) {
if (!timeseries.data.next_6_hours) continue; //next_6_hours has the most data
if (!minTemperature || timeseries.data.next_6_hours.details.air_temperature_min < minTemperature) minTemperature = timeseries.data.next_6_hours.details.air_temperature_min;
if (!maxTemperature || maxTemperature < timeseries.data.next_6_hours.details.air_temperature_max) maxTemperature = timeseries.data.next_6_hours.details.air_temperature_max;
let closestTime = Math.abs(moment(timeseries.time).local().set({ hour: 8, minute: 0, second: 0, millisecond: 0 }).diff(moment(timeseries.time).local()));
if ((forecastDiffToEight === undefined || closestTime < forecastDiffToEight) && timeseries.data.next_12_hours) {
forecastDiffToEight = closestTime;
forecast = timeseries;
}
}
const forecastXHours = forecast.data.next_12_hours ?? forecast.data.next_6_hours ?? forecast.data.next_1_hours;
if (forecastXHours) {
forecast.symbol = forecastXHours.summary?.symbol_code;
forecast.precipitationAmount = forecastXHours.details?.precipitation_amount ?? forecast.data.next_6_hours?.details?.precipitation_amount; // 6 hours is likely to have precipitation amount even if 12 hours does not
forecast.precipitationProbability = forecastXHours.details?.probability_of_precipitation;
New weather provider: Yr.no (#2948) # Added Yr.no as a weather provider Yr.no is a free Norwegian weather service. The configuration is quite simple: ```js { weatherProvider: "yr", lat: 59.9171, lon: 10.7276, altitude: 30 } ``` The latitude and longitude cannot have more than 4 decimals, but that should be plenty. To quote yr: "There is no need to ask for weather forecasts with nanometer precision!". The altitude should be meters above sea level and defaults to 0. If `type` is set to `current` the symbol can display the next 1, 6 or 12 hours by setting `currentForecastHours` (default is 1). It states in [Getting started-guide](https://developer.yr.no/doc/GettingStarted/) that users of the API should cache the results and use the `Expires`-header to know when to ask for new data. By using the `If-Modified-Since`-header we can avoid downloading the same data over and over again. I chose not to override the `User-Agent`-header set in [`server.js`](https://github.com/MichMich/MagicMirror/blob/a328ce5/js/server.js#L97) even though it does not comply with [the terms of service](https://developer.yr.no/doc/TermsOfService/). It currently works with the default header, and by searching the web for MagicMirror the GitHub-repo should be easy to find without an explicit link. I also had to make some minor changes to `server.js` and `weatherprovider.js` to be able to send and return HTTP headers. To handle the HTTP 304 response without body I chose to return `undefined` so we easily can use the response as a condition: `if (response) ...`. The documentation for the API is available here: - [API Reference overview](https://api.met.no/weatherapi/) - [Locationforecast](https://api.met.no/weatherapi/locationforecast/2.0/) - Used to get the weather forecast - [Sunrise](https://api.met.no/weatherapi/sunrise/2.0/documentation) - used to find sunrise and sunset times Co-authored-by: Veeck <github@veeck.de>
2022-11-08 06:18:47 +01:00
forecast.minTemperature = minTemperature;
forecast.maxTemperature = maxTemperature;
series.push(forecast);
}
});
for (const forecast of series) {
forecast.weatherType = this.convertWeatherType(forecast.symbol, forecast.time);
}
return series;
},
fetchWeatherForecast() {
this.getWeatherForecast("daily")
.then((forecast) => {
this.setWeatherForecast(forecast);
this.updateAvailable();
})
.catch((error) => {
Log.error(error);
throw new Error(error);
});
}
});