pyrtan / examples / weather / weather.toml
4,038 bytes · last changed 2026-10-26 in The catalog grows up: namespaces, publishing by pull request through the gate, gap reports gossiped back to authors; release 0.8.0
| 1 | # Weather for a place, from Open-Meteo (no key needed). Two hops: geocode |
| 2 | # the name, then fetch the forecast for the coordinates. Each hop has its |
| 3 | # own decision so a failure says which hop failed. |
| 4 | |
| 5 | name = "weather" |
| 6 | version = 2 |
| 7 | namespace = "pyrtan" |
| 8 | description = "Current weather for a place, from Open-Meteo." |
| 9 | tags = ["weather", "read-only"] |
| 10 | license = "MIT" |
| 11 | hosts = ["geocoding-api.open-meteo.com", "api.open-meteo.com"] |
| 12 | start = "geocode" |
| 13 | |
| 14 | # "forecast in Oslo" and "temperature in Oslo" read as "weather in Oslo". |
| 15 | [language] |
| 16 | synonyms = { weather = ["forecast", "temperature"] } |
| 17 | |
| 18 | [[triggers]] |
| 19 | pattern = "[what's the] weather in {place}" |
| 20 | [[triggers]] |
| 21 | pattern = "[what's the] weather for {place}" |
| 22 | [[triggers]] |
| 23 | pattern = "how [hot|cold|warm] is it in {place}" |
| 24 | |
| 25 | [inputs.place] |
| 26 | type = "str" |
| 27 | description = "A town or city name" |
| 28 | |
| 29 | [inputs.units] |
| 30 | type = "str" |
| 31 | default = "celsius" |
| 32 | one_of = ["celsius", "fahrenheit"] |
| 33 | |
| 34 | [nodes.geocode] |
| 35 | kind = "observe" |
| 36 | http = { method = "GET", url = "https://geocoding-api.open-meteo.com/v1/search?name={{ inputs.place | url }}&count=1&language=en&format=json" } |
| 37 | into = "geo" |
| 38 | extract = [ |
| 39 | { into = "hit", json = "json.results[0]" }, |
| 40 | { into = "lat", json = "json.results[0].latitude" }, |
| 41 | { into = "lon", json = "json.results[0].longitude" }, |
| 42 | { into = "name", json = "json.results[0].name" }, |
| 43 | { into = "country", json = "json.results[0].country" }, |
| 44 | ] |
| 45 | next = "found" |
| 46 | |
| 47 | [nodes.found] |
| 48 | kind = "decide" |
| 49 | on = "hit" |
| 50 | branches = [ |
| 51 | { when = { exists = true }, then = "forecast" }, |
| 52 | { else = "unknown-place" }, |
| 53 | ] |
| 54 | |
| 55 | [nodes.forecast] |
| 56 | kind = "observe" |
| 57 | http = { method = "GET", url = "https://api.open-meteo.com/v1/forecast?latitude={{ state.lat }}&longitude={{ state.lon }}¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code&temperature_unit={{ inputs.units }}" } |
| 58 | into = "fc" |
| 59 | extract = [ |
| 60 | { into = "temp", json = "json.current.temperature_2m" }, |
| 61 | { into = "humidity", json = "json.current.relative_humidity_2m" }, |
| 62 | { into = "wind", json = "json.current.wind_speed_10m" }, |
| 63 | { into = "code", json = "json.current.weather_code" }, |
| 64 | { into = "unit", json = "json.current_units.temperature_2m" }, |
| 65 | ] |
| 66 | next = "have-temp" |
| 67 | |
| 68 | [nodes.have-temp] |
| 69 | kind = "decide" |
| 70 | on = "temp" |
| 71 | branches = [ |
| 72 | { when = { type = "number" }, then = "sky" }, |
| 73 | { else = "no-forecast" }, |
| 74 | ] |
| 75 | |
| 76 | # WMO weather codes, folded into words a person would use. |
| 77 | [nodes.sky] |
| 78 | kind = "decide" |
| 79 | on = "code" |
| 80 | branches = [ |
| 81 | { when = { eq = 0 }, then = "clear" }, |
| 82 | { when = { between = [1, 3] }, then = "cloudy" }, |
| 83 | { when = { between = [45, 48] }, then = "fog" }, |
| 84 | { when = { between = [51, 67] }, then = "rain" }, |
| 85 | { when = { between = [71, 77] }, then = "snow" }, |
| 86 | { when = { between = [80, 82] }, then = "rain" }, |
| 87 | { when = { between = [85, 86] }, then = "snow" }, |
| 88 | { when = { between = [95, 99] }, then = "storm" }, |
| 89 | { else = "unknown-sky" }, |
| 90 | ] |
| 91 | |
| 92 | [nodes.clear] |
| 93 | kind = "set" |
| 94 | set = { sky = "clear" } |
| 95 | next = "reply" |
| 96 | [nodes.cloudy] |
| 97 | kind = "set" |
| 98 | set = { sky = "cloudy" } |
| 99 | next = "reply" |
| 100 | [nodes.fog] |
| 101 | kind = "set" |
| 102 | set = { sky = "foggy" } |
| 103 | next = "reply" |
| 104 | [nodes.rain] |
| 105 | kind = "set" |
| 106 | set = { sky = "raining" } |
| 107 | next = "reply" |
| 108 | [nodes.snow] |
| 109 | kind = "set" |
| 110 | set = { sky = "snowing" } |
| 111 | next = "reply" |
| 112 | [nodes.storm] |
| 113 | kind = "set" |
| 114 | set = { sky = "stormy" } |
| 115 | next = "reply" |
| 116 | [nodes.unknown-sky] |
| 117 | kind = "set" |
| 118 | set = { sky = "weather code {{ state.code }}" } |
| 119 | next = "reply" |
| 120 | |
| 121 | [nodes.reply] |
| 122 | kind = "done" |
| 123 | say = "{{ state.name }}, {{ state.country }}: {{ state.temp }}{{ state.unit }}, {{ state.sky }}, humidity {{ state.humidity }}%, wind {{ state.wind }} km/h." |
| 124 | output = { place = "{{ state.name }}", country = "{{ state.country }}", temperature = "{{ state.temp }}", unit = "{{ state.unit }}", sky = "{{ state.sky }}", humidity = "{{ state.humidity }}", wind_kmh = "{{ state.wind }}" } |
| 125 | |
| 126 | [nodes.unknown-place] |
| 127 | kind = "fail" |
| 128 | reason = "unknown-place" |
| 129 | say = "I could not find a place called {{ inputs.place }}." |
| 130 | |
| 131 | [nodes.no-forecast] |
| 132 | kind = "fail" |
| 133 | reason = "no-forecast" |
| 134 | say = "Open-Meteo found {{ state.name }} but returned no current temperature (status {{ state.fc.status }})." |