Templates are the part of weather-station publishing that separates a forgettable data dump from a site people actually bookmark. I have built somewhere north of forty custom templates for GraphWeather over the years β€” some clean and elegant, others held together with tape and optimism β€” and every one of them taught me something about the template engine that the documentation alone could not. This article is the distillation of those lessons.

We will start with the template variable system: syntax, conditionals, loops, and format specifiers. Then we will move into building a custom HTML template from a blank file. Finally, we will cross into plugin development β€” GraphWeather's mechanism for extending the application itself with sensor editor plugins and data export modules. By the end you will have the vocabulary and the patterns to build whatever your station demands.

If you are comparing GraphWeather's templating to other tools, the GraphWeather vs. Weewx vs. CumulusMX comparison puts the three side by side. For the variable names shared with WeatherLink-style systems, the WeatherLink and templates guide is the canonical reference.

Quick-Answer Summary

  • Template variables use the <%variableName%> syntax with optional format specifiers: <%temperature:.1f%> outputs one decimal place.
  • Conditionals use <%if condition%>...<%endif%>, supporting comparison operators and boolean logic.
  • Loops iterate over record sets with <%for record in history%>...<%endfor%>.
  • Plugins are DLL assemblies (C++ or Delphi) that implement a defined interface β€” IGraphWeatherPlugin β€” and are loaded at startup.
  • Common mistakes cluster around three areas: escaping, loop syntax, and missing-data handling.

Prerequisites

  • A working GraphWeather installation with at least one week of logged data (you need historical records to test loops).
  • A text editor with syntax highlighting β€” VS Code with an HTML extension works well.
  • For plugin development: a C++ compiler (MSVC or MinGW) or Delphi. Familiarity with DLL development on Windows.
  • Access to GraphWeather's template preview mode, which renders a template against live data without publishing it. This saves an enormous amount of FTP round-trip time during development.

Part 1 β€” The Template Variable System

Basic Syntax

Every GraphWeather template variable lives between <% and %> delimiters. The engine scans your HTML file at publish time, replaces each variable with the current value, and writes the result to the output directory.

<p>Current temperature: <%temperature%> Β°C</p>
<p>Humidity: <%humidity%>%</p>
<p>Barometric pressure: <%pressure%> hPa</p>

Variable names are case-sensitive. <%Temperature%> and <%temperature%> are different β€” and only the lowercase version exists by default. This catches people on day one.

Format Specifiers

Append a colon and a format string after the variable name:

<p>Temperature: <%temperature:.1f%> Β°C</p>   <!-- 8.3 -->
<p>Pressure: <%pressure:.0f%> hPa</p>         <!-- 1018 -->
<p>Rain today: <%rain_daily:.2f%> mm</p>       <!-- 0.00 -->

The format specifiers follow C-style printf conventions:

Specifier Meaning Example output
.1f One decimal place, fixed 8.3
.0f Zero decimal places 1018
.2f Two decimal places 12.60
d Integer 72
s String (default for text variables) NNW

If you omit the format specifier, GraphWeather uses the default defined in the station configuration β€” usually two decimal places for most numeric fields.

Conditionals

Conditionals let you show or hide blocks based on data values:

<%if temperature < 0%>
  <p class="frost-warning">⚠️ Frost conditions β€” temperature below zero.</p>
<%endif%>

<%if wind_speed > 50 and wind_gust > 70%>
  <p class="storm-warning">πŸŒͺ️ High wind warning in effect.</p>
<%endif%>

Supported operators: <, >, <=, >=, ==, !=, and, or, not. You can nest conditionals, but beyond two levels the readability drops fast. Pull complex logic into a plugin if it starts to sprawl.

A particularly useful pattern is checking for sensor availability:

<%if has_uv_sensor%>
  <p>UV Index: <%uv_index:.0f%></p>
<%endif%>

This prevents the template from rendering a blank or zero UV value when the station does not have a UV sensor installed. It sounds minor, but it is one of the most common visual bugs on amateur weather sites β€” a confident-looking "UV Index: 0" that actually means "sensor not connected."

Loops

Loops iterate over historical record sets. GraphWeather exposes several built-in collections: history_hourly, history_daily, history_monthly.

<table>
  <tr><th>Date</th><th>High</th><th>Low</th><th>Rain</th></tr>
  <%for day in history_daily%>
  <tr>
    <td><%day.date:s%></td>
    <td><%day.temp_max:.1f%> Β°C</td>
    <td><%day.temp_min:.1f%> Β°C</td>
    <td><%day.rain_total:.1f%> mm</td>
  </tr>
  <%endfor%>
</table>

The loop variable (day in this example) is scoped to the loop body. You can name it anything β€” record, row, r β€” as long as it does not collide with a top-level variable name.

Loop limits. By default, history_daily returns the last 30 days. Control the range with a parameter:

<%for day in history_daily(7)%>
  <!-- Last 7 days only -->
<%endfor%>

For hourly data over a long range, be mindful of output size. A year of hourly records produces 8,760 table rows β€” enough to slow a browser and annoy a visitor. Paginate or use daily aggregates for anything beyond a week.

Escaping Special Characters

The <% delimiter can collide with other template languages, JavaScript template literals, or even CSS that happens to contain a percent-sign-less-than sequence. Escape literal <% by doubling the percent sign:

<p>Battery level: 85<%% complete</p>  <!-- Outputs: 85% complete -->

Inside JavaScript blocks, wrap your GraphWeather variables carefully:

<script>
  const temp = <%temperature:.2f%>;  // numeric β€” no quotes needed
  const dir  = "<%wind_dir:s%>";      // string β€” quote it
</script>

Forgetting to quote string variables inside JavaScript is a classic source of "Uncaught SyntaxError" in the browser console. The template engine does not know it is inside a <script> tag β€” it performs a blind replacement.

Part 2 β€” Building a Custom HTML Template from Scratch

Let us build a minimal but complete single-page weather template.

File Structure

Create a new directory under GraphWeather's templates folder:

templates/
  my-station/
    index.html
    style.css

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%station_name:s%> β€” Current Conditions</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header>
    <h1><%station_name:s%></h1>
    <p>Last update: <%last_update:s%></p>
  </header>

  <main>
    <section class="current">
      <div class="card">
        <h2>Temperature</h2>
        <p class="value"><%temperature:.1f%><span class="unit"> Β°C</span></p>
        <p class="detail">Feels like <%windchill:.1f%> Β°C</p>
      </div>
      <div class="card">
        <h2>Humidity</h2>
        <p class="value"><%humidity:.0f%><span class="unit"> %</span></p>
        <p class="detail">Dew point <%dewpoint:.1f%> Β°C</p>
      </div>
      <div class="card">
        <h2>Wind</h2>
        <p class="value"><%wind_speed:.1f%><span class="unit"> km/h</span></p>
        <p class="detail"><%wind_dir:s%> β€” Gusts <%wind_gust:.1f%> km/h</p>
      </div>
      <div class="card">
        <h2>Pressure</h2>
        <p class="value"><%pressure:.1f%><span class="unit"> hPa</span></p>
        <p class="detail">Trend: <%pressure_trend:s%></p>
      </div>
      <div class="card">
        <h2>Rain</h2>
        <p class="value"><%rain_daily:.1f%><span class="unit"> mm</span></p>
        <p class="detail">Rate: <%rain_rate:.1f%> mm/h</p>
      </div>
      <%if has_uv_sensor%>
      <div class="card">
        <h2>UV Index</h2>
        <p class="value"><%uv_index:.0f%></p>
      </div>
      <%endif%>
    </section>

    <section class="history">
      <h2>Last 7 Days</h2>
      <table>
        <thead>
          <tr><th>Date</th><th>High</th><th>Low</th><th>Rain</th></tr>
        </thead>
        <tbody>
          <%for day in history_daily(7)%>
          <tr>
            <td><%day.date:s%></td>
            <td><%day.temp_max:.1f%> Β°C</td>
            <td><%day.temp_min:.1f%> Β°C</td>
            <td><%day.rain_total:.1f%> mm</td>
          </tr>
          <%endfor%>
        </tbody>
      </table>
    </section>
  </main>

  <footer>
    <p>Powered by GraphWeather</p>
  </footer>
</body>
</html>

Testing with Preview Mode

In GraphWeather, navigate to Templates β†’ Preview. Select your my-station/index.html file. The preview renders the template against the most recent data snapshot without triggering a publish cycle. Use this constantly. Every save-and-preview cycle takes about two seconds; every save-and-publish-and-FTP cycle takes thirty or more.

Iterating on the Design

Once the structure is correct, layer on CSS. Keep the CSS in a separate file β€” the template engine does not process .css files, so you avoid any risk of delimiter collisions. GraphWeather will copy non-template files (CSS, images, JavaScript) to the output directory unchanged.

Part 3 β€” Plugin Development

Templates control presentation. Plugins control behaviour. GraphWeather's plugin system lets you extend the application in two main directions: sensor editor plugins (add UI elements to the desktop app for new sensor types or calculations) and data export plugins (automate the export of data to external systems).

Plugin Architecture Overview

Plugins are Windows DLLs that export a standard interface. At startup, GraphWeather scans the plugins/ directory, loads each DLL, and calls its Initialize function. The interface is defined in the GraphWeather SDK header:

// IGraphWeatherPlugin.h
class IGraphWeatherPlugin {
public:
    virtual bool Initialize(IPluginHost* host) = 0;
    virtual void OnDataUpdate(const WeatherRecord& record) = 0;
    virtual void Shutdown() = 0;
    virtual const char* GetName() = 0;
    virtual const char* GetVersion() = 0;
};

IPluginHost gives you access to the data store, the configuration system, and the publish scheduler. Think of it as the plugin's window into the rest of the application.

Creating a Data Export Plugin

Let us build a plugin that writes a JSON file every time new data arrives β€” useful for feeding a custom API or a Grafana datasource (see the Grafana dashboard guide for the receiving end).

#include "IGraphWeatherPlugin.h"
#include <fstream>
#include <sstream>
#include <ctime>

class JsonExportPlugin : public IGraphWeatherPlugin {
    IPluginHost* m_host;
    std::string m_outputPath;

public:
    bool Initialize(IPluginHost* host) override {
        m_host = host;
        m_outputPath = host->GetConfigValue("json_export", "output_path",
                                             "C:\\GraphWeather\\export\\current.json");
        return true;
    }

    void OnDataUpdate(const WeatherRecord& rec) override {
        std::ofstream out(m_outputPath, std::ios::trunc);
        if (!out.is_open()) return;

        out << "{\n"
            << "  \"timestamp\": \"" << rec.timestamp_iso << "\",\n"
            << "  \"temperature\": " << rec.temperature << ",\n"
            << "  \"humidity\": " << rec.humidity << ",\n"
            << "  \"pressure\": " << rec.pressure << ",\n"
            << "  \"wind_speed\": " << rec.wind_speed << ",\n"
            << "  \"wind_dir\": \"" << rec.wind_dir << "\",\n"
            << "  \"rain_rate\": " << rec.rain_rate << ",\n"
            << "  \"rain_daily\": " << rec.rain_daily << "\n"
            << "}\n";
        out.close();
    }

    void Shutdown() override {}
    const char* GetName() override { return "JSON Export"; }
    const char* GetVersion() override { return "1.0.0"; }
};

// DLL entry point
extern "C" __declspec(dllexport)
IGraphWeatherPlugin* CreatePlugin() {
    return new JsonExportPlugin();
}

Compile this as a DLL, drop it into GraphWeather's plugins/ folder, and restart the application. The plugin reads its output path from GraphWeather's config file (under a [json_export] section) and overwrites current.json on every data update.

Creating a Sensor Editor Plugin

Sensor editor plugins add custom UI panels to GraphWeather's desktop interface. The pattern is similar, but you implement IEditorPlugin instead, which gives you access to the GUI framework:

class IEditorPlugin : public IGraphWeatherPlugin {
public:
    virtual HWND CreatePanel(HWND parent, RECT bounds) = 0;
    virtual void OnSensorData(const SensorReading& reading) = 0;
};

CreatePanel returns a Windows HWND that GraphWeather embeds in its tabbed interface. You have full Win32 API access within your panel β€” draw charts, add buttons, display calculated values. The OnSensorData callback fires every time a new reading arrives from the station hardware, before it is logged to the database.

Debugging Plugins

Attach Visual Studio's debugger to the running GraphWeather process. Set breakpoints in your OnDataUpdate or OnSensorData callbacks. GraphWeather loads plugins in-process, so standard DLL debugging techniques apply.

Common issues:

  • DLL not loading: Check that the DLL is compiled for the same architecture (x86 or x64) as GraphWeather.
  • Missing dependencies: If your plugin links against a runtime library that is not on the path, it will silently fail to load. Use Dependency Walker or dumpbin /dependents to verify.
  • Config values not found: Double-check the section and key names in the config file. The GetConfigValue function returns the default if the section does not exist β€” it does not log a warning.

Versioning Templates and Plugins

Store your templates and plugin source code in version control. A Git repository alongside your GraphWeather installation works well:

graphweather/
  templates/
    my-station/
      index.html
      style.css
  plugins/
    json-export/
      src/
        JsonExportPlugin.cpp
      build/
        JsonExport.dll
  .git/

Tag releases. When you update a template, bump the tag. When the published site breaks at 2 a.m. and you cannot remember what you changed, git diff v1.4..v1.5 will tell you in seconds.

Common Mistakes

  1. Forgetting to escape <% in non-variable contexts. The engine is greedy β€” if it sees <%, it tries to parse a variable. Use <%% for a literal percent-less-than.
  2. Incorrect loop syntax. The most common error is writing <%for day in history_daily%> without parentheses when you want to limit the range. Without a parameter, you get the default 30 days. With the wrong parameter type (e.g., a string instead of a number), the loop silently returns zero records.
  3. Not handling missing sensor data. If a sensor is offline, its variable resolves to an empty string or zero. Always wrap optional sensors in <%if has_..._sensor%> guards.
  4. Building plugins against the wrong SDK version. The plugin interface evolves between major GraphWeather releases. Always compile against the SDK header that ships with your installed version.
  5. Testing templates only in preview mode and never via actual FTP publish. Preview mode and published output can differ in path resolution β€” relative image paths that work in preview may break when the file lands in a different directory on the web server. Test the full publishing pipeline before going live.

Related Reading

FAQ

Can I use PHP inside a GraphWeather template? Yes. GraphWeather's template engine processes its own variables first, then outputs the file. If the output file has a .php extension and your web server runs PHP, the server-side PHP code will execute on request. This gives you two layers of dynamism: GraphWeather variables resolved at publish time, and PHP logic resolved at view time.

Is there a way to live-reload templates during development? Preview mode is the closest built-in option. For a true live-reload experience, point a local web server (e.g., python -m http.server) at GraphWeather's output directory and use a browser extension that auto-refreshes on file change.

Can plugins communicate with each other? Not directly through the SDK. However, plugins can read and write to the shared config file and to the data directory, so you can implement inter-plugin communication via file-based signals if necessary. Keep it simple β€” a shared JSON file with a timestamp is usually enough.

How many plugins can GraphWeather load simultaneously? There is no hard-coded limit. In practice, each plugin adds a small amount of latency to the data-update cycle (since OnDataUpdate is called sequentially for each plugin). I have run five plugins simultaneously without noticeable delay; beyond ten, profile the update cycle to ensure you are not falling behind the station's reporting interval.

Where can I find example plugins? The GraphWeather SDK ships with two example plugins: a CSV export module and a simple sensor display panel. Both are in the sdk/examples/ directory of the installation. They compile out of the box with MSVC and serve as solid starting points.