Building a live page
weewx-loopdata manual · weewx-loopdata on GitHub · Report an issue
The recipe for using LoopData in your own skin, demonstrated in full by the included sample skin (skins/LoopData):
Using the SteelSeries Weather Gauges? Wiring them to LoopData is straightforward — every value the gauges read is a field you can declare here — but the javascript that maps one to the other is yours to write and yours to maintain. If you are not a javascript programmer, use Gary Roderick’s Realtime gauge-data extension instead: it writes the gauge-data.txt the gauges expect, with no javascript from you. Gary has left GitHub and taken his repository with him; a copy of the last version is at hoetzgit/weewx-realtime_gauge-data.
1. Declare the fields your page needs
In your skin’s skin.conf, declare every field the page needs, in named groups:
[LoopData]
[[fields]]
temperature = current.outTemp, day.outTemp.max
almanac = almanac.sunset
The field reference documents everything available; Declaring fields is the declaration in full. Your report is its own target: the values arrive already in its units, formatting and language, under its name in the file.
2. Put the file where the page can fetch it
Set loop_data_dir/filename so the json file lands somewhere your web server serves. By default loop_data_dir is relative to the sample report’s HTML directory, so a page in that directory can fetch the file with a relative URL, and a page anywhere else in the reports tree with a relative path to it. The file can live outside your reports tree instead — on a memory filesystem, say — in which case the page needs the absolute URL your web server serves it at; see Where the loop-data file should live.
3. Give elements ids matching the json keys
In your page, give an id to each HTML element that should show a value. The simplest convention is to make the id the json key itself:
Outside Temperature: <span id="current.outTemp"></span>
Today's High: <span id="day.outTemp.max"></span>
Better yet, have Cheetah render the report-time value inside the element (<span id="current.outTemp">$current.outTemp</span>), so the page is correct even before the first poll — the javascript then keeps it fresh.
4. Poll loop-data.txt and fill the elements
Add javascript that fetches loop-data.txt on an interval matching your loop frequency, takes your report’s entry, and fills in the elements. A minimal version ($REPORT_NAME is the report’s name, which Cheetah substitutes — WeeWX 4.6 or later; json.dumps quotes it as a javascript string, whatever characters the name has):
#import json
<script>
async function updateLoopData() {
try {
const response = await fetch('loop-data.txt', {cache: 'no-store'});
const data = (await response.json())[$json.dumps($REPORT_NAME)];
for (const key in data) {
const element = document.getElementById(key);
if (element) element.innerHTML = data[key];
}
} catch (e) {
// File unreachable; try again next interval.
}
}
updateLoopData();
setInterval(updateLoopData, 2000); // match your loop frequency
</script>
5. Handle missing fields
A field with no value is absent from the json, so decide what your page does about it. The loop above simply leaves the old value in place; the sample skin’s gauges instead draw the dial with no needle and a -- readout — choose what suits your page. (Or force a field to always be present with string(); see Missing data.)
That is the whole recipe. The rest of this page is the contract those five steps rely on, and the touches that make a page you can leave running.
If your consumer isn’t a page — a shell script, an SNMP extension, a monitoring check reading loop-data.txt — it has no skin to declare fields in, and it should not borrow another report’s. Use the ScriptData report LoopData installs for exactly that; see Fields for scripts and other non-report consumers.
What loop-data.txt guarantees
The contract your javascript can rely on, in one place:
- Your report’s fields are under your report’s name. The top level of the file is keyed by report — the
[StdReport]section name, not the skin name — and each report’s entry holds exactly the fields it declared. (A station upgraded from before 7.0 may also carry flat keys at the top level, from the old[[Include]]fields line; see Declaring fields.) - The key is the field entry verbatim. Whatever you declared is the json key —
day.outTemp.max,almanac.sunrise.raw,station.uptime.raw— which is what makes “give the element the same id as the key” work. A quoted entry loses only its outer quotes. - A field with no value is absent, not null. Expect missing keys and react accordingly; see Missing data. The exception is
string()or an explicitNone_string, which forces the key to be present and renders the missing value as the report would. - Values are strings unless you ask otherwise. A bare field or
.formattedgives you display text (64.7°F);.rawgives you the number. Drive geometry from.rawand readouts from the formatted form — that division is the whole pattern. - Some fields are json arrays. The windrose aggregates emit arrays of numbers (
.bandeda matrix), and any almanac or station field whose endpoint is a tuple of scalars —station.latitude’s degrees/minutes/ hemisphere, for instance — emits as an array. windrose.bandsappears on its own in a report’s entry whenever that report declares any windrose field, holding the band edges in the report’s windSpeed unit so a legend never hardcodes them. It is the one key you get without asking.- Every value is already converted and formatted for your report, so nothing needs converting in javascript. If a page needs a fixed unit regardless of the report’s settings, pin it on the field (unit override) — which is worth doing on any
.rawfield you compare or plot numerically. - The file is rewritten in full on every loop packet, so there is no partial-update or merge semantics to worry about: what you fetch is the complete current state.
There is no need to guard against reading loop-data.txt mid-write: LoopData writes the file atomically (temp file plus rename). A fetch either gets the old complete file or the new complete file.
Production niceties
The sample skin’s realtime_updater.inc shows the touches that separate a demo from a page you can leave running for months:
- A LIVE/age indicator driven by
current.dateTime.raw— the packet’s own timestamp, so the page knows how fresh its data really is. Age it against the server’s clock, not the viewer’s: readDateoff the response that carried the record (response.headers.get('Date'), adding back anyAgea proxy reports). Subtracting a station timestamp fromnew Date()puts whatever the viewer’s device is set wrong by into the reading, and if your LIVE window is a few seconds wide, a device a little out never shows LIVE at all. Keep a monotonic backstop fromperformance.now()since the data last changed and take the larger of the two: elapsed is always a lower bound on the true age, so it only wins when the header reads too young, which is the cached-response case.Date.parsedecodes the header’s own string and never consults the local clock. If your page is served from a different origin than the loop-data file, a fetch may not readDateunless the server sendsAccess-Control-Expose-Headers: Date— worth adding, since it is what makes the age accurate. Where it is not available, use the viewer’s clock rather than elapsed alone: elapsed starts at zero for each page instance, so a feed that had stopped before the page loaded reads LIVE until the tab is reloaded. The sample skin distinguishes the failure modes: a rejected fetch (server unreachable) shows OFFLINE; an HTTP error showsNO DATA (HTTP 404) — check loop_data_file(the classic cause: loopdata writing outside the web server’s tree); a 200 whose body isn’t json showsBAD DATA — check loop_data_file. A later successful poll rewrites the indicator to LIVE. - A page-expiration timer that stops polling in abandoned browser tabs. In the sample skin this is the
expiration_timeExtras option (in hours,0for never); loading the page with?pageUpdate=<page_update_pwd>exempts it too (for a kiosk display that should never stop). - Keep rendering errors out of the poll loop. If your page draws (canvas gauges, charts), catch drawing errors separately from fetch errors, so a drawing bug cannot stop the polling.
-
Let the feed answer “did this period begin today?” A page that badges a reading as a week or month high needs to know whether the period is old enough for the badge to mean anything: on the first day of a week, this week’s high is today’s high, and the badge says nothing about the weather. It is tempting to settle that in Cheetah at report time and bake the answer into the page — but the answer expires at the next midnight, and a page carrying
?pageUpdate=never reloads to replace it. Declare the boundaries instead (span properties) and let every packet carry them:[LoopData] [[fields]] spans = day.start.raw, week.start.raw, month.start.raw, year.start.rawconst beganToday = p => data[p + '.start.raw'] >= data['day.start.raw'];That stays correct across every midnight the tab survives, and the page needs no calendar of its own — no
week_start, no rain-year rule, no date arithmetic in javascript. Guard the comparison where the field may be absent: an absent key makes itfalse, which reads as “did not begin today” —alltime.start.rawis missing on a station that has not archived yet (see Span properties), and on a brand-new station that is exactly when every reading is an all-time record.