My weather station has no local API, so I pull it from Weather Underground

A 7-in-1 AliExpress weather station that opens no ports: how I read its data from a Pi with the Weather Underground API and feed it to n8n and Home Assistant.

IvánPublished on 7 minLeer en español

I bought a 7-in-1 weather station on AliExpress (temperature, humidity, wind speed and direction, rain, pressure and solar radiation/UV) with a colour display for the living room. The plan was simple: get the data onto the Raspberry Pi so it could go into my daily summary and I could ask my home agent whether it’s raining. The catch: the station has no local API. Here’s how I solved it, and the afternoon I wasted first by assuming things.

1. First, find out what you actually have

Mine is sold under several brands (the same station shows up with a different logo in other shops). I found its IP in the router’s list by unplugging it and seeing which line disappeared, the trick I described in fixed IPs with a DHCP reservation. The Wi-Fi chip vendor in the MAC address sounded like the usual smart plugs, and the listings mentioned an “app”, so I assumed it was a Tuya device.

Wrong. I spent the afternoon setting up a project on Tuya’s developer platform for nothing: the station doesn’t use Tuya or any app. What I should have done first:

# Does it open any port? (example IP)
nmap -p- --open 192.168.1.70

Nothing. It doesn’t listen on any port. Its configuration page only exists in access point mode: the station creates its own Wi-Fi network, you join it and open http://192.168.4.1. There are three things there: your home Wi-Fi, Weather Underground and WeatherCloud. There’s no “custom server” option, which on other stations lets you send the data to your own server.

Before building any integration, look up the exact model and its manual. The chip or the brand on the box won’t tell you what protocol it speaks.

2. Register it on Weather Underground

If the data can only go to the cloud, I’ll get it from the cloud. Weather Underground (WU) has a free API for the owner of a personal weather station (PWS).

  1. Create an account on wunderground.com and add a device (I picked the Other type). You get a Station ID (something like IYOURTOWN12) and a station key.
  2. Put the station in AP mode, open 192.168.4.1 and enter that ID and key in the Weather Underground section.
  3. Wait a few minutes: data should start showing up on your station’s page on WU.
  4. Now go to your profile, API Keys tab, and generate the key.

Step 4 had me stuck for a day: WU won’t let you create the API key until your station is sending data. I registered it, saw it was still “no data” and left it for the next day. Once the station was configured, the key was generated straight away.

I also registered it on WeatherCloud while I was at it: the station uploads to both at once, which gives me another site with charts. The Pi only reads from WU.

3. Reading it from the Pi with Python

The endpoint you want is the current observation:

https://api.weather.com/v2/pws/observations/current?stationId=YOUR_ID&format=json&units=m&numericPrecision=decimal&apiKey=YOUR_API_KEY

units=m gives you metric and numericPrecision=decimal stops it from rounding the temperature to whole numbers. The credentials live in a JSON file with 600 permissions, outside the code:

mkdir -p ~/.config/wunderground
cat > ~/.config/wunderground/credenciales.json <<'EOF'
{"station_id": "YOUR_ID", "api_key": "YOUR_API_KEY"}
EOF
chmod 600 ~/.config/wunderground/credenciales.json

The script uses the standard library only (no pip install). This is the important part:

CACHE_SEG = 300       # the station uploads every few minutes: don't burn quota
SIN_DATOS_SEG = 1800  # observation older than 30 min = the station isn't uploading

def _leer():
    c = json.loads(CRED.read_text())
    q = urllib.parse.urlencode({"stationId": c["station_id"], "format": "json", "units": "m",
                                "numericPrecision": "decimal", "apiKey": c["api_key"]})
    with urllib.request.urlopen(f"{URL}?{q}", timeout=10) as r:
        if r.status == 204:
            return {"disponible": False, "error": "WU has no data from the station yet"}
        o = json.load(r)["observations"][0]
    m = o.get("metric") or {}
    edad = time.time() - o["epoch"]
    return {
        "disponible": edad < SIN_DATOS_SEG,
        "temperatura_c": m.get("temp"),
        "humedad_pct": o.get("humidity"),
        "viento_kmh": m.get("windSpeed"),
        "racha_kmh": m.get("windGust"),
        "viento_dir": _rumbo(o.get("winddir")),
        "lluvia_hoy_mm": m.get("precipTotal"),
        "presion_hpa": m.get("pressure"),
        "uv": o.get("uv"),
        # ... feels-like, dew point, radiation
    }

(The field names are in Spanish because the whole dashboard is; disponible means “available”.) Three things I learned along the way:

  • The API answers 204 (no content) if the station hasn’t sent anything yet. If you don’t handle it, json.load blows up with an unhelpful error.
  • It returns the last observation even if it’s hours old. That’s why I check epoch and set disponible: false when it’s older than 30 minutes. Otherwise the morning summary gives you last night’s temperature without a word.
  • Wind direction comes in degrees. I turn it into compass points (N, NNE, NE…) by dividing by 22.5, which is what you actually want to read on your phone.

On top of that, any error (network, wrong key) is returned as {"disponible": false, "error": "..."} instead of raising an exception: the dashboard shouldn’t go down because the station lost its Wi-Fi.

4. One route for everything: /api/tiempo

The script plugs into the server of my command center as just another route:

if path == "/api/tiempo":
    return self._send(200, tiempo("fresh=1" in self.path))

And this is what it returns (example values):

curl -s http://192.168.1.50:8090/api/tiempo
{"disponible": true, "sin_datos_min": 2, "temperatura_c": 22.4, "sensacion_c": 22.4,
 "humedad_pct": 55.0, "viento_kmh": 6.1, "racha_kmh": 11.2, "viento_dir": "SW",
 "lluvia_hoy_mm": 0.0, "presion_hpa": 1016.2, "radiacion_wm2": 310.5, "uv": 3.0}

The point of having a single route is that everything else (n8n, Home Assistant, whatever comes next) reads from the Pi, not from WU: the key lives in one place and the 5-minute cache protects the quota even if three programs ask at once.

5. In the daily summary and the n8n agent

In n8n I have the daily summary workflow and an agent I talk to over Telegram (I covered it in back to n8n). Adding the weather took very little:

  • Daily summary: an HTTP Request node to /api/tiempo with On Error: Continue, and a “Weather at home” section in the message. If the station fails, the summary still arrives, just without that section.
  • Agent: a tiempo tool (HTTP Request Tool node) with a description that says what it returns and, above all, what it doesn’t do:
Current weather measured by the home weather station: temperature and feels-like (°C),
humidity, wind and gusts (km/h) with direction, rain today (mm), pressure, radiation and UV.
If disponible=false the station isn't sending data. No forecast, only what it measures now.

That last sentence matters. A model asked “will it rain tomorrow?” with a tool called tiempo (“weather”) at hand tends to answer with the current reading as if it were a forecast. I repeat it in the agent’s instructions too: if asked about tomorrow, say the station doesn’t do forecasts.

6. Bonus: Home Assistant with the REST integration

Home Assistant has no integration for this station, but it doesn’t need one: the rest integration reads the same route and creates one sensor per field.

rest:
  - resource: http://192.168.1.50:8090/api/tiempo
    scan_interval: 300
    sensor:
      - name: "Station temperature"
        value_template: "{{ value_json.temperatura_c }}"
        unit_of_measurement: "°C"
        device_class: temperature
        availability: "{{ value_json.disponible }}"
      - name: "Station humidity"
        value_template: "{{ value_json.humedad_pct }}"
        unit_of_measurement: "%"
        device_class: humidity
        availability: "{{ value_json.disponible }}"
      # ... wind, rain, pressure, UV

availability makes the sensors show as unavailable when the observation is stale, instead of a frozen value. One trap I hit: my Home Assistant was set to the imperial system because I got it wrong in the onboarding wizard, and it was converting °C to °F. You change it in Settings → System → General → Unit system.

What not to do

  • Guess the protocol from the chip or the shop. Check the ports and the manual first.
  • Put the API key in the code or in a repo. It goes in a separate file with 600 permissions.
  • Call WU from every program separately. One route on the Pi with a cache, and everything reads from there.
  • Trust that the API gives you current data. Check the observation time.
  • Let a station failure take down the summary or the dashboard. Errors as data (disponible: false), not exceptions.

Summary

  1. If the station opens no ports and has no custom server option, let it upload to Weather Underground and read from its API.
  2. The API key can only be generated once the station is already sending data.
  3. A standard-library script with a 5-minute cache, handling for 204 and for the observation’s age.
  4. A single /api/tiempo route on the Pi for n8n, the agent and Home Assistant.

Relying on the cloud for something five metres away isn’t my favourite setup, but for what the station cost, it’s a fair trade. If I ever buy another one, my first question will be whether it can send data to my own server.

Mini quiz

Did it stick?

Three quick questions. Each right answer is worth 10 XP.

  1. Why can't the Pi ask the station for its data directly?
  2. You've registered the station on Weather Underground, but the API Keys tab won't let you create a key. What's going on?
  3. What's the point of checking the age of the latest observation (older than 30 minutes = unavailable)?
  • #weather-station
  • #weather-underground
  • #raspberry-pi
  • #n8n
  • #home-assistant
Esc