From df7260503d0cd7bb0e557f8ad111615491d9cda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Tue, 25 Jun 2024 08:33:30 +0200 Subject: [PATCH] refactor: restore original implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is going to be marked as deprecated by streetturtle going forward. As of today, I am still able to successfully call the v2.5 API. Signed-off-by: André Jaenisch --- weather-widget/weather.lua | 387 ++++++++++++++++++++++++++++--------- 1 file changed, 291 insertions(+), 96 deletions(-) diff --git a/weather-widget/weather.lua b/weather-widget/weather.lua index c53e484..3ec1c3f 100644 --- a/weather-widget/weather.lua +++ b/weather-widget/weather.lua @@ -1,10 +1,9 @@ ------------------------------------------------- --- Weather Widget based on the WeatherAPI --- https://weatherapi.com/ +-- Weather Widget based on the OpenWeatherMap +-- https://openweathermap.org/ -- -- @author Pavel Makhov -- @copyright 2020 Pavel Makhov --- @copyright 2024 André Jaenisch ------------------------------------------------- local awful = require("awful") local watch = require("awful.widget.watch") @@ -27,10 +26,6 @@ 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) @@ -64,59 +59,36 @@ local weather_popup = awful.popup { widget = {} } ---- Maps WeatherAPI condition code to file name w/o extension ---- See https://www.weatherapi.com/docs/#weather-icons +--- Maps openWeatherMap icon name to file name w/o extension local icon_map = { - [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" + ["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" } +--- 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 @@ -145,11 +117,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 .. '' @@ -171,16 +143,22 @@ local function worker(user_args) local api_key = args.api_key local font_name = args.font_name or beautiful.font:gsub("%s%d+$", "") local units = args.units or 'metric' + local time_format_12h = args.time_format_12h local both_units_widget = args.both_units_widget or false + local show_hourly_forecast = args.show_hourly_forecast + local show_daily_forecast = args.show_daily_forecast local icon_pack_name = args.icons or 'weather-underground-icons' local icons_extension = args.icons_extension or '.png' local timeout = args.timeout or 120 local ICONS_DIR = WIDGET_DIR .. '/icons/' .. icon_pack_name .. '/' - local weather_api = - ('https://api.weatherapi.com/v1/current.json' .. - '?q=' .. coordinates[1] .. ',' .. coordinates[2] .. '&key=' .. api_key .. - '&units=' .. units .. '&lang=' .. LANG) + 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) weather_widget = wibox.widget { { @@ -289,15 +267,233 @@ 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.condition.code] .. icons_extension) - self:get_children_by_id('temp')[1]:set_text(gen_temperature_str(weather.temp_c, '%.0f', false, units)) + 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)) self:get_children_by_id('feels_like_temp')[1]:set_text( - LCLE.feels_like .. gen_temperature_str(weather.feelslike_c, '%.0f', false, units)) - self:get_children_by_id('description')[1]:set_text(weather.condition.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) self:get_children_by_id('wind')[1]:set_markup( - LCLE.wind .. '' .. weather.wind_kph .. 'km/h (' .. weather.wind_dir .. ')') + LCLE.wind .. '' .. weather.wind_speed .. 'm/s (' .. to_direction(weather.wind_deg) .. ')') 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.uv)) + 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 end } @@ -305,7 +501,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.weatherapi.com port 443: Connection timed out' + and stderr ~= 'curl: (28) Failed to connect to api.openweathermap.org port 443: Connection timed out' and stderr:find('^curl: %(18%) transfer closed with %d+ bytes remaining to read$') ~= nil ) then show_warning(stderr) @@ -319,25 +515,14 @@ local function worker(user_args) return end - if string.match(stdout, '<') ~= nil then - if not warning_shown then - warning_shown = true - widget:is_ok(false) - tooltip:add_to_object(widget) - - widget:connect_signal('mouse::enter', function() tooltip.text = stdout end) - end - return - end - warning_shown = false tooltip:remove_from_object(widget) widget:is_ok(true) local result = json.decode(stdout) - 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)) + + 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)) current_weather_widget:update(result.current) @@ -347,6 +532,16 @@ 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, @@ -359,17 +554,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, weather_api), + string.format(GET_FORECAST_CMD, owm_one_cal_api), timeout, -- API limit is 1k req/day; day has 1440 min; every 2 min is good update_widget, weather_widget )