summaryrefslogtreecommitdiff
path: root/weather-widget/weather.lua
diff options
context:
space:
mode:
Diffstat (limited to 'weather-widget/weather.lua')
-rw-r--r--weather-widget/weather.lua581
1 files changed, 406 insertions, 175 deletions
diff --git a/weather-widget/weather.lua b/weather-widget/weather.lua
index 5aaf426..4fcdba7 100644
--- a/weather-widget/weather.lua
+++ b/weather-widget/weather.lua
@@ -3,225 +3,456 @@
-- https://openweathermap.org/
--
-- @author Pavel Makhov
--- @copyright 2018 Pavel Makhov
+-- @copyright 2020 Pavel Makhov
-------------------------------------------------
-
-local socket = require("socket")
-local http = require("socket.http")
-local ltn12 = require("ltn12")
+local awful = require("awful")
+local watch = require("awful.widget.watch")
local json = require("json")
local naughty = require("naughty")
local wibox = require("wibox")
local gears = require("gears")
+local beautiful = require("beautiful")
-local path_to_icons = "/usr/share/icons/Arc/status/symbolic/"
+local HOME_DIR = os.getenv("HOME")
+local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/weather-widget'
+local GET_FORECAST_CMD = [[bash -c "curl -s --show-error -X GET '%s'"]]
+
+local function show_warning(message)
+ naughty.notify {
+ preset = naughty.config.presets.critical,
+ title = 'Weather Widget',
+ text = message
+ }
+end
local weather_widget = {}
+local warning_shown = false
+local notification
+local tooltip = awful.tooltip {
+ mode = 'outside',
+ preferred_positions = {'bottom'}
+}
+
+local weather_popup = awful.popup {
+ ontop = true,
+ visible = false,
+ shape = gears.shape.rounded_rect,
+ border_width = 1,
+ border_color = beautiful.bg_focus,
+ maximum_width = 400,
+ offset = {y = 5},
+ widget = {}
+}
+
+--- Maps openWeatherMap icon name to file name w/o extension
+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"
+}
+
+--- 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 = {
+ "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW",
+ "WSW", "W", "WNW", "NW", "NNW", "N"
+ }
+ 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
+
+-- Convert degrees Fahrenheit to Celsius
+local function fahrenheit_to_celsius(f) return (f - 32) * 5 / 9 end
+
+local function gen_temperature_str(temp, fmt_str, show_other_units, units)
+ local temp_str = string.format(fmt_str, temp)
+ local s = temp_str .. '°' .. (units == 'metric' and 'C' or 'F')
+
+ if (show_other_units) then
+ local temp_conv, units_conv
+ if (units == 'metric') then
+ temp_conv = celsius_to_fahrenheit(temp)
+ units_conv = 'F'
+ else
+ temp_conv = fahrenheit_to_celsius(temp)
+ units_conv = 'C'
+ end
+
+ local temp_conv_str = string.format(fmt_str, temp_conv)
+ s = s .. ' ' .. '(' .. temp_conv_str .. '°' .. units_conv .. ')'
+ end
+ return s
+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'
+ end
+
+ return '<span weight="bold" foreground="' .. color .. '">' .. uvi .. '</span>'
+end
local function worker(args)
local args = args or {}
- local font = args.font or 'Play 9'
- local city = args.city or 'Montreal,ca'
- local api_key = args.api_key or naughty.notify{preset = naughty.config.presets.critical, text = 'OpenweatherMap API key is not set'}
+ --- Validate required parameters
+ if args.coordinates == nil or args.api_key == nil then
+ show_warning('Required parameters are not set: ' ..
+ (args.coordinates == nil and '<b>coordinates</b>' or '') ..
+ (args.api_key == nil and ', <b>api_key</b> ' or ''))
+ return
+ end
+
+ local coordinates = args.coordinates
+ 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 both_units_popup = args.both_units_popup or false
- local position = args.notification_position or "top_right"
+ 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 weather_api_url = (
- 'https://api.openweathermap.org/data/2.5/weather'
- .. '?q=' .. city
- .. '&appid=' .. api_key
- .. '&units=' .. units
- )
+ 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 ''))
- local icon_widget = wibox.widget {
+ weather_widget = wibox.widget {
+ {
+ {
+ id = 'icon',
+ resize = true,
+ widget = wibox.widget.imagebox
+ },
+ valign = 'center',
+ widget = wibox.container.place,
+ },
{
- id = "icon",
- resize = false,
- widget = wibox.widget.imagebox,
+ id = 'txt',
+ widget = wibox.widget.textbox
},
- layout = wibox.container.margin(_, 0, 0, 3),
+ layout = wibox.layout.fixed.horizontal,
set_image = function(self, path)
- self.icon.image = path
+ self:get_children_by_id('icon')[1].image = path
end,
+ set_text = function(self, text)
+ self:get_children_by_id('txt')[1].text = text
+ end,
+ is_ok = function(self, is_ok)
+ if is_ok then
+ self:get_children_by_id('icon')[1]:set_opacity(1)
+ self:get_children_by_id('icon')[1]:emit_signal('widget:redraw_needed')
+ else
+ self:get_children_by_id('icon')[1]:set_opacity(0.2)
+ self:get_children_by_id('icon')[1]:emit_signal('widget:redraw_needed')
+ end
+ end
}
- local temp_widget = wibox.widget {
- font = font,
- widget = wibox.widget.textbox,
+ local current_weather_widget = wibox.widget {
+ {
+ {
+ {
+ id = 'icon',
+ resize = true,
+ forced_width = 128,
+ forced_height = 128,
+ widget = wibox.widget.imagebox
+ },
+ align = 'center',
+ widget = wibox.container.place
+ },
+ {
+ id = 'description',
+ font = font_name .. ' 10',
+ align = 'center',
+ widget = wibox.widget.textbox
+ },
+ forced_width = 128,
+ layout = wibox.layout.align.vertical
+ },
+ {
+ {
+ {
+ id = 'temp',
+ font = font_name .. ' 48',
+ widget = wibox.widget.textbox
+ },
+ {
+ id = 'feels_like_temp',
+ align = 'center',
+ font = font_name .. ' 9',
+ widget = wibox.widget.textbox
+ },
+ layout = wibox.layout.fixed.vertical
+ },
+ {
+ {
+ id = 'wind',
+ font = font_name .. ' 9',
+ widget = wibox.widget.textbox
+ },
+ {
+ id = 'humidity',
+ font = font_name .. ' 9',
+ widget = wibox.widget.textbox
+ },
+ {
+ id = 'uv',
+ font = font_name .. ' 9',
+ widget = wibox.widget.textbox
+ },
+ expand = 'inside',
+ layout = wibox.layout.align.vertical
+ },
+ spacing = 16,
+ forced_width = 150,
+ layout = wibox.layout.fixed.vertical
+ },
+ forced_width = 300,
+ layout = wibox.layout.flex.horizontal,
+ update = function(self, weather)
+ self:get_children_by_id('icon')[1]:set_image(WIDGET_DIR .. '/icons/' .. icon_pack_name .. '/' .. 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('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('Wind: <b>' .. weather.wind_speed .. 'm/s (' .. to_direction(weather.wind_deg) .. ')</b>')
+ self:get_children_by_id('humidity')[1]:set_markup('Humidity: <b>' .. weather.humidity .. '%</b>')
+ self:get_children_by_id('uv')[1]:set_markup('UV: ' .. uvi_index_color(weather.uvi))
+ end
}
- weather_widget = wibox.widget {
- icon_widget,
- temp_widget,
- layout = wibox.layout.fixed.horizontal,
- }
- --- Maps openWeatherMap icons to Arc icons
- local icon_map = {
- ["01d"] = "weather-clear-symbolic.svg",
- ["02d"] = "weather-few-clouds-symbolic.svg",
- ["03d"] = "weather-clouds-symbolic.svg",
- ["04d"] = "weather-overcast-symbolic.svg",
- ["09d"] = "weather-showers-scattered-symbolic.svg",
- ["10d"] = "weather-showers-symbolic.svg",
- ["11d"] = "weather-storm-symbolic.svg",
- ["13d"] = "weather-snow-symbolic.svg",
- ["50d"] = "weather-fog-symbolic.svg",
- ["01n"] = "weather-clear-night-symbolic.svg",
- ["02n"] = "weather-few-clouds-night-symbolic.svg",
- ["03n"] = "weather-clouds-night-symbolic.svg",
- ["04n"] = "weather-overcast-symbolic.svg",
- ["09n"] = "weather-showers-scattered-symbolic.svg",
- ["10n"] = "weather-showers-symbolic.svg",
- ["11n"] = "weather-storm-symbolic.svg",
- ["13n"] = "weather-snow-symbolic.svg",
- ["50n"] = "weather-fog-symbolic.svg"
+ 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 = WIDGET_DIR .. '/icons/' .. icon_pack_name .. '/' .. 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
}
- --- 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"
+ 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 directions = {
- "N",
- "NNE",
- "NE",
- "ENE",
- "E",
- "ESE",
- "SE",
- "SSE",
- "S",
- "SSW",
- "SW",
- "WSW",
- "W",
- "WNW",
- "NW",
- "NNW",
- "N",
- }
- 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
+ local hourly_forecast_widget = {
+ layout = wibox.layout.fixed.vertical,
+ update = function(self, hourly)
+ local hours_below = {
+ id = 'hours',
+ layout = wibox.layout.flex.horizontal
+ }
+ local temp_below = {
+ id = 'temp',
+ forced_width = 300,
+ layout = wibox.layout.flex.horizontal
+ }
- -- Convert degrees Fahrenheit to Celsius
- local function fahrenheit_to_celsius(f)
- return (f-32)*5/9
- end
-
- local weather_timer = gears.timer({ timeout = 60 })
- local resp
+ 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 = '<span foreground="#2E3440">' .. string.format('%.0f', hour.temp) .. '°' .. '</span>',
+ align = 'center',
+ font = font_name .. ' 9',
+ widget = wibox.widget.textbox
+ })
+ end
+ end
+ hourly_forecast_graph:set_max_value(max_temp)
+ hourly_forecast_graph:set_min_value(min_temp * 0.7) -- move graph a bit up
+ for i, value in ipairs(values) do
+ hourly_forecast_graph:add_value(value)
+ end
- local function gen_temperature_str(temp, fmt_str, show_other_units)
- local temp_str = string.format(fmt_str, temp)
- local s = temp_str .. '°' .. (units == 'metric' and 'C' or 'F')
+ local count = #self
+ for i = 0, count do self[i]=nil end
- if (show_other_units) then
- local temp_conv, units_conv
- if (units == 'metric') then
- temp_conv = celsius_to_fahrenheit(temp)
- units_conv = 'F'
- else
- temp_conv = fahrenheit_to_celsius(temp)
- units_conv = 'C'
- end
- local temp_conv_str = string.format(fmt_str, temp_conv)
- s = s .. ' ' .. '('.. temp_conv_str .. '°' .. units_conv .. ')'
+ table.insert(self, temp_below)
+ 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)
end
- return s
- end
+ }
- local function error_display(resp_json)
- weather_timer.timeout = math.min(15 * 60, weather_timer.timeout * 2)
- weather_timer:again()
- local err_resp = json.decode(resp_json)
- naughty.notify{
- title = 'Weather Widget Error',
- text = err_resp.message,
- preset = naughty.config.presets.critical,
- }
- end
+ local function update_widget(widget, stdout, stderr)
+ if stderr ~= '' then
+ if not warning_shown then
+ show_warning(stderr)
+ warning_shown = true
+ widget:is_ok(false)
+ tooltip:add_to_object(widget)
- weather_timer:connect_signal("timeout", function ()
- local resp_json = {}
- local res, status = http.request{
- url=weather_api_url,
- sink=ltn12.sink.table(resp_json),
- -- ref:
- -- http://w3.impa.br/~diego/software/luasocket/old/luasocket-2.0/http.html
- create=function()
- -- ref: https://stackoverflow.com/a/6021774/595220
- local req_sock = socket.tcp()
- -- 't' — overall timeout
- req_sock:settimeout(0.2, 't')
- -- 'b' — block timeout
- req_sock:settimeout(0.001, 'b')
- return req_sock
+ widget:connect_signal('mouse::enter', function() tooltip.text = stderr end)
end
+ return
+ end
+
+ warning_shown = false
+ tooltip:remove_from_object(widget)
+ widget:is_ok(true)
+
+ local result = json.decode(stdout)
+
+ widget:set_image(WIDGET_DIR .. '/icons/' .. icon_pack_name .. '/' .. 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)
+
+ local final_widget = {
+ current_weather_widget,
+ spacing = 16,
+ layout = wibox.layout.fixed.vertical
}
- if (resp_json ~= nil) then
- resp_json = table.concat(resp_json)
+
+ if show_hourly_forecast then
+ hourly_forecast_widget:update(result.hourly)
+ table.insert(final_widget, hourly_forecast_widget)
end
- if (status ~= 200 and resp_json ~= nil and resp_json ~= '') then
- if (not pcall(error_display, resp_json)) then
- naughty.notify{
- title = 'Weather Widget Error',
- text = 'Cannot parse answer',
- preset = naughty.config.presets.critical,
- }
- end
- elseif (resp_json ~= nil and resp_json ~= '') then
- resp = json.decode(resp_json)
- icon_widget.image = path_to_icons .. icon_map[resp.weather[1].icon]
- temp_widget:set_text(gen_temperature_str(resp.main.temp, '%.0f', both_units_widget))
- weather_timer.timeout = 60
- weather_timer:again()
+ if show_daily_forecast then
+ daily_forecast_widget:update(result.daily, result.timezone_offset)
+ table.insert(final_widget, daily_forecast_widget)
end
- end)
- weather_timer:start()
- weather_timer:emit_signal("timeout")
-
- --- Notification with weather information. Popups when mouse hovers over the icon
- local notification
- weather_widget:connect_signal("mouse::enter", function()
- notification = naughty.notify{
- icon = path_to_icons .. icon_map[resp.weather[1].icon],
- icon_size=20,
- text =
- '<big>' .. resp.weather[1].main .. ' (' .. resp.weather[1].description .. ')</big><br>' ..
- '<b>Humidity:</b> ' .. resp.main.humidity .. '%<br>' ..
- '<b>Temperature:</b> ' .. gen_temperature_str(resp.main.temp, '%.1f',
- both_units_popup) .. '<br>' ..
- '<b>Pressure:</b> ' .. resp.main.pressure .. 'hPa<br>' ..
- '<b>Clouds:</b> ' .. resp.clouds.all .. '%<br>' ..
- '<b>Wind:</b> ' .. resp.wind.speed .. 'm/s (' .. to_direction(resp.wind.deg) .. ')',
- timeout = 5, hover_timeout = 10,
- position = position,
- screen = mouse.screen,
- width = (both_units_popup == true and 210 or 200)
- }
- end)
- weather_widget:connect_signal("mouse::leave", function()
- naughty.destroy(notification)
- end)
+ weather_popup:setup({
+ {
+ final_widget,
+ margins = 10,
+ widget = wibox.container.margin
+ },
+ bg = beautiful.bg_normal,
+ widget = wibox.container.background
+ })
+ end
+
+ weather_widget:buttons(awful.util.table.join(awful.button({}, 1, function()
+ if weather_popup.visible then
+ weather_popup.visible = not weather_popup.visible
+ else
+ weather_popup:move_next_to(mouse.current_widget_geometry)
+ end
+ end)))
+
+ -- watch('cat /home/pmakhov/.config/awesome/awesome-wm-widgets/weather-widget/weather.json', 5, update_widget, weather_widget)
+ watch(string.format(GET_FORECAST_CMD, owm_one_cal_api), 5, update_widget, weather_widget)
return weather_widget
end
-return setmetatable(weather_widget, { __call = function(_, ...)
- return worker(...)
-end })
+return setmetatable(weather_widget, {__call = function(_, ...) return worker(...) end})