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
)