From 4a162543f60a7445eb4d39e9edd61c3ca2338400 Mon Sep 17 00:00:00 2001 From: Bryan Zhu Date: Tue, 30 Jun 2020 02:40:41 -0400 Subject: [PATCH] added OpenWeatherMap One Call API function to default Weather module, added wDataHourly type --- .../weather/providers/openweathermap.js | 184 +++++++++++++++++- modules/default/weather/wdataHourly.njk | 29 +++ modules/default/weather/weather.js | 36 +++- modules/default/weather/weatherprovider.js | 18 ++ 4 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 modules/default/weather/wdataHourly.njk diff --git a/modules/default/weather/providers/openweathermap.js b/modules/default/weather/providers/openweathermap.js index 70b715a0..2cdd6d96 100755 --- a/modules/default/weather/providers/openweathermap.js +++ b/modules/default/weather/providers/openweathermap.js @@ -35,7 +35,7 @@ WeatherProvider.register("openweathermap", { .finally(() => this.updateAvailable()); }, - // Overwrite the fetchCurrentWeather method. + // Overwrite the fetchWeatherForecast method. fetchWeatherForecast() { this.fetchData(this.getUrl()) .then((data) => { @@ -56,6 +56,27 @@ WeatherProvider.register("openweathermap", { .finally(() => this.updateAvailable()); }, + // Overwrite the fetchWeatherData method. + fetchWeatherData() { + this.fetchData(this.getUrl()) + .then((data) => { + if (!data || !data.list || !data.list.length) { + // Did not receive usable new data. + // Maybe this needs a better check? + return; + } + + this.setFetchedLocation(`(${data.lat},${data.lon})`); + + const wData = this.generateWeatherObjectsFromOnecall(data); + this.setWeatherData(wData); + }) + .catch(function (request) { + Log.error("Could not load data ... ", request); + }) + .finally(() => this.updateAvailable()); + }, + /** OpenWeatherMap Specific Methods - These are not part of the default provider methods */ /* * Gets the complete url for the request @@ -95,6 +116,153 @@ WeatherProvider.register("openweathermap", { return days; }, + /* + * 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? + const wData = {current: new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits), hours: [], days: []}; + return wData; + }, + + /* + * 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 + const current = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); + if (!isNaN(data.current)) { + current.date = moment(data.current.dt, "X").utcOffset(data.timezone_offset/60); + current.windSpeed = data.current.wind_speed; + current.windDirection = data.current.wind_deg; + current.sunrise = moment(data.current.sunrise, "X").utcOffset(data.timezone_offset/60); + current.sunset = moment(data.current.sunset, "X").utcOffset(data.timezone_offset/60); + current.temperature = data.current.temp; + current.weatherType = this.convertWeatherType(data.current.weather[0].icon); + current.humidity = data.current.humidity; + if (!isNaN(data.current.rain)) { + current.rain = data.current.rain.1h; + precip = true; + } + if (!isNaN(data.current.snow)) { + current.snow = data.current.snow.1h; + precip = true; + } + if (precip) { + current.precipitation = current.rain+current.snow; + } + current.feelsLikeTemp = data.current.feels_like; + } + + let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); + + // let onecallDailyFormat = "MMM DD" + // let onecallHourlyFormat = "HH" + // let onecallMinutelyFormat = "HH:mm" + // if (this.config.timeFormat === 12) { + // if (this.config.showPeriod === true) { + // if (this.config.showPeriodUpper === true) { + // onecallHourlyFormat = "hhA" + // onecallMinutelyFormat = "hh:mmA" + // } else { + // onecallHourlyFormat = "hha" + // onecallMinutelyFormat = "hh:mma" + // } + // } else { + // onecallHourlyFormat = "hh" + // onecallMinutelyFormat = "hh:mm" + // } + // } + + // get hourly weather + const hours = []; + if (!isNaN(data.hourly)) { + for (const hour of data.hourly) { + weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset/60); + // weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset/60).format(onecallDailyFormat+","+onecallHourlyFormat); + weather.temperature = hour.temp; + weather.feelsLikeTemp = hour.feels_like; + weather.humidity = hour.humidity; + weather.windSpeed = hour.wind_speed; + weather.windDirection = hour.wind_deg; + weather.weatherType = this.convertWeatherType(hour.weather[0].icon); + precip = false; + if (!isNaN(hour.rain)) { + if (this.config.units === "imperial") { + weather.rain = hour.rain.1h / 25.4; + } else { + weather.rain = hour.rain.1h; + } + precip = true; + } + if (!isNaN(hour.snow)) { + if (this.config.units === "imperial") { + weather.snow = hour.snow.1h / 25.4; + } else { + weather.snow = hour.snow.1h; + } + precip = true; + } + if (precip) { + weather.precipitation = weather.rain+weather.snow; + } + + hours.push(weather); + weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); + } + } + + // get daily weather + const days = []; + if (!isNaN(data.daily)) { + for (const day of data.daily) { + weather.date = moment(day.dt, "X").utcOffset(data.timezone_offset/60); + weather.sunrise = moment(day.sunrise, "X").utcOffset(data.timezone_offset/60); + weather.sunset = moment(day.sunset, "X").utcOffset(data.timezone_offset/60); + // weather.date = moment(day.dt, "X").utcOffset(data.timezone_offset/60).format(onecallDailyFormat); + // weather.sunrise = moment(day.sunrise, "X").utcOffset(data.timezone_offset/60).format(onecallMinutelyFormat); + // weather.sunset = moment(day.sunset, "X").utcOffset(data.timezone_offset/60).format(onecallMinutelyFormat); + weather.minTemperature = day.temp.min; + weather.maxTemperature = day.temp.max; + weather.humidity = day.humidity; + weather.windSpeed = day.wind_speed; + weather.windDirection = day.wind_deg; + weather.weatherType = this.convertWeatherType(day.weather[0].icon); + precip = false; + if (!isNaN(day.rain)) { + if (this.config.units === "imperial") { + weather.rain = day.rain / 25.4; + } else { + weather.rain = day.rain; + } + precip = true; + } + if (!isNaN(day.snow)) { + if (this.config.units === "imperial") { + weather.snow = day.snow / 25.4; + } else { + weather.snow = day.snow; + } + precip = true; + } + if (precip) { + weather.precipitation = weather.rain+weather.snow; + } + + days.push(weather); + weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); + } + } + + return {current: current, hours: hours, days: days}; + }, + /* * fetch forecast information for 3-hourly forecast (available for free subscription). */ @@ -256,7 +424,19 @@ WeatherProvider.register("openweathermap", { */ getParams() { let params = "?"; - if (this.config.locationID) { + if (this.config.weatherEndpoint === "/onecall") { + params += "lat=" + this.config.lat; + params += "&lon=" + this.config.lon; + if (this.config.type === "wDataCurrent") { + params += "&exclude=minutely,hourly,daily"; + } else if (this.config.type === "wDataHourly") { + params += "&exclude=current,minutely,daily"; + } else if (this.config.type === "wDataDaily") { + params += "&exclude=current,minutely,hourly"; + } else { + params += "&exclude=minutely"; + } + } else if (this.config.locationID) { params += "id=" + this.config.locationID; } else if (this.config.location) { params += "q=" + this.config.location; diff --git a/modules/default/weather/wdataHourly.njk b/modules/default/weather/wdataHourly.njk new file mode 100644 index 00000000..f69ca8e7 --- /dev/null +++ b/modules/default/weather/wdataHourly.njk @@ -0,0 +1,29 @@ +{% if wData %} + {% set numSteps = wData.hours | calcNumEntries %} + {% set currentStep = 0 %} + + {% set hours = wData.hours.slice(0, numSteps) %} + {% for hour in hours %} + + + + + {% if config.showPrecipitationAmount %} + + {% endif %} + + {% set currentStep = currentStep + 1 %} + {% endfor %} +
{{ hour.date | formatTimeMoment }} + {{ hour.temperature | roundValue | unit("temperature") }} + + {{ hour.precipitation | unit("precip") }} +
+{% else %} +
+ {{ "LOADING" | translate | safe }} +
+{% endif %} + + + diff --git a/modules/default/weather/weather.js b/modules/default/weather/weather.js index 150ba084..1ec7251d 100644 --- a/modules/default/weather/weather.js +++ b/modules/default/weather/weather.js @@ -11,7 +11,7 @@ Module.register("weather", { defaults: { weatherProvider: "openweathermap", roundTemp: false, - type: "current", //current, forecast + type: "current", //current, forecast, wDataCurrent, wDataHourly, wDataDaily location: false, locationID: false, @@ -37,6 +37,7 @@ Module.register("weather", { showIndoorTemperature: false, showIndoorHumidity: false, maxNumberOfDays: 5, + maxEntries: 5, fade: true, fadePoint: 0.25, // Start on 1/4th of the list. @@ -132,6 +133,7 @@ Module.register("weather", { config: this.config, current: this.weatherProvider.currentWeather(), forecast: this.weatherProvider.weatherForecast(), + wData: this.weatherProvider.weatherData(), indoor: { humidity: this.indoorHumidity, temperature: this.indoorTemperature @@ -153,7 +155,11 @@ Module.register("weather", { } setTimeout(() => { - if (this.config.type === "forecast") { + if (this.config.type === "wDataCurrent" + || this.config.type === "wDataHourly" + || this.config.type === "wDataDaily") { + this.weatherProvider.fetchWeatherData(); + } else if (this.config.type === "forecast") { this.weatherProvider.fetchWeatherForecast(); } else { this.weatherProvider.fetchCurrentWeather(); @@ -187,6 +193,25 @@ Module.register("weather", { return date.format("HH:mm"); }.bind(this) ); + this.nunjucksEnvironment().addFilter( + "formatTimeMoment", + function (date) { + + if (this.config.timeFormat !== 24) { + if (this.config.showPeriod) { + if (this.config.showPeriodUpper) { + return date.format("h:mm A"); + } else { + return date.format("h:mm a"); + } + } else { + return date.format("h:mm"); + } + } + + return date.format("HH:mm"); + }.bind(this) + ); this.nunjucksEnvironment().addFilter( "unit", @@ -243,6 +268,13 @@ Module.register("weather", { }.bind(this) ); + this.nunjucksEnvironment().addFilter( + "calcNumEntries", + function (dataArray) { + return Math.min(dataArray.length, this.config.maxNumberOfEntries); + }.bind(this) + ); + this.nunjucksEnvironment().addFilter( "opacity", function (currentStep, numSteps) { diff --git a/modules/default/weather/weatherprovider.js b/modules/default/weather/weatherprovider.js index cfcb3ef6..ac8618eb 100644 --- a/modules/default/weather/weatherprovider.js +++ b/modules/default/weather/weatherprovider.js @@ -16,6 +16,7 @@ var WeatherProvider = Class.extend({ // Try to not access them directly. currentWeatherObject: null, weatherForecastArray: null, + weatherDataObject: null, fetchedLocationName: null, // The following properties will be set automatically. @@ -56,6 +57,12 @@ var WeatherProvider = Class.extend({ Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherForecast method.`); }, + // This method should start the API request to fetch the weather forecast. + // This method should definitely be overwritten in the provider. + fetchWeatherData: function () { + Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherData method.`); + }, + // This returns a WeatherDay object for the current weather. currentWeather: function () { return this.currentWeatherObject; @@ -66,6 +73,11 @@ var WeatherProvider = Class.extend({ return this.weatherForecastArray; }, + // This returns an object containing WeatherDay object(s) depending on the type of call. + weatherData: function () { + return this.weatherDataObject; + }, + // This returns the name of the fetched location or an empty string. fetchedLocation: function () { return this.fetchedLocationName || ""; @@ -83,6 +95,12 @@ var WeatherProvider = Class.extend({ this.weatherForecastArray = weatherForecastArray; }, + // Set the weatherDataObject and notify the delegate that new information is available. + setWeatherData: function (weatherDataObject) { + // We should check here if we are passing a WeatherDay + this.weatherDataObject = weatherDataObject; + }, + // Set the fetched location name. setFetchedLocation: function (name) { this.fetchedLocationName = name;