portrait of John Zanussi wearing sunglasses with his daughter on his shouldersJohn Zanussi
  • Posts
  • Projects
  • Weather
  • About

Loading the latest weather…

Weather

This data comes straight from a weather station on the roof of my house.

Why?

I’ve always been interested in the weather. It started with growing up surfing on Long Island. While we do get quality surf conditions, it doesn’t happen nearly as often as somewhere like Southern California, and most of our waves depend on fairly local weather instead of swell from thousands of miles away.

NOAA buoy 44025, a yellow 3-meter discus buoy, off the south shore of Long Island

The old 44025 buoy. Photo: NOAA

Before sites like Surfline.com made condition reports and live camera feeds easy to check, we had to figure out wave quality from raw data. The National Weather Service has a network of weather reporting buoys, and the page for our local buoy, 44025, is probably one of the pages I’ve visited most in my life. Checking swell height, swell period, and wind direction was how we decided whether it was worth driving to the beach.

All this to say, I enjoy tracking the weather, and for as long as I’ve been a homeowner I’ve had a personal weather station (PWS). Besides checking the current conditions in my own backyard, I use the station’s data to run a few home automations. Rain totals help determine when the lawn needs watering, and the light sensor triggers the outdoor lights based on how dark it actually is around dawn and sunset rather than a fixed schedule.

Weather data from my station is also published to the Citizen Weather Observer Program (CWOP), Weather Underground, PWSWeather, and Windy.

How It Works

Here’s how a reading gets from my roof to this page.

  1. Ecowitt weather station
    Ecowitt WS90 weather station mounted to a pole on my roof

    The station is an Ecowitt WS90 mounted on a pole on my roof. It’s an all-in-one sensor array that measures temperature, humidity, wind, rain, UV, and light. Every 8.8 seconds it radios a reading down to its gateway, which is wired into my home network.

  2. pull
    Home Assistant polls the gateway

    Every 10 seconds, Home Assistant asks the gateway for its latest readings over my local network.

  3. Home Assistant

    Home Assistant runs on a Home Assistant Yellow rack mounted in my basement. Ecowitt’s official integration makes the readings available as sensor.pws_* entities.

    Those readings run a few things around the house. Before the sprinklers water the lawn each morning, Home Assistant checks how much rain has fallen in my yard and how cold it is, then skips watering if it isn’t needed. It also stops the sprinklers mid-run if the wind picks up or it starts raining.

    The outdoor lights use the light sensor instead of a timer. When the light level stays below about 250 lux for a couple of minutes, they turn on. Once it’s bright again, they turn off.

    To get the data onto this site, I added a rest_command to configuration.yaml with my Upstash URL and token, along with an automation that bundles the station’s sensors into a JSON payload and passes it to that command.

    configuration.yaml
    rest_command:
    send_website_weather:
    url: !secret upstash_weather_set_url
    method: post
    headers:
    Authorization: !secret upstash_weather_auth
    content_type: "application/json"
    payload: "{{ payload | tojson }}"
    timeout: 10
    automation.yaml
    alias: Website Weather Upload
    description: >-
    Sends weather station readings to the johnzanussi.com weather widget
    (Upstash KV) every 5 minutes.
    triggers:
    - trigger: time_pattern
    minutes: /5
    actions:
    - action: rest_command.send_website_weather
    data:
    payload:
    updated: "{{ utcnow().isoformat() }}"
    temp_f: "{{ states('sensor.pws_outdoor_temperature') | float(none) }}"
    13 collapsed lines
    feels_like_f: "{{ states('sensor.pws_feels_like_temperature') | float(none) }}"
    dewpoint_f: "{{ states('sensor.pws_dewpoint') | float(none) }}"
    humidity: "{{ states('sensor.pws_humidity') | int(none) }}"
    wind_mph: "{{ states('sensor.pws_wind_speed') | float(none) }}"
    gust_mph: "{{ states('sensor.pws_wind_gust') | float(none) }}"
    wind_dir_deg: "{{ states('sensor.pws_wind_direction_10m_avg') | int(none) }}"
    max_gust_today_mph: "{{ states('sensor.pws_max_daily_wind_gust') | float(none) }}"
    pressure_inhg: "{{ states('sensor.pws_relative_pressure') | float(none) }}"
    pressure_trend_inhph: "{{ states('sensor.pws_pressure_trend') | float(none) }}"
    rain_today_in: "{{ states('sensor.pws_daily_rain_piezo') | float(none) }}"
    rain_rate_inh: "{{ states('sensor.pws_rain_rate_piezo') | float(none) }}"
    uv_index: "{{ states('sensor.pws_uv_index') | int(none) }}"
    solar_wm2: "{{ states('sensor.pws_solar_radiation') | float(none) }}"
    condition: "{{ states('weather.weather_station') }}"
    mode: single
  4. push
    Home Assistant uploads the payload

    Every 5 minutes, the automation POSTs that payload to Upstash’s REST API over HTTPS.

  5. Upstash KV

    Upstash is a hosted Redis key-value store with an HTTP API, so Home Assistant can write to it with a plain web request. Every upload overwrites the same weather:current key. If the uploads ever stop, the last reading sticks around, so the site still has something to show.

    I went with Upstash because it’s built right into the Vercel Marketplace, so setting it up only took a couple of clicks.

    Here’s the latest payload:

    Loading the latest payload…
  6. pull
    The site reads the latest payload

    When someone loads this page, the site requests weather:current from Upstash over HTTPS using a read-only token. Vercel’s edge caches the response for a minute and can keep serving that cached copy for up to 5 minutes in the background while it fetches a fresh one.

  7. johnzanussi.com

    This site is hosted on Vercel. The weather widget at the top of this page and the JSON payload above are Astro server islands loaded with server:defer. They’re rendered on the server when you visit, while the rest of the page stays static. If the latest reading is more than 30 minutes old, the widget dims it so it’s clear the data is out of date.

    Here’s a simplified version of the code that reads from Upstash.

    src/utils/pws.ts
    import { KV_REST_API_URL, KV_REST_API_READ_ONLY_TOKEN } from 'astro:env/server';
    export type WeatherReading = {
    data: CurrentWeather;
    raw: string;
    };
    const WEATHER_KEY = 'weather:current';
    export const getCurrentWeather = async () => {
    const response = await fetch(`${KV_REST_API_URL}/get/${WEATHER_KEY}`, {
    headers: { Authorization: `Bearer ${KV_REST_API_READ_ONLY_TOKEN}` },
    });
    const { result } = await response.json() as { result: string | null };
    const data = JSON.parse(result) as CurrentWeather;
    const reading: WeatherReading = {
    data,
    raw: JSON.stringify(data, null, 2),
    };
    return reading;
    }

With everything running normally, a reading is at most 10 minutes old by the time it shows up here, though it’s usually much less than that. In addition to wiring up the weather data and writing this page, I also built a bunch of new Astro components for the site. I used Claude Code to help out with some of it, especially the styles for different breakpoints and code review.

It’s been a fun project tying the PWS, Home Assistant, and my website together.

As always, all the code that powers this site is on my GitHub. If you’ve got a station of your own or questions about any of this, let me know in the comments.

Comments

  • Posts
  • Projects
  • Weather
  • About
  • GitHub
  • LinkedIn
  • Printables
  • Reddit
  • Strava
  • Instagram

Built with Astro, Tailwind, and more.

Subscribe to my RSS feed.

  • Buy me a coffee
  • Email me

All content copyright John Zanussi.