WARNING: Wunderground has eliminated their free API keys. If you already have an API key it will still work for some unknown amount of time but if you don’t already have a key this tutorial will not work for you.
This tutorial will illustrate how I currently use the HTTP binding to get weather information from Wunderground, bypassing the various weather bindings which, if the postings on the forum are any indication, are a bit flakey and provide incomplete information.
You could apply this to other weather services, though the JSONPATH or XPATH transforms will likely be different.
Note that the snazzy weather webview that one can configure to use with the Weather Binding is not supported with this approach.
Prerequisites
Obtain an API key from Wunderground..
Lat/Lon
There are two ways you can specify your location in the API calls. The first is via latitude/longitude which will have Wunderground choose the weather station closest to you. Determine your latitude and longitude. Depending on how variable your weather may be, using the automatically discovered lat/lon by the Astro Binding may work well.
Station ID
The second lets you specify the weather station yourself. To do this search for your location and select “Change Station”
Select the station you desire to pull from and click on it. When the page comes up look at the URL. It will end with ?query=pws:<station id>
where <station id>
is the unique identifier for that station. We will need this later.
Create the Query URLs
The format of the Wunderground API call URLs is documented here and takes the form of:
http://api.wunderground.com/api/<Your_Key>/<features>/<settings>/q/<query>.<format>
Where the parts of the URL in < >
get replaces with your specific information. For our purposes:
-
features you will likely care about are conditions and forecast or forecast10day but pay attention to the rest, there is a wealth of information available (NOTE: not all features are available for the free tier including forecast10day)
-
settings is where you will set the language and where you can enable the use of personal weather stations in the case where you want to include or specify a weather station that is controlled by an individual for current conditions
-
query is where you use the lat/lon obtained above or the specific weather station ID or one of the other documented options (e.g. city name, zip code, air port code, etc. For example, to use a lat/lon
<query>
would become<lat>,<lon>
(e.g.37.8,-122.4
). To use a weather station usepws:<id>
(e.g.pws:KCASANFR70
). -
format can be either
json
orxml
; for this tutorial we will be usingjson
Now that you are armed with the URL info you need let’s set up OH.
NOTE: See @binderth’s post below for how to get support for localization (language and units).
HTTP Binding Config
Install the HTTP binding.
We will be using the caching configuration because we don’t want to make a new call to the Wunderground API for every Item we define. The free tier limits one to 500 API calls a day so take this into account when setting the updateInterval. Note that each feature that you pull down from Wunderground counts as a separate API call so the more features you desire the less frequently you can poll for the data without paying.
Edit services/http.cfg
and add the following two lines for each feature you will be using from Wunderground:
<name>.url=http://api.wunderground.com/api/<Your Key>/<feature>/q/<query>.json
<name>.updateInterval=<interval in msec>
Replace all the parts in < >
with your appropriate values.
With 500 calls per day this means you can make an API call roughly every three minutes, or 180000 msec, to stay within your limits.
For example, if I were to query for conditions and forecast I will have:
weatherConditions.url=http://api.wunderground.com/api/<Your API>/conditions/q/<query>.json
weatherConditions.updateInterval=360000
weatherForecast.url=http://api.wunderground.com/api/<Your API/forecast/q/<query>.json
weatherForecast.updateInterval=360000
Things
There are no Things required as the HTTP binding is a 1.x version binding and does not support Things. When/if a 2.x version of the HTTP binding gets built I will try to remember and come back and revise this tutorial accordingly.
Items
For this tutorial we are pulling the API data down as JSON so the JSONPATH Transformation must be installed. Also, because Wunderground puts the %
after the humidity value, the RegEx Transformation](http://docs.openhab.org/addons/transformations/regex/readme.html) must also be installed.
The following Items are not comprehensive. See the Conditions, Forecast, etc. documentation for the full list of the data returned and the format of the data. There is even a link at the bottom that generates an example return for San Francisco.
Number vWeather_Temp "Outside Temp [%.0f °F]"
<temperature> (gChart)
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.temp_f)]" }
Number vWeather_Temp_Max "Max Outside Temp [%.0f °F]"
<temperature>
Number vWeather_Temp_Min "Min Outside Temp [%.0f °F]"
<temperature>
String vWeather_Conditions "Current Conditions [%s]"
<wunderground>
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.weather)]" }
Switch vIsCloudy "Sunlight"
<iscloudy>
Number vWeather_Humidity "Outside Humidity [%d%%]"
<humidity> (gChart)
{ http="<[weatherConditions:360000:REGEX(.*\"relative_humidity\":\"(.*)%\".*)]" }
String vWeather_Wind "Wind [%s]"
<wind>
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.wind_string)]" }
Number vWeather_Wind_MPH "Wind Speed [%.1f MPH]"
(gChart)
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.wind_mph)]" }
Number vWeather_Wind_Gusts "Wind Gusts [%.1f MPH]"
(gChart)
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.wind_gust_mph)]" }
Number vWeather_Pressure "Barometric Pressure [%.1f in]"
<line> (gChart)
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.pressure_in)]" }
String vWeather_Pressure_Trend "Barometric Pressure Trend [MAP(weather.map):%s]"
<line>
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.pressure_trend)]" }
Number vWeather_Dewpoint "Dew Point [%.1f °F]"
<temperature> (gChart)
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.dewpoint_f)]" }
Number vWeather_Feel "Feels like [%.0f °F]"
<temperature> (gChart)
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.feelslike_f)]" }
Number vWeather_Visibility "Visibility [%.1f miles]"
<sun_clouds>
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.visibility_mi)]" }
Number vWeather_SolarRad "Solar Radiation [%d]"
<sun>
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.solarradiation)]" }
Number vWeather_UV "UV Index [JS(uv.js):%s]"
<sun>
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.UV)]" }
Number vWeather_PrecipToday "Precipitation So Far [%.1f in]"
<rain> (gChart)
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.precip_today_in)]" }
String vWeather_Today_Conditions "Today Conditions [%s]"
<wunderground>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[0].conditions)]" }
Number vWeather_Today_TempHigh "Today High [%.0f °F]"
<temperature>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[0].high.fahrenheit)]" }
Number vWeather_Today_TempLow "Today Low [%.0f °F]"
<temperature>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[0].low.fahrenheit)]" }
Number vWeather_Today_QPF "Today Precip QPF [%.1f in]"
<rain>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[0].qpf_allday.in)]" }
Number vWeather_Today_Snow "Today Snow [%.1f in]"
<climate-on>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[0].snow_allday.in)]" }
String vWeather_Tomorrow_Conditions "Tomorrow Conditions [%s]"
<wunderground>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[1].conditions)]" }
Number vWeather_Tomorrow_TempHigh "Tomorrow High [%.0f °F]"
<temperature>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[1].high.fahrenheit)]" }
Number vWeather_Tomorrow_TempLow "Tomorrow Low [%.0f °F]"
<temperature>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[1].low.fahrenheit)]" }
Number vWeather_Tomorrow_QPF "Tomorrow Precip QPF [%.1f in]"
<rain>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[1].qpf_allday.in)]" }
Number vWeather_Tomorrow_Snow "Tomorrow Snow [%.1f in]"
<climate-on>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[1].snow_allday.in)]" }
Notes:
- I use the Group Based Persistence Design Pattern to designate those Items I want to chart using InfluxDB+Grafana.
- I track the daily min and max and current cloudy conditions using Unbound Items.
- I created a set of dynamic icons that correspond with the Conditions Item, more on this below
- The refresh interval on the Items Binding config matches those set in the http.cfg but they don’t have to. They can poll more frequently but they will be pulling the same information from the cache for some of those polls. I usually choose to use the same value or the value in
http.cfg/2
(e.g. in this case 180,000) which makes sure that an Item doesn’t have to wait as long in the case that the Item polls just before the binding pulls down new data. - I use a MAP transform to convert the values for barometric pressure’s direction from +, -, 0 to nice human readable words.
Each of the HTTP Items except for humidity uses a JSONPATH transformation to extract the desired value for that Item. Humidity uses REGEX in order to strip off the %
from the value so it can be assigned to a Number Item. Theoretically one could use REGEX for all the Items but the JSONPATH is more self documented.
Icons
Wunderground provides ten sets of conditions icons. Unfortunately, they are in gif format and I couldn’t find any licensing information. So I’m afraid you will have to go through the tedious process of converting these icons into png or svg and creating a version of each to cover all of the possible conditions (the weather.map file below will help).
Brute Force
The procedure I followed was to right click to download the icon files and copied them to conf/icons/classic
. I used the mogrify command line utility (comes with Ubuntu 17, ymmv) to convert them into png files. I don’t know if mogrify supports svg. The full command I used was:
mogrify -format png *.gif
Then I started the tedious process of creating all the dynamic icons versions for all the Wundergound conditions. And I’m certain I haven’t covered them all yet, particularly for the forecast conditions like “Chance of Rain” and such.
Make sure to create a default wunderground.png
which will be chosen when no other icon matches. I made a copy of the error icon from the default OH icon set.
Then make a copy of the desired icon for each condition listed in weather.map
below. Copy over all of the “Chance of” icons to a wunderground version. For example “chancesnow.png” would be copied to wunderground-chance\ of\ snow.png
. Note that the icon must be all lower case. Use escaped spaces in the names for Conditions that include spaces (which is almost all of them). You will end up with a list of icons that looks something like:
On Demand
At some point I might figure out if it is feasible to download the icons on demand from wundergound (they provide the icon url in their JSON data) and convert them to png as needed. This could be done with something like the following, which won’t work for me because I run in Docker so the Exec binding isn’t much use to me and I don’t want to figure out how to convert a gif to a png in Rules.
Items:
String vWeather_Conditions_Icon
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.icon_url)]"}
String vWeather_Today_Conditions_Icon
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[0].icon_url)]" }
String vWeather_Tomorrow_Conditions_Icon
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[1].icon_url)]" }
String vWeather_Conditions "Current Conditions [%s]"
<currcond>
{ http="<[weatherConditions:360000:JSONPATH($.current_observation.weather)]" }
String vWeather_Today_Conditions "Today Conditions [%s]"
<todaycond>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[0].conditions)]" }
String vWeather_Tomorrow_Conditions "Tomorrow Conditions [%s]"
<tomorrowcond>
{ http="<[weatherForecast:360000:JSONPATH($.forecast.simpleforecast.forecastday[1].conditions)]" }
Note the change of the icons for the Conditions Items
Rules:
import org.eclipse.xtext.xbase.lib.Functions
val Functions$Function2<String, String, Boolean> getIcon = [ url, path |
executeCommandLine("wget -O " + path + " " + url, 10000)
executeCommandLine("mogrify -format png " + path, 10000)
]
rule "Download current conditions icon"
when
Item vWeather_Conditions_Icon changed
then
getIcon.apply("/opt/openhab2/conf/icons/classic/currcond.gif", vWeather_Conditions_Icon.state.toString)
end
rule "Download today's conditions icon"
when
Item vWeather_Today_Conditions_Icon changed
then
getIcon.apply("/opt/openhab2/conf/icons/classic/todaycond.gif", vWeather_Today_Conditions_Icon.state.toString)
end
rule "Download tomorrow's conditions icon"
when
Item vWeather_Tomorrow_Conditions_Icon changed
then
getIcon.apply("/opt/openhab2/conf/icons/classic/tomorrowcond.gif", vWeather_Tomorrow_Conditions_Icon.state.toString)
end
Notes:
- I don’t know how well this works with browser caching. I think it should work but I could see the icon not updating without a hard reload of the page. If you try it out please let me know and I’ll update accordingly.
- I just typed in the above. It likely contains errors. Let me know if you get it to work.
Cached (PREFERRED)
A third approach that works even within the Docker is to pre-download the iconset from wunderground and then use a Rules to copy the icons to the proper filename as they are needed.
This time place the icons that have been converted from gif to png in an out of the way folder. I chose conf/icons/classic/wunderground
. Leave their names unchanged.
Items:
Same Items as listed above under “On Demand”
Rules:
The rules are largely the same as above only now we parse the icon we care about out of the Icon Item and copy that file to the proper wunderground named file.
import org.eclipse.xtext.xbase.lib.Functions
val Functions$Function2<String, String, Boolean> copyIcon = [ url, cond |
val parts = url.split("/")
val icon = parts.get(parts.size-1).replace("gif", "png")
var source = "/openhab/conf/icons/classic/wunderground/"+icon
var dest = "/openhab/conf/icons/classic/wunderground-"+cond.toLowerCase+".png"
var command = "cp@@" + source + "@@" + dest
var results = executeCommandLine(command, 10000)
if(results != "") {
logError("weather", "Failed to copy over icon file: " + command + "\n" + results)
false
}
else true
]
rule "Copy current conditions icon"
when
Item vWeather_Conditions_Icon received update
then
copyIcon.apply(vWeather_Conditions_Icon.state.toString, vWeather_Conditions.state.toString)
end
rule "Copy today conditions icon"
when
Item vWeather_Today_Conditions_Icon received update
then
copyIcon.apply(vWeather_Today_Conditions_Icon.state.toString, vWeather_Today_Conditions.state.toString)
end
rule "Copy tomorrow conditions icon"
when
Item vWeather_Conditions_Icon received update
then
copyIcon.apply(vWeather_Tomorrow_Conditions_Icon.state.toString, vWeather_Tomorrow_Conditions.state.toString)
end
This approach is a little more full proof than the previous approaches and is my preferred approch.
Rules
The rules I have set up to process the weather info are pretty simple. I have one rule to calculate the day’s min and max temperature and populate those Items and another that determines whether it is cloudy which is an application of the Separation of Behaviors Design Pattern.
rule "Update min and max temps"
when
Time cron "1 0 0 * * ? *" or
Item vWeather_Temp changed
then
vWeather_Temp_Min.postUpdate(vWeather_Temp.minimumSince(now.withTimeAtStartOfDay, "influxdb").state)
vWeather_Temp_Max.postUpdate(vWeather_Temp.maximumSince(now.withTimeAtStartOfDay, "influxdb").state)
end
rule "Is it cloudy outside?"
when
Item vWeather_Conditions changed
then
logInfo(logName, "New weather conditions: " + vWeather_Conditions.state.toString)
val isCloudy = transform("MAP", "weather.map", vWeather_Conditions.state.toString)
val newState = if(isCloudy == null || isCloudy == "false") OFF else ON
if(newState != vIsCloudy.state) {
logInfo(logName, "Setting isCloudy to " + newState.toString)
vIsCloudy.postUpdate(newState)
}
end
Theory of operation for “Update min and max temps”: Whenever the observed temperature changes and at one second after midnight the rule triggers. The rule pulls the minimumSince midnight from influxdb and assigns it to the Min item and does the same for the max.
Theory of operation for “Is it cloudy outside?”: I use this rule to set a flag used by my lighting rules to turn on/off certain lamps when it is cloudy. I placed all possible weather condition strings into a map file which transforms each to either a “true” or a “false” indicating whether that condition should be treated as cloudy or not. I transform the current conditions using this map file and if the transform returns “false” or null I set isCloudy to OFF, otherwise I set it to ON.
weather.map
NULL=unknown
# Pressure: Note, if you have uninitialzed Items they may be transformed to "falling"
-=falling
+=rising
0=steady
# Cloudy or not
Light\ Drizzle=true
Heavy\ Drizzle=true
Drizzle=true
Light\ Rain=true
Heavy\ Rain=true
Rain=true
Light\ Snow=true
Heavy\ Snow=true
Snow=true
Light\ Snow\ Grains=true
Heavy\ Snow\ Grains=true
Snow\ Grains=true
Light\ Ice\ Crystals=true
Heavy\ Ice\ Crystals=true
Ice\ Crystals=true
Light\ Ice\ Pellets=true
Heavy\ Ice\ Pellets=true
Ice\ Pellets=true
Light\ Hail=true
Heavy\ Hail=true
Hail=true
Light\ Mist=true
Heavy\ Mist=true
Mist=true
Light\ Fog=true
Heavy\ Fog=true
Fog=true
Light\ Fog\ Patches=true
Heavy\ Fog\ Patches=true
Fog\ Patches=true
Light\ Smoke=false
Heavy\ Smoke=true
Smoke=true
Light\ Volcanic\ Ash=false
Heavy\ Volcanic\ Ash=true
Volcanic\ Ash=true
Light\ Widespread\ Dust=false
Heavy\ Widespread\ Dust=true
Widespread\ Dust=true
Light\ Sand=false
Heavy\ Sand=true
Sand=true
Light\ Haze=false
Heavy\ Haze=true
Haze=false
Light\ Spray=false
Heavy\ Spray=false
Spray=false
Light\ Dust\ Whirls=false
Heavy\ Dust\ Whirls=true
Dust\ Whirls=false
Light\ Sandstorm=false
Heavy\ Sandstorm=true
Sandstorm=true
Light\ Low\ Drifting\ Snow=true
Heavy\ Low\ Drifting\ Snow=true
Low\ Drifting\ Snow= true
Light\ Low\ Drifting\ Widespread\ Dust=true
Heavy\ Low\ Drifting\ Widespread\ Dust=true
Low\ Drifting\ Widespread\ Dust=true
Light\ Low\ Drifting\ Sand=true
Heavy\ Low\ Drifting\ Sand=true
Low\ Drifting\ Sand=true
Light\ Blowing\ Snow=true
Heavy\ Blowing\ Snow=true
Blowing\ Snow=true
Light\ Blowing\ Widespread\ Dust=true
Heavy\ Blowing\ Widespread\ Dust=true
Blowing\ Widespread\ Dust=true
Light\ Blowing\ Sand=true
Heavy\ Blowing\ Sand=true
Blowing\ Sand=true
Light\ Rain\ Mist=true
Heavy\ Rain\ Mist=true
Rain\ Mist=true
Light\ Rain\ Showers=true
Heavy\ Rain\ Showers=true
Rain\ Showers=true
Light\ Snow\ Showers=true
Heavy\ Snow\ Showers=true
Snow\ Showers=true
Light\ Snow\ Blowing\ Snow\ Mist=true
Heavy\ Snow\ Blowing\ Snow\ Mist=true
Snow\ Blowing\ Snow\ Mist=true
Light\ Ice\ Pellet\ Showers=true
Heavy\ Ice\ Pellet\ Showers=true
Ice\ Pellet\ Showers=true
Light\ Hail\ Showers=true
Heavy\ Hail\ Showers=true
Hail\ Showers=true
Light\ Small\ Hail\ Showers=true
Heavy\ Small\ Hail\ Showers=true
Small\ Hail\ Showers=true
Light\ Thunderstorm=true
Heavy\ Thunderstorm=true
Thunderstorm=true
Light\ Thunderstorms\ and\ Rain=true
Heavy\ Thunderstorms\ and\ Rain=true
Thunderstorms\ and\ Rain=true
Light\ Thunderstorms\ and\ Snow=true
Heavy\ Thunderstorms\ and\ Snow=true
Thunderstorms\ and\ Snow=true
Light\ Thunderstorms\ and\ Ice\ Pellets=true
Heavy\ Thunderstorms\ and\ Ice\ Pellets=true
Thunderstorms\ and\ Ice\ Pellets=true
Light\ Thunderstorms\ with\ Hail=true
Heavy\ Thunderstorms\ with\ Hail=true
Thunderstorms\ with\ Hail=true
Light\ Thunderstorms\ with\ Small\ Hail=true
Heavy\ Thunderstorms\ with\ Small\ Hail=true
Thunderstorms\ with\ Small\ Hail=true
Light\ Freezing\ Drizzle=true
Heavy\ Freezing\ Drizzle=true
Freezing\ Drizzle=true
Light\ Freezing\ Rain=true
Heavy\ Freezing\ Rain=true
Freezing\ Rain=true
Light\ Freezing\ Fog=true
Heavy\ Freezing\ Fog=true
Freezing\ Fog=true
Patches\ of\ Fog=true
Shallow\ Fog=true
Partial\ Fog=true
Overcast=true
Clear=false
Partly\ Cloudy=true
Mostly\ Cloudy=true
Scattered\ Clouds=false
Small\ Hail=true
Squalls=true
Funnel\ Cloud=true
Unknown\ Precipitation=true
Unknown=false
Sitemap
There is nothing special about the sitemap. For completeness, here is the weather section of my current one.
Text item=vWeather_Conditions {
Frame item=vTimeOfDay {
Text item=vMorning_Time
Text item=vSunrise_Time
Text item=vEvening_Time
Text item=vSunset_Time
Text item=vNight_Time
Text item=vBed_Time
}
Frame item=vWeather_Conditions {
Text item=vIsCloudy
Text item=vWeather_Temp
Text item=vWeather_Temp_Max
Text item=vWeather_Temp_Min
Text item=vWeather_Feel
Text item=vWeather_Dewpoint
Text item=vWeather_Conditions
Text item=vWeather_Humidity
Text item=vWeather_PrecipToday
Text item=vWeather_Wind
Text item=vWeather_Pressure_Trend
Text item=vWeather_Visibility
Text item=vWeather_SolarRad
Text item=vWeather_UV
}
Frame label=Forecast {
Text item=vWeather_Today_Conditions
Text item=vWeather_Tomorrow_Conditions
Text item=vWeather_Today_TempHigh
Text item=vWeather_Tomorrow_TempHigh
Text item=vWeather_Today_TempLow
Text item=vWeather_Tomorrow_TempLow
Text item=vWeather_Today_QPF
Text item=vWeather_Tomorrow_QPF
Text item=vWeather_Today_Snow
Text item=vWeather_Tomorrow_Snow
}
}