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  
#                can be 0 for Metric, 1 for Imperial
#           !w 
#               Grabs current weather in location. If no location is suuplied will
#               try to use users saved location
#           !wf 
#               Same as !w but provides a 3 day forecast
#
# Version               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.
#########################################################################################
namespace eval m00nie {
    namespace eval openweather {
        package require http
        package require tdom
        package require tls
        tls::init -tls1 true -ssl2 false -ssl3 false
        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.2"
        # 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"
        # Set below to 1 to add hourly info for the next few hours to each current weather report
        # (note this adds an extra API call to each query so on a busy chan you may it API limits)
        setudef flag openweather
        # key for openweathermap.org
        variable key "---GET-YOUR-OWN---"
        # Sadly at the moment openweather doesnt pass the timezone in the API (although the do have a
        # placeholder to do so). In the meantime you can enable an extra api to lookup the timezone
        # of the users saved location by signing up to https://timezonedb.com/.
        variable tkey "---GET-YOUR-OWN---"
        ::http::config -useragent "Mozilla/5.0 (X11; CrOS x86_64 10575.58.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36"


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 ', Forecasts can be checked with '!wf '."
        puthelp "PRIVMSG $nick :To save repeat typing you can also save a favourite location for yourself with '!wl <0-1> , ' where 0-1 is an option to recieve results in metric (0) or imperial (1). Note the comma between city and country code this is required"
        puthlp "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"
        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-1 where 0 = metric and 1 = imperial. 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  '"
        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"
    } else {
      puthelp "PRIVMSG $chan :You must specifiy a unit type (0 or 1) 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"
    } 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"
    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]
    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 windir [[$root selectNodes /current/wind/direction] getAttribute name]

    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 "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 x "$wind$unit"
        return $x
}


# Takes tempreture and units then returns a single formatted string
proc tunit {temp unit} {
    if { ($unit eq "fahrenheit") || ($unit eq "imperial") } {
                set unit "F"
        } elseif { ($unit eq "metric") || ($unit eq "celsius") } {
                set unit "Β°C"
        } else {
                set unit "K"
        }
    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"
    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 forcast 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
        }
                set day [clock format $utc -format %A -timezone :$utzone]
        set highl($x) [lsort $highl($x)]
        set humavg [avg $huml($x)]
        set pressavg [avg $pressl($x)]
        set winavg [wunit [avg $winl($x)] $tunt]
        set raintot [sum $rainl($x)]
        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 lost
proc sum {l} {
    set sum 0
    set mm "mm"
    set l [lsearch -all -inline -not -exact $l {}]
    foreach i $l {
        set sum [expr $sum + $i]
    }
    set sum [expr {round($sum)}]
    set sum "$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} {
    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

Comments

  • Thank you for coming up with a new script this is awesome.

    I have implemented the script however i am unable to get it to work here are the errors i get:
    Tcl error [m00nie::openweather::current_call]: Unknown channel setting.

    I was able to set my default location no problem. However when i type !w or !wf i get that error.

    I inserted my api key for openweather as instructed in the script and the same for the timezone api key as well. For obvious reason i omitted the actually key and just wrote “mykey” so i can post here

    # key for openweathermap.org
    variable key “mykey”
    # Sadly at the moment openweather doesnt pass the timezone in the API (although the do have a
    # placeholder to do so). In the meantime you can enable an extra api to lookup the timezone
    # of the users saved location by signing up to https://timezonedb.com/.
    variable tkey “mykey”

    Thank you for your help

  • Hi Skeletor

    I’ve uploaded v1.1 that corrects what I hope was the problem around me being lazy and not editing the chanset check text properly for this script :p
    Hope its working now for you
    Cheers

    m00nie

  • Hi Tom

    My mistake I keep forgetting I load the TLS package elsewhere on my own bots. I’ve added it as a required package now in this script so hopefully it should be working for you now πŸ™‚
    Cheers

    m00nie

  • Not sure what happened to my other comment but doesn’t seem to be showing up.

    Thank you for updating the script so fast. I used the new version and it works perfectly. I am now running it on 2 bots. Thanks again m00nie.

  • Hey, thanks for all the scripts! I’m getting an error as below with v1.1:

    [07:18:02] m00nie::openweather::current is running against location: london,UK, metric pref of metric and timezone of 0 for user vel
    [07:18:02] m00nie::openweather::getinfo grabbing data from https://api.openweathermap.org/data/2.5/weather?q=london,UK&appid=&mode=xml&units=metric
    [07:18:02] m00nie::openweather::getinfo xmlpage length is: 752
    [07:18:02] No user location saved so lets grab the local zone
    [07:18:02] m00nie::openweather::gettzone grabbing data from http://api.timezonedb.com/v2/get-time-zone?key=&format=xml&by=position&lat=51.51&lng=-0.13
    [07:18:07] m00nie::openweather::gettzone xmlpage length is: 300
    [07:18:07] Tcl error [m00nie::openweather::current_call]: invalid command name “”

  • Tcl error [m00nie::openweather::current_call]: can’t read “m00nie::weather::forc”: no such variable
    happens when forc is set to metric or imperial. dont understand :< need more info or such let me know
    thank you for all you code!

  • Hey Felon,

    I found that error also! It’s even there in your error message after I realized it. The naming for the namespace is wrong when setting one of the $forc variables….”set forc m00nie::weather::forc”. It should be “set forc m00nie::openweather::forc” in line 128 of the code.

  • line 128 in else statement should be “set forc $m00nie::openweather::forc” because $m00nie::weather::forc is the wrong naming for the namespace.

Leave a Reply

Your email address will not be published. Required fields are marked *