1
0

weather-darksky.tcl 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. # A weather script that uses www.darksky.net as its source. Its output is based
  2. # on incith-weather.tcl.
  3. #
  4. # It relies on two packages:
  5. # - https://github.com/horgh/geonames-tcl (for looking up latitude/longitude)
  6. # - https://github.com/horgh/darksky-tcl (for looking up weather)
  7. #
  8. # Setup:
  9. # - Create a weather-darksky.conf and put it in your Eggdrop root directory.
  10. # See weather-darksky.conf.sample for the format.
  11. # - You'll need to source both of those .tcl files in your bot prior to this
  12. # one.
  13. # - Partyline: .chanset #channel +weather-darksky
  14. # - Channel: .wz <location> for current weather or .wzf <location> for a
  15. # forecast
  16. package require darksky
  17. package require geonames
  18. namespace eval ::wds {
  19. variable output_cmd putserv
  20. variable geonames_cache [dict create]
  21. }
  22. proc ::wds::lookup_current {nick uhost hand chan argv} {
  23. if {![channel get $chan weather-darksky]} { return }
  24. set query [::wds::get_query $nick $uhost $argv]
  25. if {$query == ""} {
  26. $::wds::output_cmd "PRIVMSG $chan :Usage: .wz <location>"
  27. return
  28. }
  29. set data [::wds::get_data $chan $query]
  30. if {![dict exists $data geonames] || ![dict exists $data darksky]} {
  31. return
  32. }
  33. set geonames [dict get $data geonames]
  34. set darksky [dict get $data darksky]
  35. if {[catch {::wds::output_current $chan $geonames $darksky} error]} {
  36. $::wds::output_cmd "PRIVMSG $chan :Error: $error"
  37. }
  38. }
  39. proc ::wds::lookup_forecast {nick uhost hand chan argv} {
  40. if {![channel get $chan weather-darksky]} { return }
  41. set query [::wds::get_query $nick $uhost $argv]
  42. if {$query == ""} {
  43. $::wds::output_cmd "PRIVMSG $chan :Usage: .wzf <location>"
  44. return
  45. }
  46. set data [::wds::get_data $chan $query]
  47. if {![dict exists $data geonames] || ![dict exists $data darksky]} {
  48. return
  49. }
  50. set geonames [dict get $data geonames]
  51. set darksky [dict get $data darksky]
  52. if {[catch {::wds::output_forecast $chan $geonames $darksky} error]} {
  53. $::wds::output_cmd "PRIVMSG $chan :Error: $error"
  54. }
  55. }
  56. # Retrieve the query to look up. If none is given, return the last one we used.
  57. # If one was given, remember it.
  58. #
  59. # We only remember it if there is a matching user record. In theory we could add
  60. # users but in order to do that we must have a handle to assign them, and what
  61. # to use is unclear. I suppose we could generate something random.
  62. proc ::wds::get_query {nick uhost argv} {
  63. set query [string trim $argv]
  64. set query [string tolower $query]
  65. if {$query != ""} {
  66. ::wds::set_default_location $nick $uhost $query
  67. return $query
  68. }
  69. return [::wds::get_default_location $nick $uhost]
  70. }
  71. proc ::wds::set_default_location {nick uhost query} {
  72. set hand [finduser $nick!$uhost]
  73. if {$hand == "*"} {
  74. return
  75. }
  76. setuser $hand XTRA weather-darksky $query
  77. }
  78. proc ::wds::get_default_location {nick uhost} {
  79. set hand [finduser $nick!$uhost]
  80. if {$hand == "*"} {
  81. return ""
  82. }
  83. set location [getuser $hand XTRA weather-darksky]
  84. return $location
  85. }
  86. proc ::wds::get_data {chan query} {
  87. set conf [::wds::load_config]
  88. set geonames_result [::wds::get_lat_long $conf $query]
  89. if {[dict exists $geonames_result error]} {
  90. $::wds::output_cmd "PRIVMSG $chan :Error looking up latitude/longitude: [dict get $geonames_result error]"
  91. return [dict create]
  92. }
  93. set darksky [::darksky::new [dict get $conf darksky_key]]
  94. set darksky_result [::darksky::forecast $darksky \
  95. [dict get $geonames_result lat] [dict get $geonames_result lng]]
  96. if {[dict exists $darksky_result error]} {
  97. $::wds::output_cmd "PRIVMSG $chan :Error looking up forecast: [dict get $darksky_result error]"
  98. return [dict create]
  99. }
  100. return [dict create geonames $geonames_result darksky $darksky_result]
  101. }
  102. proc ::wds::get_lat_long {conf query} {
  103. if {[dict exists $::wds::geonames_cache $query]} {
  104. return [dict get $::wds::geonames_cache $query]
  105. }
  106. set geonames [::geonames::new [dict get $conf geonames_username]]
  107. set geonames_result {}
  108. # If the user gave us what looks like a US zip code, use the postal code
  109. # search API rather than the text search API. The text search API gives
  110. # unreliable results using zip codes alone.
  111. if {[regexp -- {\A[0-9]{5}\Z} $query]} {
  112. set geonames_result [::geonames::postalcode_latlong $geonames $query US]
  113. } else {
  114. set geonames_result [::geonames::search_latlong $geonames $query]
  115. }
  116. if {![dict exists $geonames_result error]} {
  117. dict set ::wds::geonames_cache $query $geonames_result
  118. ::wds::save_cache $conf
  119. }
  120. return $geonames_result
  121. }
  122. proc ::wds::save_cache {conf} {
  123. set fh [open [dict get $conf geonames_cache_file] w]
  124. puts -nonewline $fh $::wds::geonames_cache
  125. close $fh
  126. }
  127. proc ::wds::load_cache {} {
  128. set conf [::wds::load_config]
  129. if {![file exists [dict get $conf geonames_cache_file]]} {
  130. set ::wds::geonames_cache [dict create]
  131. return
  132. }
  133. set fh [open [dict get $conf geonames_cache_file]]
  134. set ::wds::geonames_cache [read -nonewline $fh]
  135. close $fh
  136. }
  137. proc ::wds::load_config {} {
  138. set fh [open weather-darksky.conf]
  139. set contents [read -nonewline $fh]
  140. close $fh
  141. set lines [split $contents \n]
  142. set conf [dict create]
  143. foreach line $lines {
  144. set line [string trim $line]
  145. if {$line == ""} {
  146. continue
  147. }
  148. set pieces [split $line "="]
  149. if {[llength $pieces] != 2} {
  150. putlog "weather-darksky.tcl: Invalid configuration line: $line"
  151. continue
  152. }
  153. set key [string trim [lindex $pieces 0]]
  154. set value [string trim [lindex $pieces 1]]
  155. if {$key == ""} {
  156. putlog "weather-darksky.tcl: Invalid key on line: $line"
  157. continue
  158. }
  159. if {$value == ""} {
  160. putlog "weather-darksky.tcl: Invalid value on line: $line"
  161. continue
  162. }
  163. dict set conf $key $value
  164. }
  165. if {![dict exists $conf geonames_username]} {
  166. error "no geonames_username set"
  167. }
  168. if {![dict exists $conf darksky_key]} {
  169. error "no darksky_key set"
  170. }
  171. if {![dict exists $conf geonames_cache_file ]} {
  172. error "no geonames_cache_file set"
  173. }
  174. return $conf
  175. }
  176. proc ::wds::output_current {chan geonames darksky} {
  177. set output ""
  178. append output [dict get $geonames name]
  179. append output ", "
  180. append output [dict get $geonames countryName]
  181. append output " ("
  182. append output [::wds::format_decimal [dict get $darksky latitude]]
  183. append output "°N/"
  184. append output [::wds::format_decimal [dict get $darksky longitude]]
  185. append output "°W)"
  186. append output " \002Local time\002: "
  187. append output [clock format [dict get $darksky time] \
  188. -format "%H:%M" \
  189. -timezone :[dict get $darksky timezone] \
  190. ]
  191. append output " \002Conditions\002: "
  192. append output [dict get $darksky summary]
  193. $::wds::output_cmd "PRIVMSG $chan :$output"
  194. set output ""
  195. append output "\002Temperature\002: "
  196. append output [dict get $darksky temperature]
  197. append output "°C"
  198. append output " ("
  199. append output [::wds::celsius_to_fahrenheit [dict get $darksky temperature]]
  200. append output "°F"
  201. append output ")"
  202. append output " \002Humidity\002: "
  203. append output [::wds::format_decimal [expr [dict get $darksky humidity]*100]]
  204. append output "%"
  205. append output " \002Pressure\002: "
  206. append output [dict get $darksky pressure]
  207. append output " hPa"
  208. $::wds::output_cmd "PRIVMSG $chan :$output"
  209. set output ""
  210. append output "\002Wind\002: "
  211. append output [dict get $darksky windSpeed]
  212. append output "m/s"
  213. append output " \002Clouds\002: "
  214. append output [::wds::format_decimal \
  215. [expr [dict get $darksky cloudCover]*100] \
  216. ]
  217. append output "%"
  218. $::wds::output_cmd "PRIVMSG $chan :$output"
  219. }
  220. proc ::wds::output_forecast {chan geonames darksky} {
  221. set output ""
  222. append output [dict get $geonames name]
  223. append output ", "
  224. append output [dict get $geonames countryName]
  225. append output " ("
  226. append output [::wds::format_decimal [dict get $darksky latitude]]
  227. append output "°N/"
  228. append output [::wds::format_decimal [dict get $darksky longitude]]
  229. append output "°W) "
  230. $::wds::output_cmd "PRIVMSG $chan :$output"
  231. set output ""
  232. set count 0
  233. foreach forecast [dict get $darksky forecast] {
  234. if {$count == 5} {
  235. break
  236. }
  237. if {![wds::should_show_forecast [dict get $darksky timezone] $forecast]} {
  238. continue
  239. }
  240. if {$output != ""} {
  241. append output " "
  242. }
  243. set day [clock format [dict get $forecast time] -format "%A"]
  244. append output "\002$day\002: "
  245. append output [dict get $forecast summary]
  246. append output " "
  247. append output [dict get $forecast temperatureMax]
  248. append output "/"
  249. append output [dict get $forecast temperatureMin]
  250. append output "°C"
  251. append output " ("
  252. append output [::wds::celsius_to_fahrenheit \
  253. [dict get $forecast temperatureMax] \
  254. ]
  255. append output "/"
  256. append output [::wds::celsius_to_fahrenheit \
  257. [dict get $forecast temperatureMin] \
  258. ]
  259. append output "°F"
  260. append output ")"
  261. incr count
  262. }
  263. $::wds::output_cmd "PRIVMSG $chan :$output"
  264. }
  265. # We can get old forecast info. For example if it's Sunday we could get
  266. # Saturday's. We don't want to show that.
  267. #
  268. # We do want to show today's though. The time given in the forecast is at
  269. # 00:00:00 of the day.
  270. proc ::wds::should_show_forecast {timezone forecast} {
  271. set now [clock seconds]
  272. set today [::wds::format_ymd $timezone $now]
  273. set forecast_day [::wds::format_ymd $timezone [dict get $forecast time]]
  274. return $today == $forecast_day || [dict get $forecast time] >= $now
  275. }
  276. proc ::wds::format_ymd {timezone time} {
  277. return [clock format $time \
  278. -format "%Y-%m-%d" \
  279. -timezone :$timezone \
  280. ]
  281. }
  282. proc ::wds::celsius_to_fahrenheit {celsius} {
  283. set fahrenheit [expr $celsius*9.0/5.0+32.0]
  284. return [::wds::format_decimal $fahrenheit]
  285. }
  286. proc ::wds::format_decimal {number} {
  287. return [format "%.2f" $number]
  288. }
  289. setudef flag weather-darksky
  290. bind pub -|- .wz ::wds::lookup_current
  291. bind pub -|- .wzf ::wds::lookup_forecast
  292. ::wds::load_cache
  293. putlog "weather-darksky.tcl (https://github.com/horgh/eggdrop-scripts) loaded"
  294. putlog "weather-darksky.tcl: Powered by Dark Sky (www.darksky.net)"
  295. putlog "weather-darksky.tcl: Powered by GeoNames (www.geonames.org)"