mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 03:39:55 +00:00
## [2.25.0] - 2023-10-01 Thanks to: @bugsounet, @dgoth, @dependabot, @kenzal, @Knapoc, @KristjanESPERANTO, @martingron, @NolanKingdon, @Paranoid93, @TeddyStarinvest and @Ybbet. Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome! > ⚠️ This release needs nodejs version >= `v18`, older releases have reached end of life and will not work! ### Added - Added UV Index support to OpenWeatherMap - Added 'hideDuplicates' flag to the calendar module - Added `allowOverrideNotification` to weather module to enable sending current weather objects with the `CURRENT_WEATHER_OVERRIDE` notification to supplement/replace the current weather displayed - Added optional AnimateCSS animate for `hide()`, `show()`, `updateDom()` - Added AnimateIn and animateOut in module config definition - Apply AnimateIn rules on the first start - Added automatic client page reload when server was restarted by setting `reloadAfterServerRestart: true` in `config.js`, per default `false` (#3105) - Added eventClass option for customEvents on the default calendar - Added AnimateCSS integration in tests suite (#3206) - Added npm dependabot [Reserved to developer] (#3210) - Added improved logging for calendar (#3110) ### Removed - **Breaking Change**: Removed `digest` authentication method from calendar module (which was already broken since release `2.15.0`) ### Updated - Update roboto fonts to version v5 - Update issue template - Update dev/dependencies incl. electron to v26 - Replace pretty-quick by lint-staged (<https://github.com/azz/pretty-quick/issues/164>) - Update engine node >=18. v16 reached it's end of life. (#3170) - Update typescript definition for modules - Cleaned up nunjuck templates - Replace `node-fetch` with internal fetch (#2649) and remove `digest-fetch` - Update the French translation according to the English file. - Update dependabot incl. vendor/fonts (monthly check) - Renew `package-lock.json` for release ### Fixed - Fix engine check on npm install (#3135) - Fix undefined formatTime method in clock module (#3143) - Fix clientonly startup fails after async added (#3151) - Fix electron width/heigth when using xrandr under bullseye - Fix time issue with certain recurring events in calendar module - Fix ipWhiteList test (#3179) - Fix newsfeed: Convert HTML entities, codes and tag in description (#3191) - Respect width/height (no fullscreen) if set in electronOptions (together with `fullscreen: false`) in `config.js` (#3174) - Fix: AnimateCSS merge hide() and show() animated css class when we do multiple call - Fix `Uncaught SyntaxError: Identifier 'getCorsUrl' has already been declared (at utils.js:1:1)` when using `clock` and `weather` module (#3204) - Fix overriding `config.js` when running tests (#3201) - Fix issue in weathergov provider with probability of precipitation not showing up on hourly or daily forecast --------- Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Karsten Hassel <hassel@gmx.de> Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: veeck <michael@veeck.de> Co-authored-by: dWoolridge <dwoolridge@charter.net> Co-authored-by: Johan <jojjepersson@yahoo.se> Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com> Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com> Co-authored-by: buxxi <buxxi@omfilm.net> Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com> Co-authored-by: Dave Child <dave@addedbytes.com> Co-authored-by: grenagit <46225780+grenagit@users.noreply.github.com> Co-authored-by: Grena <grena@grenabox.fr> Co-authored-by: Magnus Marthinsen <magmar@online.no> Co-authored-by: Patrick <psieg@users.noreply.github.com> Co-authored-by: Piotr Rajnisz <56397164+rajniszp@users.noreply.github.com> Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com> Co-authored-by: CarJem Generations (Carter Wallace) <cwallacecs@gmail.com> Co-authored-by: Nicholas Fogal <nfogal.misc@gmail.com> Co-authored-by: JakeBinney <126349119+JakeBinney@users.noreply.github.com> Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com> Co-authored-by: Oscar Björkman <17575446+oscarb@users.noreply.github.com> Co-authored-by: Ismar Slomic <ismar@slomic.no> Co-authored-by: Jørgen Veum-Wahlberg <jorgen.wahlberg@amedia.no> Co-authored-by: Eddie Hung <6740044+eddiehung@users.noreply.github.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: bugsounet <bugsounet@bugsounet.fr> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Knapoc <Knapoc@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: veeck <michael.veeck@nebenan.de> Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com> Co-authored-by: NolanKingdon <27908974+NolanKingdon@users.noreply.github.com> Co-authored-by: J. Kenzal Hunter <kenzal.hunter@gmail.com> Co-authored-by: Teddy <teddy.payet@gmail.com> Co-authored-by: TeddyStarinvest <teddy.payet@starinvest.com> Co-authored-by: martingron <61826403+martingron@users.noreply.github.com> Co-authored-by: dgoth <132394363+dgoth@users.noreply.github.com>
452 lines
14 KiB
JavaScript
452 lines
14 KiB
JavaScript
/* global WeatherProvider, WeatherObject */
|
|
|
|
/* MagicMirror²
|
|
* Module: Weather
|
|
*
|
|
* By Michael Teeuw https://michaelteeuw.nl
|
|
* MIT Licensed.
|
|
*
|
|
* This class is the blueprint for a weather provider.
|
|
*/
|
|
WeatherProvider.register("openweathermap", {
|
|
// Set the name of the provider.
|
|
// This isn't strictly necessary, since it will fallback to the provider identifier
|
|
// But for debugging (and future alerts) it would be nice to have the real name.
|
|
providerName: "OpenWeatherMap",
|
|
|
|
// Set the default config properties that is specific to this provider
|
|
defaults: {
|
|
apiVersion: "2.5",
|
|
apiBase: "https://api.openweathermap.org/data/",
|
|
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
|
|
locationID: false,
|
|
location: false,
|
|
lat: 0, // the onecall endpoint needs lat / lon values, it doesn't support the locationId
|
|
lon: 0,
|
|
apiKey: ""
|
|
},
|
|
|
|
// Overwrite the fetchCurrentWeather method.
|
|
fetchCurrentWeather() {
|
|
this.fetchData(this.getUrl())
|
|
.then((data) => {
|
|
let currentWeather;
|
|
if (this.config.weatherEndpoint === "/onecall") {
|
|
currentWeather = this.generateWeatherObjectsFromOnecall(data).current;
|
|
this.setFetchedLocation(`${data.timezone}`);
|
|
} else {
|
|
currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
|
}
|
|
this.setCurrentWeather(currentWeather);
|
|
})
|
|
.catch(function (request) {
|
|
Log.error("Could not load data ... ", request);
|
|
})
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
// Overwrite the fetchWeatherForecast method.
|
|
fetchWeatherForecast() {
|
|
this.fetchData(this.getUrl())
|
|
.then((data) => {
|
|
let forecast;
|
|
let location;
|
|
if (this.config.weatherEndpoint === "/onecall") {
|
|
forecast = this.generateWeatherObjectsFromOnecall(data).days;
|
|
location = `${data.timezone}`;
|
|
} else {
|
|
forecast = this.generateWeatherObjectsFromForecast(data.list);
|
|
location = `${data.city.name}, ${data.city.country}`;
|
|
}
|
|
this.setWeatherForecast(forecast);
|
|
this.setFetchedLocation(location);
|
|
})
|
|
.catch(function (request) {
|
|
Log.error("Could not load data ... ", request);
|
|
})
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
// Overwrite the fetchWeatherHourly method.
|
|
fetchWeatherHourly() {
|
|
this.fetchData(this.getUrl())
|
|
.then((data) => {
|
|
if (!data) {
|
|
// Did not receive usable new data.
|
|
// Maybe this needs a better check?
|
|
return;
|
|
}
|
|
|
|
this.setFetchedLocation(`(${data.lat},${data.lon})`);
|
|
|
|
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
|
this.setWeatherHourly(weatherData.hours);
|
|
})
|
|
.catch(function (request) {
|
|
Log.error("Could not load data ... ", request);
|
|
})
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
/**
|
|
* Overrides method for setting config to check if endpoint is correct for hourly
|
|
* @param {object} config The configuration object
|
|
*/
|
|
setConfig(config) {
|
|
this.config = config;
|
|
if (!this.config.weatherEndpoint) {
|
|
switch (this.config.type) {
|
|
case "hourly":
|
|
this.config.weatherEndpoint = "/onecall";
|
|
break;
|
|
case "daily":
|
|
case "forecast":
|
|
this.config.weatherEndpoint = "/forecast";
|
|
break;
|
|
case "current":
|
|
this.config.weatherEndpoint = "/weather";
|
|
break;
|
|
default:
|
|
Log.error("weatherEndpoint not configured and could not resolve it based on type");
|
|
}
|
|
}
|
|
},
|
|
|
|
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
|
|
/*
|
|
* Gets the complete url for the request
|
|
*/
|
|
getUrl() {
|
|
return this.config.apiBase + this.config.apiVersion + this.config.weatherEndpoint + this.getParams();
|
|
},
|
|
|
|
/*
|
|
* Generate a WeatherObject based on currentWeatherInformation
|
|
*/
|
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
|
const currentWeather = new WeatherObject();
|
|
|
|
currentWeather.date = moment.unix(currentWeatherData.dt);
|
|
currentWeather.humidity = currentWeatherData.main.humidity;
|
|
currentWeather.temperature = currentWeatherData.main.temp;
|
|
currentWeather.feelsLikeTemp = currentWeatherData.main.feels_like;
|
|
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
|
currentWeather.windFromDirection = currentWeatherData.wind.deg;
|
|
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
|
currentWeather.sunrise = moment.unix(currentWeatherData.sys.sunrise);
|
|
currentWeather.sunset = moment.unix(currentWeatherData.sys.sunset);
|
|
|
|
return currentWeather;
|
|
},
|
|
|
|
/*
|
|
* Generate WeatherObjects based on forecast information
|
|
*/
|
|
generateWeatherObjectsFromForecast(forecasts) {
|
|
if (this.config.weatherEndpoint === "/forecast") {
|
|
return this.generateForecastHourly(forecasts);
|
|
} else if (this.config.weatherEndpoint === "/forecast/daily") {
|
|
return this.generateForecastDaily(forecasts);
|
|
}
|
|
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
|
return [new WeatherObject()];
|
|
},
|
|
|
|
/*
|
|
* Generate WeatherObjects based on One Call forecast information
|
|
*/
|
|
generateWeatherObjectsFromOnecall(data) {
|
|
if (this.config.weatherEndpoint === "/onecall") {
|
|
return this.fetchOnecall(data);
|
|
}
|
|
// if weatherEndpoint does not match onecall, what should be returned?
|
|
return { current: new WeatherObject(), hours: [], days: [] };
|
|
},
|
|
|
|
/*
|
|
* Generate forecast information for 3-hourly forecast (available for free
|
|
* subscription).
|
|
*/
|
|
generateForecastHourly(forecasts) {
|
|
// initial variable declaration
|
|
const days = [];
|
|
// variables for temperature range and rain
|
|
let minTemp = [];
|
|
let maxTemp = [];
|
|
let rain = 0;
|
|
let snow = 0;
|
|
// variable for date
|
|
let date = "";
|
|
let weather = new WeatherObject();
|
|
|
|
for (const forecast of forecasts) {
|
|
if (date !== moment.unix(forecast.dt).format("YYYY-MM-DD")) {
|
|
// calculate minimum/maximum temperature, specify rain amount
|
|
weather.minTemperature = Math.min.apply(null, minTemp);
|
|
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
|
weather.rain = rain;
|
|
weather.snow = snow;
|
|
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
|
// push weather information to days array
|
|
days.push(weather);
|
|
// create new weather-object
|
|
weather = new WeatherObject();
|
|
|
|
minTemp = [];
|
|
maxTemp = [];
|
|
rain = 0;
|
|
snow = 0;
|
|
|
|
// set new date
|
|
date = moment.unix(forecast.dt).format("YYYY-MM-DD");
|
|
|
|
// specify date
|
|
weather.date = moment.unix(forecast.dt);
|
|
|
|
// If the first value of today is later than 17:00, we have an icon at least!
|
|
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
|
}
|
|
|
|
if (moment.unix(forecast.dt).format("H") >= 8 && moment.unix(forecast.dt).format("H") <= 17) {
|
|
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
|
}
|
|
|
|
// the same day as before
|
|
// add values from forecast to corresponding variables
|
|
minTemp.push(forecast.main.temp_min);
|
|
maxTemp.push(forecast.main.temp_max);
|
|
|
|
if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain["3h"])) {
|
|
rain += forecast.rain["3h"];
|
|
}
|
|
|
|
if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow["3h"])) {
|
|
snow += forecast.snow["3h"];
|
|
}
|
|
}
|
|
|
|
// last day
|
|
// calculate minimum/maximum temperature, specify rain amount
|
|
weather.minTemperature = Math.min.apply(null, minTemp);
|
|
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
|
weather.rain = rain;
|
|
weather.snow = snow;
|
|
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
|
// push weather information to days array
|
|
days.push(weather);
|
|
return days.slice(1);
|
|
},
|
|
|
|
/*
|
|
* Generate forecast information for daily forecast (available for paid
|
|
* subscription or old apiKey).
|
|
*/
|
|
generateForecastDaily(forecasts) {
|
|
// initial variable declaration
|
|
const days = [];
|
|
|
|
for (const forecast of forecasts) {
|
|
const weather = new WeatherObject();
|
|
|
|
weather.date = moment.unix(forecast.dt);
|
|
weather.minTemperature = forecast.temp.min;
|
|
weather.maxTemperature = forecast.temp.max;
|
|
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
|
weather.rain = 0;
|
|
weather.snow = 0;
|
|
|
|
// forecast.rain not available if amount is zero
|
|
// The API always returns in millimeters
|
|
if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain)) {
|
|
weather.rain = forecast.rain;
|
|
}
|
|
|
|
// forecast.snow not available if amount is zero
|
|
// The API always returns in millimeters
|
|
if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow)) {
|
|
weather.snow = forecast.snow;
|
|
}
|
|
|
|
weather.precipitationAmount = weather.rain + weather.snow;
|
|
weather.precipitationProbability = forecast.pop ? forecast.pop * 100 : undefined;
|
|
|
|
days.push(weather);
|
|
}
|
|
|
|
return days;
|
|
},
|
|
|
|
/*
|
|
* Fetch One Call forecast information (available for free subscription).
|
|
* Factors in timezone offsets.
|
|
* Minutely forecasts are excluded for the moment, see getParams().
|
|
*/
|
|
fetchOnecall(data) {
|
|
let precip = false;
|
|
|
|
// get current weather, if requested
|
|
const current = new WeatherObject();
|
|
if (data.hasOwnProperty("current")) {
|
|
current.date = moment.unix(data.current.dt).utcOffset(data.timezone_offset / 60);
|
|
current.windSpeed = data.current.wind_speed;
|
|
current.windFromDirection = data.current.wind_deg;
|
|
current.sunrise = moment.unix(data.current.sunrise).utcOffset(data.timezone_offset / 60);
|
|
current.sunset = moment.unix(data.current.sunset).utcOffset(data.timezone_offset / 60);
|
|
current.temperature = data.current.temp;
|
|
current.weatherType = this.convertWeatherType(data.current.weather[0].icon);
|
|
current.humidity = data.current.humidity;
|
|
current.uv_index = data.current.uvi;
|
|
if (data.current.hasOwnProperty("rain") && !isNaN(data.current["rain"]["1h"])) {
|
|
current.rain = data.current["rain"]["1h"];
|
|
precip = true;
|
|
}
|
|
if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) {
|
|
current.snow = data.current["snow"]["1h"];
|
|
precip = true;
|
|
}
|
|
if (precip) {
|
|
current.precipitationAmount = (current.rain ?? 0) + (current.snow ?? 0);
|
|
}
|
|
current.feelsLikeTemp = data.current.feels_like;
|
|
}
|
|
|
|
let weather = new WeatherObject();
|
|
|
|
// get hourly weather, if requested
|
|
const hours = [];
|
|
if (data.hasOwnProperty("hourly")) {
|
|
for (const hour of data.hourly) {
|
|
weather.date = moment.unix(hour.dt).utcOffset(data.timezone_offset / 60);
|
|
weather.temperature = hour.temp;
|
|
weather.feelsLikeTemp = hour.feels_like;
|
|
weather.humidity = hour.humidity;
|
|
weather.windSpeed = hour.wind_speed;
|
|
weather.windFromDirection = hour.wind_deg;
|
|
weather.weatherType = this.convertWeatherType(hour.weather[0].icon);
|
|
weather.precipitationProbability = hour.pop ? hour.pop * 100 : undefined;
|
|
weather.uv_index = hour.uvi;
|
|
precip = false;
|
|
if (hour.hasOwnProperty("rain") && !isNaN(hour.rain["1h"])) {
|
|
weather.rain = hour.rain["1h"];
|
|
precip = true;
|
|
}
|
|
if (hour.hasOwnProperty("snow") && !isNaN(hour.snow["1h"])) {
|
|
weather.snow = hour.snow["1h"];
|
|
precip = true;
|
|
}
|
|
if (precip) {
|
|
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
|
}
|
|
|
|
hours.push(weather);
|
|
weather = new WeatherObject();
|
|
}
|
|
}
|
|
|
|
// get daily weather, if requested
|
|
const days = [];
|
|
if (data.hasOwnProperty("daily")) {
|
|
for (const day of data.daily) {
|
|
weather.date = moment.unix(day.dt).utcOffset(data.timezone_offset / 60);
|
|
weather.sunrise = moment.unix(day.sunrise).utcOffset(data.timezone_offset / 60);
|
|
weather.sunset = moment.unix(day.sunset).utcOffset(data.timezone_offset / 60);
|
|
weather.minTemperature = day.temp.min;
|
|
weather.maxTemperature = day.temp.max;
|
|
weather.humidity = day.humidity;
|
|
weather.windSpeed = day.wind_speed;
|
|
weather.windFromDirection = day.wind_deg;
|
|
weather.weatherType = this.convertWeatherType(day.weather[0].icon);
|
|
weather.precipitationProbability = day.pop ? day.pop * 100 : undefined;
|
|
weather.uv_index = day.uvi;
|
|
precip = false;
|
|
if (!isNaN(day.rain)) {
|
|
weather.rain = day.rain;
|
|
precip = true;
|
|
}
|
|
if (!isNaN(day.snow)) {
|
|
weather.snow = day.snow;
|
|
precip = true;
|
|
}
|
|
if (precip) {
|
|
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
|
}
|
|
|
|
days.push(weather);
|
|
weather = new WeatherObject();
|
|
}
|
|
}
|
|
|
|
return { current: current, hours: hours, days: days };
|
|
},
|
|
|
|
/*
|
|
* Convert the OpenWeatherMap icons to a more usable name.
|
|
*/
|
|
convertWeatherType(weatherType) {
|
|
const weatherTypes = {
|
|
"01d": "day-sunny",
|
|
"02d": "day-cloudy",
|
|
"03d": "cloudy",
|
|
"04d": "cloudy-windy",
|
|
"09d": "showers",
|
|
"10d": "rain",
|
|
"11d": "thunderstorm",
|
|
"13d": "snow",
|
|
"50d": "fog",
|
|
"01n": "night-clear",
|
|
"02n": "night-cloudy",
|
|
"03n": "night-cloudy",
|
|
"04n": "night-cloudy",
|
|
"09n": "night-showers",
|
|
"10n": "night-rain",
|
|
"11n": "night-thunderstorm",
|
|
"13n": "night-snow",
|
|
"50n": "night-alt-cloudy-windy"
|
|
};
|
|
|
|
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
|
},
|
|
|
|
/* getParams(compliments)
|
|
* Generates an url with api parameters based on the config.
|
|
*
|
|
* return String - URL params.
|
|
*/
|
|
getParams() {
|
|
let params = "?";
|
|
if (this.config.weatherEndpoint === "/onecall") {
|
|
params += `lat=${this.config.lat}`;
|
|
params += `&lon=${this.config.lon}`;
|
|
if (this.config.type === "current") {
|
|
params += "&exclude=minutely,hourly,daily";
|
|
} else if (this.config.type === "hourly") {
|
|
params += "&exclude=current,minutely,daily";
|
|
} else if (this.config.type === "daily" || this.config.type === "forecast") {
|
|
params += "&exclude=current,minutely,hourly";
|
|
} else {
|
|
params += "&exclude=minutely";
|
|
}
|
|
} else if (this.config.lat && this.config.lon) {
|
|
params += `lat=${this.config.lat}&lon=${this.config.lon}`;
|
|
} else if (this.config.locationID) {
|
|
params += `id=${this.config.locationID}`;
|
|
} else if (this.config.location) {
|
|
params += `q=${this.config.location}`;
|
|
} else if (this.firstEvent && this.firstEvent.geo) {
|
|
params += `lat=${this.firstEvent.geo.lat}&lon=${this.firstEvent.geo.lon}`;
|
|
} else if (this.firstEvent && this.firstEvent.location) {
|
|
params += `q=${this.firstEvent.location}`;
|
|
} else {
|
|
// TODO hide doesnt exist!
|
|
this.hide(this.config.animationSpeed, { lockString: this.identifier });
|
|
return;
|
|
}
|
|
|
|
params += "&units=metric"; // WeatherProviders should use metric internally and use the units only for when displaying data
|
|
params += `&lang=${this.config.lang}`;
|
|
params += `&APPID=${this.config.apiKey}`;
|
|
|
|
return params;
|
|
}
|
|
});
|