openweathermap script for eggdrop
Recently wunderground announced the end of new registrations for "free" API access making an earlier eggdrop weather script [here] somewhat less useful. The script below attempts to replicate the functionality but using the very nice people over at openweathermap.org API.
It also makes use of a second API from from timezonedb.com to resolve the UTC times to something more meaningful for each user based on their saved location (if they have one)
You can grab the script from [here].
Script functioning looks a bit like this:
The script itself is still quite (very) rough in places but I have been testing it for a week or so. As ever any comments/suggestions are more than welcome :)
Code:
#########################################################################################
# Name m00nie::openweather
#
# Description Uses openweather API to grab some weather...based on a previous
# script that used wunderground as the API. They've sadly stopped
# free access to their api. This script should be (fairly) compatable
# with that script in terms of user info that had already been set. One
# key thing to note is the requirement of a comma between the location
# and country for this API. "Edinburgh GB" isnt valid but "Edinburgh,UK"
# is!
#
#
# Requires channel to be flagged with +openweather (.chanset #chan +openweather)
#
#
# Commands:
# !wl <format> <location>
# <format> can be 0 for Metric, 1 for Imperial
# !w <location>
# Grabs current weather in location. If no location is suuplied will
# try to use users saved location
# !wf <forecast>
# Same as !w but provides a 3 day forecast
#
#
#
# Version 1.8 - Encoding fixes....Thanks to ldm77 from #eggdrop for the pointers
# The script will now attempt to auto detect utf8 support on the
# bot and work around if this is missing.
# 1.7 - Fixing yet another rounding error under metric forcasts...
# 1.6 - Fixing bug on the temperature calculationg (Thanks to Sha Zzam)
# 1.5 - (re)Adds fuctionality to spam both metric types
# 1.4 - Handles bad results from the API better Thanks to Juho for the
# comment on what the issue was (Finnish John also thanks you).
# Also add support to accomodate zipcodes as locations (Thanks
# to E R for the info that allowed this to be supported :))
# 1.3 - Few typos and less accurate but much more readable results
# 1.2 - Fixing namespace typo! Thanks to eck0 for spotting this
# 1.1 - Fixing channel typo...and tsl package required
# 1.0 - Initial Release (This is fairly horrible in places and could
# do with a bit of cleaning up..)
#
# Website https://www.m00nie.com
#
# Notes Grab your own key @ https://openweathermap.org for weather
# and https://timezonedb.com/ for time adjustments and add to variables
# below.
#########################################################################################
namespace eval m00nie {
namespace eval openweather {
### -==[ EDIT THESE VALUES ]==- ###
# key for openweathermap.org
variable key "---GET-YOUR-OWN---"
# key from timezonedb.com
variable tkey "---GET-YOUR-OWN---"
# Set forc variable to either metric or imperial for the default metric type
# Users can overwrite this default behaviour if they save their own location
variable forc "metric"
##########################################
#### Shouldnt need to edit below here ####
package require http
package require tdom
package require tls
http::register https 443 tls::socket
bind pub - !w m00nie::openweather::current_call
bind pub - !wl m00nie::openweather::location
bind pub - !wf m00nie::openweather::forecast_call
variable version "1.8"
setudef flag openweather
::http::config -useragent "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0"
if {[encoding system] eq "utf-8"} {
variable degreec "°C"
} else {
variable degreec "\u00BAC"
}
proc current_call {nick uhost hand chan text} {
set text [string trim $text]
if {$text eq "help"} {
puthelp "PRIVMSG $nick :\002m00nie::openweather v$m00nie::openweather::version\002 - A simple weather script that collects either live weather or a three day weather forecast from http://www.openweathermap.org"
puthelp "PRIVMSG $nick :Live weather can be checked with '!w <some location>', Forecasts can be checked with '!wf <some location>'."
puthelp "PRIVMSG $nick :To save repeat typing you can also save a favourite location for yourself with '!wl <0-2> <some location>, <country code>' where 0-2 is an option to recieve results in metric (0), imperial (1) or both metric types (2). Note the comma between city and country code this is required"
puthelp "PRIVMSG $nick :For example: !wl 1 Edinburgh, GB"
puthelp "PRIVMSG $nick :After a location is saved you can simply use '!w' or '!wf' and without specifying a location your saved location will be used instead :)"
puthelp "PRIVMSG $nick :You can also query other users saved locations (if you are interested about their weather!) by specifying their nick as the location. E.g '!w John' would display the current weather for Johns saved location"
puthelp "PRIVMSG $nick : Using a zipcode as a location is also supported. If supplied without a country code this will default to the USA"
return
}
set location [verify $hand $chan $text]
if {$location != 0} {
set forc [metricpref $hand]
set tzone [tzcheck $hand $location]
current $forc $location $chan $tzone $hand
}
}
proc forecast_call {nick uhost hand chan text} {
set location [verify $hand $chan $text]
if {$location != 0} {
set forc [metricpref $hand]
set tzone [tzcheck $hand $location]
forecast $forc $location $chan $tzone $hand
}
}
# Allow a user to save their location so they can search without defining it each time
proc location {nick uhost hand chan text} {
putlog "nick: $nick, uhost: $uhost, hand: $hand"
set forc [string index $text 0]
set text [string range $text 2 end]
if {!(($forc == 0) || ($forc == 1) || ($forc == 2)) } {
puthelp "PRIVMSG $chan :Output units must be specified from 0-2 where 0 = metric, 1 = imperial and 2 = both metric types. E.g \"!wl 1 Edinburgh, GB\" would spam imperial units for Edinburgh in the UK.."
return
}
if { [string length $text] <= 0 } {
puthelp "PRIVMSG $chan :Your location seemed very short? Please try again using the syntax '!wl <metric pref(0-2)> <location>'"
return
}
if {![validuser $hand]} {
adduser $nick
set mask [maskhost [getchanhost $nick $chan]]
setuser $nick HOSTS $mask
chattr $nick -hp
putlog "m00nie::openweather::location added user $nick with host $mask"
}
setuser $hand XTRA m00nie:weather.location $text
setuser $hand XTRA m00nie:weather.forc $forc
if { $forc == 0 } {
set unit "metric"
} elseif { $forc == 1 } {
set unit "imperial"
} elseif { $forc == 2 } {
set unit "both"
} else {
puthelp "PRIVMSG $chan :You must specifiy a unit type (0 or 2) for the location"
return
}
puthelp "PRIVMSG $chan :Default weather location for \002$nick\002 set to \002$text\002 and output units set to \002$unit\002"
putlog "m00nie::openweather::location $nick set their default location to $text."
}
# Checks the users preferred metric type and sets it.
proc metricpref {hand} {
set forc [getuser $hand XTRA m00nie:weather.forc]
if { $forc == 0 } {
set forc "metric"
} elseif { $forc == 1 } {
set forc "imperial"
} elseif { $forc == 2 } {
set forc "both"
} else {
set forc $m00nie::openweather::forc
}
return $forc
}
# Confirms we have a location or tries to read the users set default location
proc verify {hand chan text} {
set text [string trimright $text]
if {(![channel get $chan openweather])} {
putlog "m00nie::openweather::search Trigger seen but channel doesnt have +openweather set!"
return 0
}
if {$text != ""} {
set location $text
if {[validuser $text] == 1} {
set location [getuser $text XTRA m00nie:weather.location]
}
} else {
set location [getuser $hand XTRA m00nie:weather.location]
}
if {[string length $location] == 0 || [regexp {[^0-9a-zA-Z,. ]} $location match] == 1} {
putlog "m00nie::openweather::search location b0rked or no location said/default? Argument: $location"
puthelp "PRIVMSG $chan :Did you ask to search somewhere? Or use !wl to set a default location"
return
} else {
return $location
}
}
# Confirm if user haz a timezone set if no return 0. Just saves an API call
proc tzcheck {hand location} {
if {[validuser $hand] == 1} {
set slocation [getuser $hand XTRA m00nie:weather.location]
if {$slocation eq $location} {
putlog "m00nie::openweather::tzcheck checking timezone for $hand as they are querying their saved location"
if {([validuser $hand] == 1) && ([string length [getuser $hand XTRA m00nie:openweather.tzone]] > 1)} {
set tzone [getuser $hand XTRA m00nie:openweather.tzone]
putlog "m00nie::openweather::tzcheck $hand has a timezone set to: $tzone"
} else {
set tzone "0"
}
} else {
set tzone "0"
}
} else {
set tzone "0"
}
return $tzone
}
# Set a user timezone if we dont have one set already (and its a lookup on the saved user location!)
proc settz {hand tzone location} {
putlog "m00nie::openweather::settz setting $hand timezone to $tzone"
if {[validuser $hand] == 1} {
set slocation [getuser $hand XTRA m00nie:weather.location]
if {$slocation eq $location} {
putlog "m00nie::openweather::settz user is querying their saved location"
setuser $hand XTRA m00nie:openweather.tzone $tzone
return
}
}
}
# Requests the current weather, reads it and outputs to the chan
proc current {forc location chan utzone hand} {
putlog "m00nie::openweather::current is running against location: $location, metric pref of $forc and timezone of $utzone for user $hand"
# Checking if we're looking for both metric types. These strings are actually used to build the URL so some dodgy (lazy) hacking here
set bchk 0
if {$forc eq "both"} {
putlog "m00nie::openweather::current BOTH metric types have been requested"
set forc "metric"
set bchk 1
}
putlog "m00nie::openweather::current bcheck is: $bchk & forc is: $forc"
set rawpage [getinfo $location weather $forc]
set doc [dom parse $rawpage]
set root [$doc documentElement]
# Check for error
set notfound [$root selectNodes /ClientError/message/text()]
if {[llength $notfound] > 0 } {
set errormsg [$notfound nodeValue]
putlog "m00nie::openweather::current ran but could not find any info for $location or an API error occured: $errormsg"
puthelp "PRIVMSG $chan :$errormsg"
return
}
if {$utzone eq "0" } {
putlog "No user location saved so lets grab the local zone"
set tz [$root selectNodes /current/city/coord]
set lat [$tz getAttribute lat]
set lng [$tz getAttribute lon]
set utzone [gettzone $lat $lng]
settz $hand $utzone $location
}
set place [$root selectNodes /current/city]
set city [$place getAttribute name]
set country [[$root selectNodes /current/city/country/text()] nodeValue]
set sun [$root selectNodes /current/city/sun]
set rise [timefix $utzone [$sun getAttribute rise] 0]
set fall [timefix $utzone [$sun getAttribute "set"] 0]
set tempr [$root selectNodes /current/temperature]
set temp [$tempr getAttribute value]
set tempu [$tempr getAttribute unit]
# Again trying to accomodate both metric types in the least horrible (but lazy) way
if {$bchk == 1} {
set tempu both
}
set temp [tunit $temp $tempu]
set humidr [$root selectNodes /current/humidity]
set humid [$humidr getAttribute value]
set humidu [$humidr getAttribute unit]
set pressr [$root selectNodes /current/pressure]
set press [$pressr getAttribute value]
set pressu [$pressr getAttribute unit]
set windr [$root selectNodes /current/wind/speed]
set wind [wunit [$windr getAttribute value] $tempu]
set windd [$windr getAttribute name]
set windi [$root selectNodes /current/wind/direction]
# Having issues with this not being returned by the API - Ideally we'll test all attributes in
# next version. Hacky fix to work in the meantime
if {[$windi hasAttribute name]} {
set windir [$windi getAttribute name]
} else {
set windir "-"
}
set weath [[$root selectNodes /current/weather] getAttribute value]
set last [timefix $utzone [[$root selectNodes /current/lastupdate] getAttribute value] 1]
set spam "Current weather for \002$city, $country\002 (Last Update: $last) \002Current conditions:\002 $weath, \002Temperature:\002 $temp, \002Wind:\002 $wind $windir ($windd), \002Humidity:\002 $humid$humidu, \002Pressure:\002 $press$pressu, \002Sunrise:\002 $rise, \002Sunset:\002 $fall"
puthelp "PRIVMSG $chan :$spam"
}
# Should take a UTC time and return some other time or format...."full" for update and hour for sunrise
proc timefix {utzone t x} {
set utc [clock scan $t -format {%Y-%m-%dT%H:%M:%S} -timezone :UTC]
if {$x == 0} {
set t [clock format $utc -format %H:%M -timezone :$utzone]
} elseif {$x == 1 } {
set t [clock format $utc -timezone :$utzone]
} else {
putlog "m00nie::openweather::timefix time formatting failed!"
error
}
return $t
}
# Takes wind speed and unit then returns single formatted string
proc wunit {wind unit} {
if {$unit eq "both"} {
putlog "m00nie::openweather::wunit attempting to format for both speed metrics"
set fwind [expr ($wind * 2.237)]
set fwind [expr {round($fwind)}]
set wind [format "%.1f" [expr ($wind * 60 * 60) / 1000]]
set wind [expr {round($wind)}]
set x "${wind}kph (${fwind}mph)"
putlog "m00nie::openweather::wunit Formatted both to: $x"
return $x
} elseif { ($unit eq "fahrenheit") || ($unit eq "imperial") } {
set unit "mph"
} elseif { ($unit eq "metric") || ($unit eq "celsius")} {
set wind [format "%.1f" [expr ($wind * 60 * 60) / 1000]]
set unit "kph"
} else {
set unit "m/s"
}
set wind [expr {round($wind)}]
set x "$wind$unit"
return $x
}
# Takes tempreture and units then returns a single formatted string
proc tunit {temp unit} {
if {$unit eq "both"} {
putlog "m00nie::openweather::tunit attempting to format for both temp metrics"
set temp [expr {round($temp)}]
set ftemp [format "%.0f" [expr ($temp * 1.8) + 32]]
set x "${temp}${m00nie::openweather::degreec} (${ftemp}F)"
putlog "m00nie::openweather::tunit Formatted both to: $x"
return $x
} elseif { ($unit eq "fahrenheit") || ($unit eq "imperial") } {
set unit "F"
} elseif { ($unit eq "metric") || ($unit eq "celsius") } {
set unit $m00nie::openweather::degreec
} else {
set unit "K"
}
set temp [expr {round($temp)}]
set x "$temp$unit"
return $x
}
# Requests the forcast page, reads it and outputs to the chan
proc forecast {forc location chan utzone hand} {
putlog "m00nie::openweather::forecast is running against location: $location, metric pref of $forc and timezone of $utzone for user $hand"
# Checking if we're looking for both metric types. These strings are actually used to build the URL so some dodgy (lazy) hacking here
set bchk 0
if {$forc eq "both"} {
putlog "m00nie::openweather::current BOTH metric types have been requested"
set forc "metric"
set bchk 1
}
putlog "m00nie::openweather::current bcheck is: $bchk & forc is: $forc"
set rawpage [getinfo $location forecast $forc]
set doc [dom parse $rawpage]
set root [$doc documentElement]
# Check for error
set notfound [$root selectNodes /ClientError/message/text()]
if {[llength $notfound] > 0 } {
set errormsg [$notfound nodeValue]
putlog "m00nie::openweather::current ran but could not find any info for $location or an API error occured: $errormsg"
puthelp "PRIVMSG $chan :$errormsg"
return
}
if {$utzone eq "0" } {
putlog "No user location saved so lets grab the local zone"
set tz [$root selectNodes /weatherdata/location/location]
set lat [$tz getAttribute latitude]
set lng [$tz getAttribute longitude]
set utzone [gettzone $lat $lng]
settz $hand $utzone $location
}
set daylist ""
array set dayl {}
# Gets nasty....Finding unique days
foreach timelist [$root selectNodes /weatherdata/forecast/time] {
set timel [$timelist getAttribute from]
set dayc [clock scan $timel -format {%Y-%m-%dT%H:%M:%S} -timezone :UTC]
set day [clock format $dayc -format %d -timezone :$utzone]
if {[lsearch -exact $daylist $day] == "-1"} {
lappend daylist $day
lappend dayl($day) $timel
} else {
lappend dayl($day) $timel
}
}
# Gets worse .... Find Highs and lows
array set highl {}
array set huml {}
array set winl {}
array set pressl {}
array set wethl {}
array set rainl {}
set loop 0
puthelp "PRIVMSG $chan :Three day forecast for \002[[$root selectNodes /weatherdata/location/name/text()] nodeValue], [[$root selectNodes /weatherdata/location/country/text()] nodeValue]\002"
foreach x $daylist {
foreach y $dayl($x) {
set high [$root selectNodes {string(/weatherdata/forecast/time[@from=$y]/temperature/@max)}]
set hum [$root selectNodes {string(/weatherdata/forecast/time[@from=$y]/humidity/@value)}]
set win [$root selectNodes {string(/weatherdata/forecast/time[@from=$y]/windSpeed/@mps)}]
set press [$root selectNodes {string(/weatherdata/forecast/time[@from=$y]/pressure/@value)}]
set rain [$root selectNodes {string(/weatherdata/forecast/time[@from=$y]/precipitation/@value)}]
lappend highl($x) $high
lappend huml($x) $hum
lappend winl($x) $win
lappend pressl($x) $press
lappend rainl($x) $rain
set utc [clock scan $y -format {%Y-%m-%dT%H:%M:%S} -timezone :UTC]
set punit [$root selectNodes {string(/weatherdata/forecast/time[@from=$y]/pressure/@unit)}]
set tunt [$root selectNodes {string(/weatherdata/forecast/time[@from=$y]/temperature/@unit)}]
set humit [$root selectNodes {string(/weatherdata/forecast/time[@from=$y]/humidity/@unit)}]
set weth [$root selectNodes {string(/weatherdata/forecast/time[@from=$y]/symbol/@name)}]
lappend wethl($x) $weth
}
# Again trying to accomodate both metric types in the least horrible (but lazy) way
if {$bchk == 1} {
set tunt both
}
set day [clock format $utc -format %A -timezone :$utzone]
set highl($x) [lsort -real $highl($x)]
set humavg [avg $huml($x)]
set pressavg [avg $pressl($x)]
set winavg [wunit [avg $winl($x)] $tunt]
set raintot [sum $rainl($x) $tunt]
if {$loop < 3} {
puthelp "PRIVMSG $chan :\002$day\002 - $weth, \002Temperature\002 High: [tunit [lindex $highl($x) end] $tunt], Low: [tunit [lindex $highl($x) 0] $tunt], \002Precipitation:\002 $raintot, \002Humidity:\002 $humavg$humit, \002Pressure:\002 $pressavg$punit, \002Avg. Wind speed:\002 $winavg"
}
incr loop
}
}
# Returns the sum of the list
proc sum {l tunt} {
# Try to calculate in inches .... 25.4
set sum 0
set l [lsearch -all -inline -not -exact $l {}]
foreach i $l {
set sum [expr $sum + $i]
}
if {$tunt eq "both"} {
set sumi [format "%.2f" [expr ($sum / 25.4)]]
set sum [expr {round($sum)}]
set x "${sum}mm ($sumi)"
} else {
set sum "[format "%.0f" $sum]mm"
return $sum
}
}
# Returns the average for the list
proc avg {l} {
set sum 0
foreach i $l {
set sum [expr $sum + $i]
}
set avg [format "%g" [expr $sum / [llength $l]]]
set avg [expr {round($avg)}]
return $avg
}
# Grabs the page and returns it!
proc getinfo {location type units} {
# I cant think of any locations with a number at the moment....Just testing for now but the API seems to handle it ok for the moment
if {[regexp -nocase {[\d]} $location]} {
putlog "m00nie::openweather::getinfo Location is a zipcode!?!"
}
regsub -all -- { } $location {%20} location
set url "https://api.openweathermap.org/data/2.5/$type?q=$location&appid=$m00nie::openweather::key&mode=xml&units=$units"
putlog "m00nie::openweather::getinfo grabbing data from $url"
for { set i 1 } { $i <= 5 } { incr i } {
set xmlpage [::http::data [::http::geturl "$url" -timeout 10000]]
if {[string length xmlpage] > 0} { break }
}
putlog "m00nie::openweather::getinfo xmlpage length is: [string length $xmlpage]"
if { [string length $xmlpage] == 0 } {
error "openweather.org returned ZERO no data :( or we couldnt connect properly"
}
return $xmlpage
}
# Get the timezone for a user from the cordinates of their saved location
proc gettzone {lat lng} {
set url "http://api.timezonedb.com/v2/get-time-zone?key=$m00nie::openweather::tkey&format=xml&by=position&lat=$lat&lng=$lng"
putlog "m00nie::openweather::gettzone grabbing data from $url"
for { set i 1 } { $i <= 5 } { incr i } {
set xmlpage [::http::data [::http::geturl "$url" -timeout 10000]]
if {[string length xmlpage] > 0} { break }
}
putlog "m00nie::openweather::gettzone xmlpage length is: [string length $xmlpage]"
if { [string length $xmlpage] == 0 } {
error "timezonedb.com returned ZERO no data :( or we couldnt connect properly"
}
set doc [dom parse $xmlpage]
set root [$doc documentElement]
# Check for error
set notfound [$root selectNodes /result/status/text()]
if { $notfound eq "FAILED" } {
set errormsg [$root selectNodes /result/status/message/text()]]
putlog "m00nie::openweather::current ran but could not find any info for $lat,$lng or an API error occured. Error: $errormsg"
puthelp "PRIVMSG $chan :$errormsg"
return
}
set tzone [[$root selectNodes /result/zoneName/text()] nodeValue]
putlog "m00nie::openweather::gettzone found timezone of $tzone"
return $tzone
}
}
}
putlog "m00nie::openweather $m00nie::openweather::version loaded"
Enjoy
m00nie