openweathermap script for eggdrop

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