DEV Community

DailyHigh
DailyHigh

Posted on • Originally published at dailyhigh.app

Build a Weather Bot, Part 2: Tracking Predictions Through the Day

A temperature prediction changes through the day. In the morning it's a best guess. By early afternoon the answer is almost locked in. By evening it's history.

This is Part 2 of a three-part series on building with the DailyHigh API. Part 1 built a threshold alert bot. Part 3 covers monitoring multiple stations at once.

💡 Tip: All the code from this series is on GitHub: dailyhigh/weather-bot.

In this post, we'll follow one station through the full day using three API endpoints, watching the prediction converge toward the final result.

The three endpoints

Each endpoint covers a different stage of the day:

Endpoint When to use it What it gives you
/api/v1/prediction/:icao Morning through peak predictedMax, confidence, slope, isPastPeak
/api/v1/weather/:icao Anytime (live observation) todayMax, current temperature, forecast.maxTemp
/api/v1/history/:icao/:date After peak or end of day Final maxTemp, minTemp, full hourly[] array

The prediction endpoint gives you the forward-looking estimate. The weather endpoint shows you what's happening right now. The history endpoint gives you the settled truth.

Setup

Same structure as Part 1. One helper per endpoint:

import requests
from datetime import date

API_KEY = "dh_live_xxxxx"
BASE = "https://dailyhigh.app"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

def get_prediction(icao: str) -> dict | None:
    resp = requests.get(
        f"{BASE}/api/v1/prediction/{icao}",
        headers=HEADERS,
        timeout=10,
    )
    if resp.status_code == 202:
        return None
    resp.raise_for_status()
    return resp.json()["data"]

def get_weather(icao: str) -> dict:
    resp = requests.get(
        f"{BASE}/api/v1/weather/{icao}",
        headers=HEADERS,
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["data"]

def get_history(icao: str, dt: str) -> dict:
    resp = requests.get(
        f"{BASE}/api/v1/history/{icao}/{dt}",
        headers=HEADERS,
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["data"]
Enter fullscreen mode Exit fullscreen mode

Phase 1: Morning (before peak)

Early in the day, the prediction endpoint is your primary source. The station hasn't reached its high yet, so observedMax is well below where the day will end up. This is where predictedMax does the heavy lifting.

pred = get_prediction("KLGA")
if pred is None:
    print("Prediction not cached yet, retry in 30s")
else:
    print(f"Predicted max: {pred['predictedMax']} °C")
    print(f"Observed so far: {pred['observedMax']} °C")
    print(f"Confidence: {pred['confidence']}/10")
    print(f"Hours until peak: {pred['hoursUntilPeak']}")
    print(f"Slope: {pred['slope']} °C/h")
Enter fullscreen mode Exit fullscreen mode

At 8 AM local time, you might see:

{
  "predictedMax": 31.2,
  "observedMax": 24.1,
  "confidence": 3,
  "hoursUntilPeak": 6.0,
  "slope": 2.1,
  "isPastPeak": false,
  "predictionReason": "Warming trend"
}
Enter fullscreen mode Exit fullscreen mode

Confidence is low because there's still a lot of day left. The slope tells you temperatures are climbing at 2.1 °C per hour. That rate won't hold all day, but it tells you the morning trend is strong.

ℹ️ Info: The confidence field is designed to rise through the day. A score of 3 at 8 AM is normal. Don't treat it as "the prediction is bad." It means "there's still a lot of uncertainty left."

Phase 2: Midday (approaching peak)

By noon or early afternoon, the prediction is much tighter. Now we can cross-reference with the live weather observation.

pred = get_prediction("KLGA")
wx = get_weather("KLGA")

print(f"Prediction: {pred['predictedMax']} °C (confidence {pred['confidence']})")
print(f"Current temp: {wx['temperature']} °C")
print(f"Today's max so far: {wx['todayMax']} °C")
print(f"Forecast max: {wx['forecast']['maxTemp']} °C")
print(f"Past peak: {pred['isPastPeak']}")
Enter fullscreen mode Exit fullscreen mode

At 1 PM:

{
  "predictedMax": 30.8,
  "observedMax": 30.2,
  "confidence": 7,
  "hoursUntilPeak": 1.0,
  "slope": 0.4,
  "isPastPeak": false
}
Enter fullscreen mode Exit fullscreen mode

Confidence is 7. The observed max is already 30.2 °C, and the slope has flattened to 0.4 °C/h. The prediction has narrowed from 31.2 to 30.8 as real data replaced the model estimate. With one hour until peak, the gap between observed and predicted is small.

This is the critical window: the slope is slowing, hoursUntilPeak is low, and the prediction and observation are converging.

Phase 3: After peak

Once isPastPeak flips to true, the station has likely recorded its high for the day. The observed max and the predicted max should be very close now.

pred = get_prediction("KLGA")
print(f"Past peak: {pred['isPastPeak']}")
print(f"Observed max: {pred['observedMax']} °C")
print(f"Predicted max: {pred['predictedMax']} °C")
print(f"Confidence: {pred['confidence']}/10")
Enter fullscreen mode Exit fullscreen mode

At 4 PM:

{
  "predictedMax": 30.5,
  "observedMax": 30.5,
  "confidence": 9,
  "hoursUntilPeak": 0,
  "slope": -0.8,
  "isPastPeak": true
}
Enter fullscreen mode Exit fullscreen mode

Confidence is 9. The slope is negative (cooling). Predicted and observed have converged to the same value. The day's high is essentially decided.

Phase 4: Get the final result

At end of day (or any time after), the history endpoint gives you the confirmed daily high along with the full hourly profile.

today = date.today().isoformat()
history = get_history("KLGA", today)

print(f"Final daily max: {history['maxTemp']} °C")
print(f"Final daily min: {history['minTemp']} °C")
print(f"Forecast had predicted: {history['forecast']['maxTemp']} °C")
print(f"Hourly observations: {len(history['hourly'])}")
Enter fullscreen mode Exit fullscreen mode
{
  "date": "2026-02-03",
  "maxTemp": 30.5,
  "minTemp": 18.2,
  "forecast": { "maxTemp": 31, "minTemp": 17 },
  "hourly": [
    { "time": "2026-02-03T00:00:00", "temperature": 19.8 },
    { "time": "2026-02-03T01:00:00", "temperature": 19.1 },
    "..."
  ]
}
Enter fullscreen mode Exit fullscreen mode

Now you can compare all three numbers:

Source Max temp
Morning prediction (8 AM) 31.2 °C
Forecast (forecast.maxTemp) 31.0 °C
Final observed (maxTemp) 30.5 °C

The prediction started at 31.2 °C and converged to 30.5 °C as observations came in. The third-party forecast said 31.0 °C. The actual high was 30.5 °C. That convergence pattern is typical: the prediction gets pulled toward reality through the day.

Putting it all together

Here's a script that runs the full lifecycle. Call it three times during the day (or schedule it on a cron) and it logs each phase:

import json
from datetime import date, datetime
from pathlib import Path

LOG_FILE = Path("daily_log.json")

def log_snapshot(icao: str):
    pred = get_prediction(icao)
    wx = get_weather(icao)
    today = date.today().isoformat()

    snapshot = {
        "time": datetime.now().isoformat(),
        "currentTemp": wx["temperature"],
        "todayMax": wx["todayMax"],
        "predictedMax": pred["predictedMax"] if pred else None,
        "confidence": pred["confidence"] if pred else None,
        "isPastPeak": pred["isPastPeak"] if pred else None,
        "slope": pred["slope"] if pred else None,
    }

    # Append to log
    log = []
    if LOG_FILE.exists():
        log = json.loads(LOG_FILE.read_text())
    log.append(snapshot)
    LOG_FILE.write_text(json.dumps(log, indent=2))

    print(json.dumps(snapshot, indent=2))

    # If past peak, also fetch history for the final number
    if pred and pred["isPastPeak"]:
        history = get_history(icao, today)
        print(f"\nFinal daily max: {history['maxTemp']} °C")

log_snapshot("KLGA")
Enter fullscreen mode Exit fullscreen mode

Run it at 8 AM, 1 PM, and 5 PM. You'll get a JSON log showing how the prediction evolved:

[
  { "time": "08:00", "todayMax": 24.1, "predictedMax": 31.2, "confidence": 3, "isPastPeak": false },
  { "time": "13:00", "todayMax": 30.2, "predictedMax": 30.8, "confidence": 7, "isPastPeak": false },
  { "time": "17:00", "todayMax": 30.5, "predictedMax": 30.5, "confidence": 9, "isPastPeak": true }
]
Enter fullscreen mode Exit fullscreen mode

What to watch for

Confidence jumps. A sudden jump from 4 to 7 usually means the prediction and observation have started converging. That's the point where the outcome becomes much more predictable.

Slope sign change. When slope goes from positive to negative, the station is cooling. This typically happens within an hour of the actual peak.

Forecast vs prediction divergence. The forecastMax field in the prediction endpoint is the third-party forecast. If predictedMax starts pulling away from forecastMax, it means real-time observations are overriding the model. This often happens on unusual weather days.

Next steps

Part 1 of this series built a threshold alert bot using just the prediction endpoint. Part 3 scales the pattern to all tracked stations with a multi-station dashboard.

The full API reference documents every field in each response. Browse the stations index to find ICAO codes for all 12 tracked stations.


Originally published on DailyHigh. DailyHigh tracks daily high temperatures at major airport weather stations worldwide using real-time METAR observations.

Top comments (0)