DEV Community

DailyHigh
DailyHigh

Posted on • Originally published at dailyhigh.app

Build a Weather Bot, Part 1: Temperature Threshold Alerts

You pick a station. You set a number. The bot tells you when the temperature crosses it, or when the day's trajectory says it won't. About 80 lines of Python.

This is Part 1 of a three-part series on building with the DailyHigh API. Part 2 covers tracking a daily high from prediction to final result, and Part 3 covers monitoring multiple stations at once.

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

Why threshold alerts?

Forecasts give you a range. "High near 84 °F." That's useful for packing a jacket, less useful when you need to know whether a station will actually hit 85 °F on a specific day.

The DailyHigh prediction endpoint updates every two minutes. It returns the current observed max, a predicted max, a confidence score, and a flag for whether the station has passed its daily peak. That's enough to build a bot that watches a single number and tells you the moment the answer is clear.

What you'll need

  • Python 3.9+
  • A DailyHigh Pro API key (starts with dh_live_)
  • A Discord webhook URL (or Slack, or any service that accepts a POST request)

Step 1: Fetch the prediction

The /api/v1/prediction/:icao endpoint returns everything we need. Here's a minimal fetch:

import requests

API_KEY = "dh_live_xxxxx"
BASE = "https://dailyhigh.app"

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

A typical response looks like this:

{
  "currentTemp": 28.4,
  "observedMax": 29.1,
  "predictedMax": 31.2,
  "forecastMax": 31.0,
  "confidence": 6,
  "isPastPeak": false,
  "hoursUntilPeak": 2.5,
  "slope": 1.4,
  "slopeWindow": "90 min",
  "predictionReason": "Warming trend",
  "reasoning": [
    "About 2h until peak warming",
    "Temperatures still climbing",
    "Observed temps tracking close to prediction"
  ]
}
Enter fullscreen mode Exit fullscreen mode

The fields that matter most for threshold checking:

Field What it tells you
observedMax Highest temperature recorded so far today
predictedMax Where DailyHigh thinks the day will end up
confidence 1 (low) to 10 (high), rises as the day progresses
isPastPeak true once the station has likely hit its high for the day
slope Rate of temperature change in °C/hour over the last 90 minutes

Step 2: Define your threshold

Keep it simple. A config dict with the station, target temperature, and direction:

ALERT = {
    "icao": "KLGA",
    "target": 29.4,  # 85 °F in Celsius
    "direction": "over",  # "over" or "under"
}
Enter fullscreen mode Exit fullscreen mode

ℹ️ Info: The API returns all temperatures in °C. If you're working in Fahrenheit, convert your target before comparing: (85 - 32) * 5 / 9 = 29.44 °C.

Step 3: Check the threshold

Three possible outcomes each time you poll:

  1. Crossed. The observed max has already passed the target.
  2. Unlikely. The station is past peak and the predicted max didn't reach it.
  3. Still in play. Neither condition is met yet.
def check_threshold(data: dict, target: float, direction: str) -> str:
    observed = data["observedMax"]
    predicted = data["predictedMax"]
    past_peak = data["isPastPeak"]

    if direction == "over":
        if observed >= target:
            return "crossed"
        if past_peak and predicted < target:
            return "unlikely"
    else:  # "under"
        if past_peak and observed < target:
            return "crossed"
        if observed >= target:
            return "unlikely"

    return "pending"
Enter fullscreen mode Exit fullscreen mode

The isPastPeak flag is the key signal. Once it flips to true, the observed max is very close to the final daily high. If the number hasn't been hit by then, it probably won't be.

Step 4: Send an alert

Discord webhooks accept a simple JSON POST. Same pattern works for Slack.

WEBHOOK_URL = "https://discord.com/api/webhooks/..."

def send_alert(message: str):
    requests.post(
        WEBHOOK_URL,
        json={"content": message},
        timeout=10,
    )
Enter fullscreen mode Exit fullscreen mode

Format the message with the data that matters:

def format_message(data: dict, status: str, alert: dict) -> str:
    icao = alert["icao"]
    target = alert["target"]
    observed = data["observedMax"]
    predicted = data["predictedMax"]
    confidence = data["confidence"]

    if status == "crossed":
        return (
            f"✅ **{icao}** crossed {target} °C\n"
            f"Observed max: {observed} °C | "
            f"Predicted max: {predicted} °C | "
            f"Confidence: {confidence}/10"
        )
    elif status == "unlikely":
        return (
            f"❌ **{icao}** unlikely to reach {target} °C\n"
            f"Observed max: {observed} °C | "
            f"Predicted max: {predicted} °C | "
            f"Past peak: yes"
        )
    return ""
Enter fullscreen mode Exit fullscreen mode

Step 5: Put it together

The full script polls the prediction endpoint, checks the threshold, and sends one alert when the outcome is decided.

import requests
import sys

API_KEY = "dh_live_xxxxx"
BASE = "https://dailyhigh.app"
WEBHOOK_URL = "https://discord.com/api/webhooks/..."

ALERT = {
    "icao": "KLGA",
    "target": 29.4,
    "direction": "over",
}

def get_prediction(icao: str) -> dict:
    resp = requests.get(
        f"{BASE}/api/v1/prediction/{icao}",
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=10,
    )
    if resp.status_code == 202:
        return None  # prediction not yet cached, retry later
    resp.raise_for_status()
    return resp.json()["data"]

def check_threshold(data: dict, target: float, direction: str) -> str:
    observed = data["observedMax"]
    predicted = data["predictedMax"]
    past_peak = data["isPastPeak"]

    if direction == "over":
        if observed >= target:
            return "crossed"
        if past_peak and predicted < target:
            return "unlikely"
    else:
        if past_peak and observed < target:
            return "crossed"
        if observed >= target:
            return "unlikely"

    return "pending"

def send_alert(message: str):
    requests.post(WEBHOOK_URL, json={"content": message}, timeout=10)

def main():
    data = get_prediction(ALERT["icao"])
    if data is None:
        print("Prediction not ready, will retry next run")
        sys.exit(0)

    status = check_threshold(data, ALERT["target"], ALERT["direction"])
    print(f'{ALERT["icao"]}: {status} '
          f'(observed={data["observedMax"]}, '
          f'predicted={data["predictedMax"]}, '
          f'confidence={data["confidence"]})')

    if status in ("crossed", "unlikely"):
        msg = format_message(data, status, ALERT)
        send_alert(msg)
        print("Alert sent")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Step 6: Schedule it

Run the script every 30 minutes during the station's daylight hours. The prediction endpoint updates every two minutes, but for a threshold bot, checking twice an hour is plenty.

Cron (macOS/Linux):

# Every 30 minutes from 6 AM to 8 PM UTC
*/30 6-20 * * * cd /path/to/bot && python alert.py >> alert.log 2>&1
Enter fullscreen mode Exit fullscreen mode

GitHub Actions (free tier):

on:
  schedule:
    - cron: "*/30 6-20 * * *"
Enter fullscreen mode Exit fullscreen mode

One thing to handle: you only want to send the alert once per day, not every 30 minutes after the threshold is decided. The simplest fix is a state file:

import json
from pathlib import Path

STATE_FILE = Path("state.json")

def already_alerted(icao: str, date: str) -> bool:
    if not STATE_FILE.exists():
        return False
    state = json.loads(STATE_FILE.read_text())
    return state.get(icao) == date

def mark_alerted(icao: str, date: str):
    state = {}
    if STATE_FILE.exists():
        state = json.loads(STATE_FILE.read_text())
    state[icao] = date
    STATE_FILE.write_text(json.dumps(state))
Enter fullscreen mode Exit fullscreen mode

Check already_alerted before sending, call mark_alerted after. The state file resets naturally when the date changes.

Going further

This bot watches one station and one threshold. In Part 2 of this series, we'll follow a station through the full day: morning prediction, midday observations, and the final result from the history endpoint. Part 3 extends the pattern to all 12 tracked stations at once.

A few ideas to extend this bot on your own:

  • Add confidence gating. Only alert when confidence is 7 or higher, so you're not acting on shaky early-morning predictions.
  • Use slope for early signals. A steep positive slope (e.g. +2.0 °C/hour) with 3 hours until peak makes a high target more plausible.
  • Fetch the forecast for context. Call /api/v1/weather/:icao and compare forecast.maxTemp to your target before the prediction engine has enough data.

The full API reference covers all available endpoints and fields. Browse the stations index to find ICAO codes for all 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)