mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
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>
This commit is contained in:
parent
b9b7d2c95d
commit
bd0b3c00ad
@ -19,6 +19,7 @@ Special thanks to: @rejas, @sdetweil, @MagMar94
|
|||||||
- Added css class names "today" and "tomorrow" for default calendar
|
- Added css class names "today" and "tomorrow" for default calendar
|
||||||
- Added Collaboration.md
|
- Added Collaboration.md
|
||||||
- Added new github action for dependency review (#2862)
|
- Added new github action for dependency review (#2862)
|
||||||
|
- Added Yr as a weather provider
|
||||||
- Added config options "ignoreXOriginHeader" and "ignoreContentSecurityPolicy"
|
- Added config options "ignoreXOriginHeader" and "ignoreContentSecurityPolicy"
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
147
modules/default/utils.js
Normal file
147
modules/default/utils.js
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* A function to make HTTP requests via the server to avoid CORS-errors.
|
||||||
|
*
|
||||||
|
* @param {string} url the url to fetch from
|
||||||
|
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
|
||||||
|
* @param {boolean} useCorsProxy A flag to indicate
|
||||||
|
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||||
|
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||||
|
* @returns {Promise} resolved when the fetch is done. The response headers is placed in a headers-property (provided the response does not allready contain a headers-property).
|
||||||
|
*/
|
||||||
|
async function performWebRequest(url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined) {
|
||||||
|
const request = {};
|
||||||
|
if (useCorsProxy) {
|
||||||
|
url = getCorsUrl(url, requestHeaders, expectedResponseHeaders);
|
||||||
|
} else {
|
||||||
|
request.headers = getHeadersToSend(requestHeaders);
|
||||||
|
}
|
||||||
|
const response = await fetch(url, request);
|
||||||
|
const data = await response.text();
|
||||||
|
|
||||||
|
if (type === "xml") {
|
||||||
|
return new DOMParser().parseFromString(data, "text/html");
|
||||||
|
} else {
|
||||||
|
if (!data || !data.length > 0) return undefined;
|
||||||
|
|
||||||
|
const dataResponse = JSON.parse(data);
|
||||||
|
if (!dataResponse.headers) {
|
||||||
|
dataResponse.headers = getHeadersFromResponse(expectedResponseHeaders, response);
|
||||||
|
}
|
||||||
|
return dataResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a URL that will be used when calling the CORS-method on the server.
|
||||||
|
*
|
||||||
|
* @param {string} url the url to fetch from
|
||||||
|
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||||
|
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||||
|
* @returns {string} to be used as URL when calling CORS-method on server.
|
||||||
|
*/
|
||||||
|
const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders) {
|
||||||
|
if (!url || url.length < 1) {
|
||||||
|
throw new Error(`Invalid URL: ${url}`);
|
||||||
|
} else {
|
||||||
|
let corsUrl = `${location.protocol}//${location.host}/cors?`;
|
||||||
|
|
||||||
|
const requestHeaderString = getRequestHeaderString(requestHeaders);
|
||||||
|
if (requestHeaderString) corsUrl = `${corsUrl}sendheaders=${requestHeaderString}`;
|
||||||
|
|
||||||
|
const expectedResponseHeadersString = getExpectedResponseHeadersString(expectedResponseHeaders);
|
||||||
|
if (requestHeaderString && expectedResponseHeadersString) {
|
||||||
|
corsUrl = `${corsUrl}&expectedheaders=${expectedResponseHeadersString}`;
|
||||||
|
} else if (expectedResponseHeadersString) {
|
||||||
|
corsUrl = `${corsUrl}expectedheaders=${expectedResponseHeadersString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestHeaderString || expectedResponseHeadersString) {
|
||||||
|
return `${corsUrl}&url=${url}`;
|
||||||
|
}
|
||||||
|
return `${corsUrl}url=${url}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the part of the CORS URL that represents the HTTP headers to send.
|
||||||
|
*
|
||||||
|
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||||
|
* @returns {string} to be used as request-headers component in CORS URL.
|
||||||
|
*/
|
||||||
|
const getRequestHeaderString = function (requestHeaders) {
|
||||||
|
let requestHeaderString = "";
|
||||||
|
if (requestHeaders) {
|
||||||
|
for (const header of requestHeaders) {
|
||||||
|
if (requestHeaderString.length === 0) {
|
||||||
|
requestHeaderString = `${header.name}:${encodeURIComponent(header.value)}`;
|
||||||
|
} else {
|
||||||
|
requestHeaderString = `${requestHeaderString},${header.name}:${encodeURIComponent(header.value)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requestHeaderString;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets headers and values to attatch to the web request.
|
||||||
|
*
|
||||||
|
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||||
|
* @returns {object} An object specifying name and value of the headers.
|
||||||
|
*/
|
||||||
|
const getHeadersToSend = (requestHeaders) => {
|
||||||
|
const headersToSend = {};
|
||||||
|
if (requestHeaders) {
|
||||||
|
for (const header of requestHeaders) {
|
||||||
|
headersToSend[header.name] = header.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headersToSend;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the part of the CORS URL that represents the expected HTTP headers to recieve.
|
||||||
|
*
|
||||||
|
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||||
|
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
|
||||||
|
*/
|
||||||
|
const getExpectedResponseHeadersString = function (expectedResponseHeaders) {
|
||||||
|
let expectedResponseHeadersString = "";
|
||||||
|
if (expectedResponseHeaders) {
|
||||||
|
for (const header of expectedResponseHeaders) {
|
||||||
|
if (expectedResponseHeadersString.length === 0) {
|
||||||
|
expectedResponseHeadersString = `${header}`;
|
||||||
|
} else {
|
||||||
|
expectedResponseHeadersString = `${expectedResponseHeadersString},${header}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expectedResponseHeaders;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the values for the expected headers from the response.
|
||||||
|
*
|
||||||
|
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||||
|
* @param {Response} response the HTTP response
|
||||||
|
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
|
||||||
|
*/
|
||||||
|
const getHeadersFromResponse = (expectedResponseHeaders, response) => {
|
||||||
|
const responseHeaders = [];
|
||||||
|
|
||||||
|
if (expectedResponseHeaders) {
|
||||||
|
for (const header of expectedResponseHeaders) {
|
||||||
|
const headerValue = response.headers.get(header);
|
||||||
|
responseHeaders.push({ name: header, value: headerValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseHeaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof module !== "undefined")
|
||||||
|
module.exports = {
|
||||||
|
performWebRequest
|
||||||
|
};
|
626
modules/default/weather/providers/yr.js
Normal file
626
modules/default/weather/providers/yr.js
Normal file
@ -0,0 +1,626 @@
|
|||||||
|
/* global WeatherProvider, WeatherObject */
|
||||||
|
|
||||||
|
/* MagicMirror²
|
||||||
|
* Module: Weather
|
||||||
|
* Provider: Yr.no
|
||||||
|
*
|
||||||
|
* By Magnus Marthinsen
|
||||||
|
* MIT Licensed
|
||||||
|
*
|
||||||
|
* This class is a provider for Yr.no, a norwegian sweather service.
|
||||||
|
*
|
||||||
|
* 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 stelar data available.");
|
||||||
|
}
|
||||||
|
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.precipitation = forecastXHours.details?.precipitation_amount;
|
||||||
|
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(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
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.windDirection = (forecast.data.instant.details.wind_from_direction + 180) % 360;
|
||||||
|
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.precipitation = forecast.precipitation;
|
||||||
|
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 stelar data available.");
|
||||||
|
}
|
||||||
|
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.precipitation = forecast.data.next_1_hours?.details?.precipitation_amount;
|
||||||
|
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.precipitation = forecastXHours.details?.precipitation_amount;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -58,7 +58,7 @@ Module.register("weather", {
|
|||||||
|
|
||||||
// Return the scripts that are necessary for the weather module.
|
// Return the scripts that are necessary for the weather module.
|
||||||
getScripts: function () {
|
getScripts: function () {
|
||||||
return ["moment.js", "weatherutils.js", "weatherprovider.js", "weatherobject.js", "suncalc.js", this.file("providers/" + this.config.weatherProvider.toLowerCase() + ".js")];
|
return ["moment.js", this.file("../utils.js"), "weatherutils.js", "weatherprovider.js", "weatherobject.js", "suncalc.js", this.file("providers/" + this.config.weatherProvider.toLowerCase() + ".js")];
|
||||||
},
|
},
|
||||||
|
|
||||||
// Override getHeader method.
|
// Override getHeader method.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
/* global Class */
|
/* global Class, performWebRequest */
|
||||||
|
|
||||||
/* MagicMirror²
|
/* MagicMirror²
|
||||||
* Module: Weather
|
* Module: Weather
|
||||||
@ -111,36 +111,23 @@ const WeatherProvider = Class.extend({
|
|||||||
this.delegate.updateAvailable(this);
|
this.delegate.updateAvailable(this);
|
||||||
},
|
},
|
||||||
|
|
||||||
getCorsUrl: function () {
|
|
||||||
if (this.config.mockData || typeof this.config.useCorsProxy === "undefined" || !this.config.useCorsProxy) {
|
|
||||||
return "";
|
|
||||||
} else {
|
|
||||||
return location.protocol + "//" + location.host + "/cors?url=";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A convenience function to make requests.
|
* A convenience function to make requests.
|
||||||
*
|
*
|
||||||
* @param {string} url the url to fetch from
|
* @param {string} url the url to fetch from
|
||||||
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
|
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
|
||||||
|
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||||
|
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||||
* @returns {Promise} resolved when the fetch is done
|
* @returns {Promise} resolved when the fetch is done
|
||||||
*/
|
*/
|
||||||
fetchData: async function (url, type = "json") {
|
fetchData: async function (url, type = "json", requestHeaders = undefined, expectedResponseHeaders = undefined) {
|
||||||
url = this.getCorsUrl() + url;
|
|
||||||
const mockData = this.config.mockData;
|
const mockData = this.config.mockData;
|
||||||
if (mockData) {
|
if (mockData) {
|
||||||
const data = mockData.substring(1, mockData.length - 1);
|
const data = mockData.substring(1, mockData.length - 1);
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
} else {
|
|
||||||
const response = await fetch(url);
|
|
||||||
const data = await response.text();
|
|
||||||
if (type === "xml") {
|
|
||||||
return new DOMParser().parseFromString(data, "text/html");
|
|
||||||
} else {
|
|
||||||
return JSON.parse(data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const useCorsProxy = typeof this.config.useCorsProxy !== "undefined" && this.config.useCorsProxy;
|
||||||
|
return performWebRequest(url, type, useCorsProxy, requestHeaders, expectedResponseHeaders);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
111
tests/unit/modules/default/utils_spec.js
Normal file
111
tests/unit/modules/default/utils_spec.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
const { performWebRequest } = require("../../../../modules/default/utils.js");
|
||||||
|
const nodeVersion = process.version.match(/^v(\d+)\.*/)[1];
|
||||||
|
|
||||||
|
describe("Utils tests", () => {
|
||||||
|
describe("The performWebRequest-method", () => {
|
||||||
|
if (nodeVersion > 18) {
|
||||||
|
const locationHost = "localhost:8080";
|
||||||
|
const locationProtocol = "http";
|
||||||
|
|
||||||
|
let fetchResponse;
|
||||||
|
let fetchMock;
|
||||||
|
let url;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchResponse = new Response();
|
||||||
|
global.fetch = jest.fn(() => Promise.resolve(fetchResponse));
|
||||||
|
fetchMock = global.fetch;
|
||||||
|
|
||||||
|
url = "www.test.com";
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("When using cors proxy", () => {
|
||||||
|
Object.defineProperty(global, "location", {
|
||||||
|
value: {
|
||||||
|
host: locationHost,
|
||||||
|
protocol: locationProtocol
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Calls correct URL once", async () => {
|
||||||
|
const urlToCall = "http://www.test.com/path?param1=value1";
|
||||||
|
url = urlToCall;
|
||||||
|
|
||||||
|
await performWebRequest(url, "json", true);
|
||||||
|
|
||||||
|
expect(fetchMock.mock.calls.length).toBe(1);
|
||||||
|
expect(fetchMock.mock.calls[0][0]).toBe(`${locationProtocol}//${locationHost}/cors?url=${urlToCall}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Sends correct headers", async () => {
|
||||||
|
const urlToCall = "http://www.test.com/path?param1=value1";
|
||||||
|
url = urlToCall;
|
||||||
|
const headers = [
|
||||||
|
{ name: "header1", value: "value1" },
|
||||||
|
{ name: "header2", value: "value2" }
|
||||||
|
];
|
||||||
|
|
||||||
|
await performWebRequest(url, "json", true, headers);
|
||||||
|
|
||||||
|
expect(fetchMock.mock.calls.length).toBe(1);
|
||||||
|
expect(fetchMock.mock.calls[0][0]).toBe(`${locationProtocol}//${locationHost}/cors?sendheaders=header1:value1,header2:value2&url=${urlToCall}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("When not using cors proxy", () => {
|
||||||
|
test("Calls correct URL once", async () => {
|
||||||
|
const urlToCall = "http://www.test.com/path?param1=value1";
|
||||||
|
url = urlToCall;
|
||||||
|
|
||||||
|
await performWebRequest(url);
|
||||||
|
|
||||||
|
expect(fetchMock.mock.calls.length).toBe(1);
|
||||||
|
expect(fetchMock.mock.calls[0][0]).toBe(urlToCall);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Sends correct headers", async () => {
|
||||||
|
const urlToCall = "http://www.test.com/path?param1=value1";
|
||||||
|
url = urlToCall;
|
||||||
|
const headers = [
|
||||||
|
{ name: "header1", value: "value1" },
|
||||||
|
{ name: "header2", value: "value2" }
|
||||||
|
];
|
||||||
|
|
||||||
|
await performWebRequest(url, "json", false, headers);
|
||||||
|
|
||||||
|
const expectedHeaders = { headers: { header1: "value1", header2: "value2" } };
|
||||||
|
expect(fetchMock.mock.calls.length).toBe(1);
|
||||||
|
expect(fetchMock.mock.calls[0][1]).toStrictEqual(expectedHeaders);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("When receiving json format", () => {
|
||||||
|
test("Returns undefined when no data is received", async () => {
|
||||||
|
const response = await performWebRequest(url);
|
||||||
|
|
||||||
|
expect(response).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Returns object when data is received", async () => {
|
||||||
|
fetchResponse = new Response('{"body": "some content"}');
|
||||||
|
|
||||||
|
const response = await performWebRequest(url);
|
||||||
|
|
||||||
|
expect(response.body).toBe("some content");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Returns expected headers when data is received", async () => {
|
||||||
|
fetchResponse = new Response('{"body": "some content"}', { headers: { header1: "value1", header2: "value2" } });
|
||||||
|
|
||||||
|
const response = await performWebRequest(url, "json", false, undefined, ["header1"]);
|
||||||
|
|
||||||
|
expect(response.headers.length).toBe(1);
|
||||||
|
expect(response.headers[0].name).toBe("header1");
|
||||||
|
expect(response.headers[0].value).toBe("value1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
test("Always ok, need one test", () => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user