In my Home Control HabPanel the weather widget shows the forecast for today and the next four days, and is based on data from the free UK Met Office Datapoint service.
Functionality requires three elements, with processing for each residing on the home controller code on the TrueNAS server (jails), and then displayed on a tablet.
The three elements include:
1. Node-RED
Every hour, an Injection node prompts the two HTTP Request nodes to get the daily report for my location code and the regional report for my area (I only use the daily report now but left the other code in place). The HTTP request has the following format, and requires a user API key to access the data which can be obtained for free:
http://datapoint.metoffice.gov.uk/public/data/val/wxfcs/all/json/<LOCATION_CODE>?res=daily&key=<USER_API_KEY>
The JSON formatted messages are passed to two Function nodes that simply add identifiers to show which data stream is being processed, then both feed into another Function node that extracts the wanted information and builds a JSON string:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//
// Met Office JSON : extract and reformat relevant data
//
var weather_codes = [
"Clear night", // 0
"Sunny", // 1
"Partly cloudy", // 2 night
"Partly cloudy", // 3 day
"", // 4 Not used
"Mist", // 5
"Fog", // 6
"Cloudy", // 7
"Overcast", // 8
"Light rain shower",// 9 Night
"Light rain shower",// 10 Day
"Drizzle", // 11
"Light rain", // 12
"Heavy rain shower",// 13 Night
"Heavy rain shower",// 14 Day
"Heavy rain", // 15
"Sleet shower", // 16 Night
"Sleet shower", // 17 Day
"Sleet", // 18
"Hail shower", // 19 Night
"Hail shower", // 20 Day
"Hail", // 21
"Light snow shower",// 22 Night
"Light snow shower",// 23 Day
"Light snow", // 24
"Heavy snow shower",// 25 Night
"Heavy snow shower",// 26 Day
"Heavy snow", // 27
"Thunder shower", // 28 Night
"Thunder shower", // 29 Day
"Thunder" // 30
];
var days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
var daily = context.get('daily') || '';
var region = context.get('region') || '';
if (msg.topic === 1) {
// Daily weather data
var obj1 = msg.payload.SiteRep.DV.Location.Period;
// Need to get current observations
var currentTemp = obj1[0].Rep[0].Dm;
var currentHum = 75;
daily = '"temperature":' + currentTemp + ',' + '"humidity":' + currentHum + ',';
// Get daily values
var i;
for (i = 0; i < 5; i++) {
var id = parseInt(obj1[i].Rep[0].W);
var time = new Date(obj1[i].value);
daily +=
'"day' + i + '":"' + days[time.getDay()] + '",' +
'"id' + i + '":' + id + ',' +
'"cond' + i + '":"' + weather_codes[id] + '",' +
'"max' + i + '":' + obj1[i].Rep[0].Dm + ',' +
'"min' + i + '":' + obj1[i].Rep[1].Nm;
if (i < 4) {
daily += ',';
}
}
context.set('daily', daily);
}
else {
// Regional weather text
var obj2 = msg.payload.RegionalFcst.FcstPeriods.Period;
region = '"tx":[';
for (i = 0; i < 4; i++) {
switch (i) {
case 0:
region +=
'{"h":"' + obj2[0].Paragraph[1].title + '",' +
'"t":"' + strip(obj2[0].Paragraph[1].$) + '"},';
break;
case 1:
var pos;
if (obj2[0].Paragraph.length === 4)
pos = 3;
else
pos = 2;
region += '{"h":"' + obj2[0].Paragraph[pos].title + '",' +
'"t":"' + strip(obj2[0].Paragraph[pos].$) + '"},';
break;
case 2:
region += '{"h":"' + obj2[1].Paragraph.title + '",' +
'"t":"' + obj2[1].Paragraph.$ + '"},';
break;
case 3:
region += '{"h":"~","t":"~"}';
break;
}
}
region += ']';
context.set('region', region);
}
if (daily !== '' && region !== '') {
context.set('daily', '');
context.set('region', '');
// msg.payload = '{"src":"met","loc":"Horndean",' +
// daily + ',' + region + '}';
msg.payload = '{"src":"met","loc":"Horndean",' + daily + '}';
var d = new Date();
node.status({fill:'blue',shape:'dot',text:'Last update ' +
('0'+d.getHours()).slice(-2) + ':' +
('0'+d.getMinutes()).slice(-2)});
return msg; }
else {
return null;
}
// Remove unwanted max/min temp
function strip(txt) {
var pos = txt.indexOf(' Minimum Temp');
if (pos > 0)
txt = txt.substring(0, pos);
pos = txt.indexOf(' Maximum Temp');
if (pos > 0)
txt = txt.substring(0, pos);
return txt;
}
This then feeds an MQTT Out node, with topic “nodered/weather“, as a standard MQTT message.
There are a few other nodes that are used for debugging. One takes the MQTT message input, and splits the JSON into a Node-RED object to check the JSON was formatted correctly. Another injects a test weather object to the MQTT Out node to confirm operation of the code elements in OpenHab and HabPanel.
2. OpenHab Item Definitions
In OpenHab I have a weather.items file (in the /etc/openhab2/items folder) that defines number/string items that are populated by extracting the JSON values from the MQTT messages they reference, as per the following examples which show day “0”. Days “1” to “4” have very similar code.
1
2
3
4
5
6
7
8
Number Weather_Temp_Min0 "Temperature min [%.2f °C]" {mqtt="<[mqttbroker:nodred/weather:state:JSONPATH($.min0)]" }
Number Weather_Temp_Max0 "Temperature max [%.2f °C]" {mqtt="<[mqttbroker:nodered/weather:state:JSONPATH($.max0)]" }
String Weather_Forecast_Day0 "Forecast time [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" {mqtt="<[mqttbroker:nodered/weather:state:JSONPATH($.day0)]" }
String Weather_Condition0 "Condition [%s]" {mqtt="<[mqttbroker:nodered/weather:state:JSONPATH($.cond0)]" }
String Weather_ConditionId0 "ConditionId [%s]" {mqtt="<[mqttbroker:nodered/weather:state:JSONPATH($.id0)]" }
Once the item values are populated through OpenHab they can be shown in the HabPanel.
3. HabPanel Widget
The HabPanel uses code in a Template widget to display the relevant weather data as shown below. It uses a set of SVG weather icons that I made minor amendments to match my colour scheme. The initialisation of ServerPath at the start of the code, allows access to the icons which are in a folder on the webserver on the home controller (TrueNAS jail). Whilst some CSS items are defined locally in the widget, there is also a custom.css file on the server which has some general CSS defines. (I still need to tidy the CSS for consistency.) The /static/ element references the /etc/openhab2/html folder on the home controller where these files are located.
The Item variables in OpenHab are accessed using the *itemValue(‘
Note re Sizing: all sized elements in my HabPanel code use “vw” units rather than “px” so that I see the same scaled view whether it is opened in HabPanel on my 10″ tablet, or my 27″ monitors. Hence the icon and text for “today” can be shown large, whilst for the upcoming days it is scaled down appropriately.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<div ng-init="ServerPath='/static/weather'"></div>
<style>
.day:first-letter {
text-transform: uppercase;
}
.condition {
font-weight: 300;
}
img.center {
display: block;
margin-top: 0.6vw;
margin-left: auto;
margin-right: auto;
}
.weather-icon {
width: 18vw;
height: 18vw;
}
.weather-temp {
font-weight: 400;
}
</style>
<div class="rounded-box">
<table>
<tr>
<td><img class="weather-icon" src="{{ServerPath}}/{{itemValue('Weather_ConditionId0')}}.svg" /></td>
<td class="weather-temp">
<table width="100%" style="text-align:center">
<tr><td style="font-size:3.6vw; color:#ffffff"><span class="weather-hitemp">{{'%.0f' | sprintf:itemValue('Weather_Temp_Max0')}}</span> <span style="font-weight:100;">/</span> <span class="weather-lotemp">{{'%.0f' | sprintf:itemValue('Weather_Temp_Min0')}}</span> <span style="font-weight:100;">°C</span></td></tr>
<tr><td style="font-size:2.4vw; text-align:center; color:#ffffff">{{itemValue('Weather_Condition0')}}</td></tr>
</table>
</td>
</tr>
</table>
<table width="100%" style="text-align:center; font-weight:300">
<tr class="day">
<td colspan="2" style="padding:0.8vw; border-top: 1px solid #ffffff">Sunrise: {{itemValue('Sunrise') | date:'HH:mm'}}</td>
<td colspan="2" style="border-top: 1px solid #ffffff">Sunset: {{itemValue('Sunset') | date:'HH:mm'}}</td>
</tr>
<tr class="day">
<td width="25%" style="border: 1px solid #ffffff; border-left:0">{{itemValue('Weather_Forecast_Day1')}}</td>
<td width="25%" style="border: 1px solid #ffffff">{{itemValue('Weather_Forecast_Day2')}}</td>
<td width="25%" style="border: 1px solid #ffffff">{{itemValue('Weather_Forecast_Day3')}}</td>
<td width="25%" style="border: 1px solid #ffffff; border-right:0">{{itemValue('Weather_Forecast_Day4')}}</td>
</tr>
<tr>
<td style="border-right: 1px solid #ffffff"><img class="weather-forecast-icon center" src="{{ServerPath}}/{{itemValue('Weather_ConditionId1')}}.svg"/></td>
<td style="border-right: 1px solid #ffffff"><img class="weather-forecast-icon center" src="{{ServerPath}}/{{itemValue('Weather_ConditionId2')}}.svg"/></td>
<td style="border-right: 1px solid #ffffff"><img class="weather-forecast-icon center" src="{{ServerPath}}/{{itemValue('Weather_ConditionId3')}}.svg"/></td>
<td><img class="weather-forecast-icon center" src="{{ServerPath}}/{{itemValue('Weather_ConditionId4')}}.svg"/></td>
</tr>
<tr style="font-weight:300">
<td style="border-right: 1px solid #ffffff">{{itemValue('Weather_Condition1')}}</td>
<td style="border-right: 1px solid #ffffff">{{itemValue('Weather_Condition2')}}</td>
<td style="border-right: 1px solid #ffffff">{{itemValue('Weather_Condition3')}}</td>
<td>{{itemValue('Weather_Condition4')}}</td>
</tr>
<tr>
<td class="weather-temps" style="border-right: 1px solid #ffffff"><span class="weather-hitemp">{{'%.0f' | sprintf:itemValue('Weather_Temp_Max1')}}</span> / <span class="weather-lotemp">{{'%.0f' | sprintf:itemValue('Weather_Temp_Min1')}}</span> °C</td>
<td class="weather-temps" style="border-right: 1px solid #ffffff"><span class="weather-hitemp">{{'%.0f' | sprintf:itemValue('Weather_Temp_Max2')}}</span> / <span class="weather-lotemp">{{'%.0f' | sprintf:itemValue('Weather_Temp_Min2')}}</span> °C</td>
<td class="weather-temps" style="border-right: 1px solid #ffffff"><span class="weather-hitemp">{{'%.0f' | sprintf:itemValue('Weather_Temp_Max3')}}</span> / <span class="weather-lotemp">{{'%.0f' | sprintf:itemValue('Weather_Temp_Min3')}}</span> °C</td>
<td class="weather-temps"><span class="weather-hitemp">{{'%.0f' | sprintf:itemValue('Weather_Temp_Max4')}}</span> / <span class="weather-lotemp">{{'%.0f' | sprintf:itemValue('Weather_Temp_Min4')}}</span> °C</td>
</tr>
</table>
</div>
References / Info
- Met Office Datapoint: Getting started
- Met Office Datapoint: Google group (datapoint users discussion)
- Node-RED: Writing Functions
- OpenHab: Item definitions
- OpenHab: Extracting values with the JsonPath transformation
- HabPanel introduction
- HabPanel Template Widget tutorial