From 39d72e4c0d71ad1f9b405b4b0ee3c8c9d463939a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Mon, 17 Jun 2024 16:56:10 +0200 Subject: [PATCH] refactor: rewrite weather.lua to use WeatherAPI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When I looked into OpenWeatherMap 3.0 API they demanded credit card information even for their free tier. Therefore I decided to move to another provider with a generous tier. In a first step I rewrote the current weather report (i.e. no forecast) to use it. I haven't received any commentary on #442 so I don't know where your mind was going. If OpenWeatherMap phases out its 2.5 API and nobody else intends to use their 3.0 one, I offer to take this implementation instead. Signed-off-by: André Jaenisch --- weather-widget/weather.lua | 373 +++++++++---------------------------- 1 file changed, 85 insertions(+), 288 deletions(-) diff --git a/weather-widget/weather.lua b/weather-widget/weather.lua index 3ec1c3f1..3d0a5f85 100644 --- a/weather-widget/weather.lua +++ b/weather-widget/weather.lua @@ -1,9 +1,10 @@ ------------------------------------------------- --- Weather Widget based on the OpenWeatherMap --- https://openweathermap.org/ +-- Weather Widget based on the WeatherAPI +-- https://weatherapi.com/ -- -- @author Pavel Makhov -- @copyright 2020 Pavel Makhov +-- @copyright 2024 André Jaenisch ------------------------------------------------- local awful = require("awful") local watch = require("awful.widget.watch") @@ -26,6 +27,10 @@ end local LANG = gears.filesystem.file_readable(WIDGET_DIR .. "/" .. "locale/" .. SYS_LANG .. ".lua") and SYS_LANG or "en" local LCLE = require("awesome-wm-widgets.weather-widget.locale." .. LANG) +-- WeatherAPI supports only these according to https://www.weatherapi.com/docs/ +-- ar, bn, bg, zh, zh_tw, cs, da, nl, fi, fr, de, el, hi, hu, it, ja, jv, ko, +-- zh_cmn, mr, pl, pt, pa, ro, ru, sr, si, sk, es, sv, ta, te, tr, uk, ur, vi, +-- zh_wuu, zh_hsn, zh_yue, zu local function show_warning(message) @@ -59,36 +64,59 @@ local weather_popup = awful.popup { widget = {} } ---- Maps openWeatherMap icon name to file name w/o extension +--- Maps WeatherAPI condition code to file name w/o extension +--- See https://www.weatherapi.com/docs/#weather-icons local icon_map = { - ["01d"] = "clear-sky", - ["02d"] = "few-clouds", - ["03d"] = "scattered-clouds", - ["04d"] = "broken-clouds", - ["09d"] = "shower-rain", - ["10d"] = "rain", - ["11d"] = "thunderstorm", - ["13d"] = "snow", - ["50d"] = "mist", - ["01n"] = "clear-sky-night", - ["02n"] = "few-clouds-night", - ["03n"] = "scattered-clouds-night", - ["04n"] = "broken-clouds-night", - ["09n"] = "shower-rain-night", - ["10n"] = "rain-night", - ["11n"] = "thunderstorm-night", - ["13n"] = "snow-night", - ["50n"] = "mist-night" + [1000] = "clear-sky", + [1003] = "few-clouds", + [1006] = "scattered-clouds", + [1009] = "scattered-clouds", + [1030] = "mist", + [1063] = "rain", + [1066] = "snow", + [1069] = "rain", + [1072] = "snow", + [1087] = "thunderstorm", + [1114] = "snow", + [1117] = "snow", + [1135] = "mist", + [1147] = "mist", + [1150] = "snow", + [1153] = "snow", + [1168] = "snow", + [1171] = "snow", + [1180] = "rain", + [1183] = "rain", + [1186] = "rain", + [1189] = "rain", + [1192] = "rain", + [1195] = "rain", + [1198] = "rain", + [1201] = "rain", + [1204] = "snow", + [1207] = "snow", + [1210] = "snow", + [1213] = "snow", + [1216] = "snow", + [1219] = "snow", + [1222] = "snow", + [1225] = "snow", + [1237] = "snow", + [1240] = "rain", + [1243] = "rain", + [1246] = "rain", + [1249] = "snow", + [1252] = "snow", + [1255] = "snow", + [1258] = "snow", + [1261] = "snow", + [1264] = "snow", + [1273] = "thunderstorm", + [1276] = "thunderstorm", + [1279] = "thunderstorm", + [1282] = "thunderstorm" } ---- Return wind direction as a string -local function to_direction(degrees) - -- Ref: https://www.campbellsci.eu/blog/convert-wind-directions - if degrees == nil then return "Unknown dir" end - local directions = LCLE.directions - return directions[math.floor((degrees % 360) / 22.5) + 1] -end - --- Convert degrees Celsius to Fahrenheit local function celsius_to_fahrenheit(c) return c * 9 / 5 + 32 end @@ -117,11 +145,11 @@ end local function uvi_index_color(uvi) local color - if uvi >= 0 and uvi < 3 then color = '#A3BE8C' - elseif uvi >= 3 and uvi < 6 then color = '#EBCB8B' - elseif uvi >= 6 and uvi < 8 then color = '#D08770' - elseif uvi >= 8 and uvi < 11 then color = '#BF616A' - elseif uvi >= 11 then color = '#B48EAD' + if uvi >= 0 and uvi < 3 then color = '#a3be8c' + elseif uvi >= 3 and uvi < 6 then color = '#ebcb8b' + elseif uvi >= 6 and uvi < 8 then color = '#d08770' + elseif uvi >= 8 and uvi < 11 then color = '#bf616a' + elseif uvi >= 11 then color = '#b48ead' end return '' .. uvi .. '' @@ -152,13 +180,10 @@ local function worker(user_args) local timeout = args.timeout or 120 local ICONS_DIR = WIDGET_DIR .. '/icons/' .. icon_pack_name .. '/' - local owm_one_cal_api = - ('https://api.openweathermap.org/data/2.5/onecall' .. - '?lat=' .. coordinates[1] .. '&lon=' .. coordinates[2] .. '&appid=' .. api_key .. - '&units=' .. units .. '&exclude=minutely' .. - (show_hourly_forecast == false and ',hourly' or '') .. - (show_daily_forecast == false and ',daily' or '') .. - '&lang=' .. LANG) + local weather_api = + ('https://api.weatherapi.com/v1/current.json' .. + '?q=' .. coordinates[1] .. ',' .. coordinates[2] .. '&key=' .. api_key .. + '&units=' .. units .. '&lang=' .. LANG) weather_widget = wibox.widget { { @@ -267,233 +292,15 @@ local function worker(user_args) layout = wibox.layout.flex.horizontal, update = function(self, weather) self:get_children_by_id('icon')[1]:set_image( - ICONS_DIR .. icon_map[weather.weather[1].icon] .. icons_extension) - self:get_children_by_id('temp')[1]:set_text(gen_temperature_str(weather.temp, '%.0f', false, units)) + ICONS_DIR .. icon_map[weather.condition.code] .. icons_extension) + self:get_children_by_id('temp')[1]:set_text(gen_temperature_str(weather.temp_c, '%.0f', false, units)) self:get_children_by_id('feels_like_temp')[1]:set_text( - LCLE.feels_like .. gen_temperature_str(weather.feels_like, '%.0f', false, units)) - self:get_children_by_id('description')[1]:set_text(weather.weather[1].description) + LCLE.feels_like .. gen_temperature_str(weather.feelslike_c, '%.0f', false, units)) + self:get_children_by_id('description')[1]:set_text(weather.condition.text) self:get_children_by_id('wind')[1]:set_markup( - LCLE.wind .. '' .. weather.wind_speed .. 'm/s (' .. to_direction(weather.wind_deg) .. ')') + LCLE.wind .. '' .. weather.wind_kph .. 'km/h (' .. weather.wind_dir .. ')') self:get_children_by_id('humidity')[1]:set_markup(LCLE.humidity .. '' .. weather.humidity .. '%') - self:get_children_by_id('uv')[1]:set_markup(LCLE.uv .. uvi_index_color(weather.uvi)) - end - } - - - local daily_forecast_widget = { - forced_width = 300, - layout = wibox.layout.flex.horizontal, - update = function(self, forecast, timezone_offset) - local count = #self - for i = 0, count do self[i]=nil end - for i, day in ipairs(forecast) do - if i > 5 then break end - local day_forecast = wibox.widget { - { - text = os.date('%a', tonumber(day.dt) + tonumber(timezone_offset)), - align = 'center', - font = font_name .. ' 9', - widget = wibox.widget.textbox - }, - { - { - { - image = ICONS_DIR .. icon_map[day.weather[1].icon] .. icons_extension, - resize = true, - forced_width = 48, - forced_height = 48, - widget = wibox.widget.imagebox - }, - align = 'center', - layout = wibox.container.place - }, - { - text = day.weather[1].description, - font = font_name .. ' 8', - align = 'center', - forced_height = 50, - widget = wibox.widget.textbox - }, - layout = wibox.layout.fixed.vertical - }, - { - { - text = gen_temperature_str(day.temp.day, '%.0f', false, units), - align = 'center', - font = font_name .. ' 9', - widget = wibox.widget.textbox - }, - { - text = gen_temperature_str(day.temp.night, '%.0f', false, units), - align = 'center', - font = font_name .. ' 9', - widget = wibox.widget.textbox - }, - layout = wibox.layout.fixed.vertical - }, - spacing = 8, - layout = wibox.layout.fixed.vertical - } - table.insert(self, day_forecast) - end - end - } - - local hourly_forecast_graph = wibox.widget { - step_width = 12, - color = '#EBCB8B', - background_color = beautiful.bg_normal, - forced_height = 100, - forced_width = 300, - widget = wibox.widget.graph, - set_max_value = function(self, new_max_value) - self.max_value = new_max_value - end, - set_min_value = function(self, new_min_value) - self.min_value = new_min_value - end - } - local hourly_forecast_negative_graph = wibox.widget { - step_width = 12, - color = '#5E81AC', - background_color = beautiful.bg_normal, - forced_height = 100, - forced_width = 300, - widget = wibox.widget.graph, - set_max_value = function(self, new_max_value) - self.max_value = new_max_value - end, - set_min_value = function(self, new_min_value) - self.min_value = new_min_value - end - } - - local hourly_forecast_widget = { - layout = wibox.layout.fixed.vertical, - update = function(self, hourly) - local hours_below = { - id = 'hours', - forced_width = 300, - layout = wibox.layout.flex.horizontal - } - local temp_below = { - id = 'temp', - forced_width = 300, - layout = wibox.layout.flex.horizontal - } - - local max_temp = -1000 - local min_temp = 1000 - local values = {} - for i, hour in ipairs(hourly) do - if i > 25 then break end - values[i] = hour.temp - if max_temp < hour.temp then max_temp = hour.temp end - if min_temp > hour.temp then min_temp = hour.temp end - if (i - 1) % 5 == 0 then - table.insert(hours_below, wibox.widget { - text = os.date(time_format_12h and '%I%p' or '%H:00', tonumber(hour.dt)), - align = 'center', - font = font_name .. ' 9', - widget = wibox.widget.textbox - }) - table.insert(temp_below, wibox.widget { - markup = '' - .. string.format('%.0f', hour.temp) .. '°' .. '', - align = 'center', - font = font_name .. ' 9', - widget = wibox.widget.textbox - }) - end - end - - hourly_forecast_graph:set_max_value(math.max(max_temp, math.abs(min_temp))) - hourly_forecast_graph:set_min_value(min_temp > 0 and min_temp * 0.7 or 0) -- move graph a bit up - - hourly_forecast_negative_graph:set_max_value(math.abs(min_temp)) - hourly_forecast_negative_graph:set_min_value(max_temp < 0 and math.abs(max_temp) * 0.7 or 0) - - for _, value in ipairs(values) do - if value >= 0 then - hourly_forecast_graph:add_value(value) - hourly_forecast_negative_graph:add_value(0) - else - hourly_forecast_graph:add_value(0) - hourly_forecast_negative_graph:add_value(math.abs(value)) - end - end - - local count = #self - for i = 0, count do self[i]=nil end - - -- all temperatures are positive - if min_temp > 0 then - table.insert(self, wibox.widget{ - { - hourly_forecast_graph, - reflection = {horizontal = true}, - widget = wibox.container.mirror - }, - { - temp_below, - valign = 'bottom', - widget = wibox.container.place - }, - id = 'graph', - layout = wibox.layout.stack - }) - table.insert(self, hours_below) - - -- all temperatures are negative - elseif max_temp < 0 then - table.insert(self, hours_below) - table.insert(self, wibox.widget{ - { - hourly_forecast_negative_graph, - reflection = {horizontal = true, vertical = true}, - widget = wibox.container.mirror - }, - { - temp_below, - valign = 'top', - widget = wibox.container.place - }, - id = 'graph', - layout = wibox.layout.stack - }) - - -- there are both negative and positive temperatures - else - table.insert(self, wibox.widget{ - { - hourly_forecast_graph, - reflection = {horizontal = true}, - widget = wibox.container.mirror - }, - { - temp_below, - valign = 'bottom', - widget = wibox.container.place - }, - id = 'graph', - layout = wibox.layout.stack - }) - table.insert(self, wibox.widget{ - { - hourly_forecast_negative_graph, - reflection = {horizontal = true, vertical = true}, - widget = wibox.container.mirror - }, - { - hours_below, - valign = 'top', - widget = wibox.container.place - }, - id = 'graph', - layout = wibox.layout.stack - }) - end + self:get_children_by_id('uv')[1]:set_markup(LCLE.uv .. uvi_index_color(weather.uv)) end } @@ -501,7 +308,7 @@ local function worker(user_args) if stderr ~= '' then if not warning_shown then if (stderr ~= 'curl: (52) Empty reply from server' - and stderr ~= 'curl: (28) Failed to connect to api.openweathermap.org port 443: Connection timed out' + and stderr ~= 'curl: (28) Failed to connect to api.weatherapi.com port 443: Connection timed out' and stderr:find('^curl: %(18%) transfer closed with %d+ bytes remaining to read$') ~= nil ) then show_warning(stderr) @@ -520,9 +327,9 @@ local function worker(user_args) widget:is_ok(true) local result = json.decode(stdout) - - widget:set_image(ICONS_DIR .. icon_map[result.current.weather[1].icon] .. icons_extension) - widget:set_text(gen_temperature_str(result.current.temp, '%.0f', both_units_widget, units)) + widget:set_image(ICONS_DIR .. icon_map[result.current.condition.code] .. icons_extension) + -- TODO: if units isn't "metric", read temp_f instead + widget:set_text(gen_temperature_str(result.current.temp_c, '%.0f', both_units_widget, units)) current_weather_widget:update(result.current) @@ -532,16 +339,6 @@ local function worker(user_args) layout = wibox.layout.fixed.vertical } - if show_hourly_forecast then - hourly_forecast_widget:update(result.hourly) - table.insert(final_widget, hourly_forecast_widget) - end - - if show_daily_forecast then - daily_forecast_widget:update(result.daily, result.timezone_offset) - table.insert(final_widget, daily_forecast_widget) - end - weather_popup:setup({ { final_widget, @@ -554,17 +351,17 @@ local function worker(user_args) end weather_widget:buttons(gears.table.join(awful.button({}, 1, function() - if weather_popup.visible then - weather_widget:set_bg('#00000000') - weather_popup.visible = not weather_popup.visible - else - weather_widget:set_bg(beautiful.bg_focus) - weather_popup:move_next_to(mouse.current_widget_geometry) - end - end))) + if weather_popup.visible then + weather_widget:set_bg('#00000000') + weather_popup.visible = not weather_popup.visible + else + weather_widget:set_bg(beautiful.bg_focus) + weather_popup:move_next_to(mouse.current_widget_geometry) + end + end))) watch( - string.format(GET_FORECAST_CMD, owm_one_cal_api), + string.format(GET_FORECAST_CMD, weather_api), timeout, -- API limit is 1k req/day; day has 1440 min; every 2 min is good update_widget, weather_widget )