Contents
- System Overview
- Configuration File
- Cron Jobs
- Cache Files
- PHP Classes
- Display Widgets
- Weather Alert System
- Wind Rose Data Accumulator
- Moon & Sunset Scoring
- Troubleshooting
- Notes for Future Administrators
1. System Overview
ScooterCam is a custom PHP-based weather monitoring and sunset photography system serving the Lake Michigan shoreline between South Haven and Saugatuck, Michigan. It is hosted on GlowHost shared hosting with Cloudflare as the DNS and CDN layer.
The system consists of a Raspberry Pi 5 at the shoreline running a Python scheduler (camctl), two Reolink IP cameras, a Tempest weather station, and a PHP web application on the server. A WordPress installation runs separately in the /blog/ subdirectory and is largely independent of the main application.
1.1 Infrastructure at a Glance
| Component | Details |
|---|---|
| Web host | GlowHost shared hosting |
| CDN / DNS | Cloudflare |
| Server path | /home/scooterc/public_html |
| WordPress | /home/scooterc/public_html/blog/ |
| Pi access | Raspberry Pi Connect (remote) + SSHFS (file mount) |
| Dev environment | Mac + BBEdit with SSHFS mount |
| PHP template system | Custom: header.php, footer.php, $page_config |
| Timezone | America/Detroit (all timestamps) |
| Coordinates | 42.5593, −86.2361 (South Haven/Saugatuck area) |
1.2 Key Directories
| Path | Purpose |
|---|---|
includes/ | PHP classes, config, widgets, alert banner |
includes/widgets/ | Individual display widgets (chart, pressure, wind rose, etc.) |
cache/ | All JSON cache files written by cron and API classes |
cron/ | All cron scripts — run via server crontab |
data/tempest/ | Tempest API response cache (MD5-keyed JSON files) |
logs/ | Cron output logs |
timelapse/ | Timelapse video output |
live/ | Live camera image staging |
blog/ | WordPress installation (independent) |
2. Configuration File
All site-wide settings live in a single file: includes/config.php. This file returns a PHP array and is loaded by every page and cron script. Never hardcode credentials or paths anywhere else.
includes/config.php is the single source of truth for all API keys, credentials, paths, and settings. If something stops working, check here first.
2.1 Config Blocks
| Block | Purpose |
|---|---|
tempest | Tempest API token, station ID, device ID, cache durations, alert and wind rose cache paths |
visual_crossing | Visual Crossing API key and location string for 15-day forecast |
openweather | OpenWeatherMap API key, lat/lon for marine and moonset weather data |
sunmoon | All sun/moon/sunset scoring config: API keys, home + horizon coordinates, moon data file path, cache expiry |
site | Site name, tagline, timezone, location name |
paths | Absolute filesystem paths for cameras, images, timelapse, logs, data directories |
periods | Time period definitions for timelapse (morning, afternoon, evening, night) |
cameras | Per-camera configuration: name, display name, enabled flag, capture timing, capabilities |
2.2 API Credentials
| Service | Key location | Notes |
|---|---|---|
| Tempest WeatherFlow | config[tempest][token] | Personal use token — does not expire |
| Visual Crossing | config[visual_crossing][api_key] | 15-day forecast |
| OpenWeatherMap | config[openweather][api_key] | Used by sunset + moonset scoring |
| RapidAPI (moon-phase) | config[sunmoon][moon_api_key] | Called only by cron — never at page load |
⚠️ The RapidAPI moon-phase key must never be called from a page load. In March 2026 a caching failure caused 3,200% quota overrun and significant unexpected charges. Moon data is now fetched exclusively by
cron/moon-cache-cron.phponce daily.
3. Cron Jobs
All time-sensitive data is populated by cron scripts, never at page load time. This is critical for performance on shared hosting. Run crontab -l to verify all entries are active.
3.1 Tempest Weather Cron
File: cron/tempest-cache-cron.php
Schedule: Every 5 minutes — */5 * * * *
Log: logs/tempest-cron.log
This is the main data cron. It runs six steps in sequence, each reusing data from earlier steps to avoid redundant API calls:
| Step | What it does | Output |
|---|---|---|
| 1 | Fetch current conditions from Tempest station API | $current observation array |
| 2 | Fetch 10-day daily forecast | data/tempest/ cache files |
| 3 | Fetch 24-hour hourly forecast | data/tempest/ cache files |
| 4 | Fetch 24 hours of historical observations | $historical array |
| 5 | Derive weather alerts from $current | cache/sc_alerts.json |
| 6 | Append wind direction/speed to rolling 30-day log | cache/sc_wind_rose.json |
3.2 Moon Data Cron
File: cron/moon-cache-cron.php
Schedule: Once daily at 1 AM — 0 1 * * *
Log: logs/moon-cron.log
Fetches moon phase data from RapidAPI (moon-phase.p.rapidapi.com) and sun data via PHP’s built-in date_sun_info(). Writes a normalized JSON file to cache/sc_moon_data.json. All moon and sun display on the site reads this file — zero API calls happen at page load.
A lock file (sys_get_temp_dir()/sc_moon_cron.lock) prevents duplicate runs within the same hour in case of crontab misconfiguration. If the cache file is more than 36 hours old, a warning is written to the PHP error log; the old data continues to display rather than showing an error.
3.3 Camera / Timelapse Cron (Pi-side)
The Raspberry Pi 5 runs its own Python scheduler (camctl) which handles image capture, timelapse compilation, and FTP upload to the server. This is separate from the server-side cron jobs above. See the camera system documentation for details.
4. Cache Files
All cache files live in the cache/ directory at the web root. The directory must be writable by the web server user. Files are written atomically (tmp file then rename) to prevent partial reads.
| File | Written by | Read by | Notes |
|---|---|---|---|
sc_alerts.json | Tempest cron Step 5 | includes/alert-banner.php | Refreshed every 5 min. Stale after 10 min. |
sc_wind_rose.json | Tempest cron Step 6 | widget-wind-rose.php | Append-only, 30-day rolling window. |
sc_moon_data.json | moon-cache-cron.php | ScooterCam_SunMoon_Display, ScooterCam_Moonset_Predictor | Refreshed daily at 1 AM. |
Tempest API response files are cached in data/tempest/ using MD5-keyed filenames, managed automatically by the ScooterCam_Tempest class. OpenWeatherMap responses for the moonset predictor are cached in sys_get_temp_dir() for 30 minutes using the pattern sc_owm_{md5}.json.
5. PHP Classes
5.1 ScooterCam_Tempest
File: includes/tempest.php
The main weather data class. Wraps all Tempest WeatherFlow API calls with file-based caching. Key methods:
| Method | Description |
|---|---|
get_current() | Returns latest station observation array. Cached per config[tempest][cache_current]. |
get_historical($hours) | Returns obs array for past N hours from device endpoint. |
get_forecast_daily() | Returns 10-day daily forecast array. |
get_forecast_hourly($hours) | Returns N-hour hourly forecast array. |
get_alerts($cache_path) | Derives 5 alert types from current obs. Reads/writes sc_alerts.json for onset tracking. |
get_temperature_trend($hours) | Returns trend direction and change amount over past N hours. |
degrees_to_cardinal($deg) | Converts wind degrees to 16-point compass label. |
Alert thresholds are defined as class constants and can be adjusted without touching logic:
| Constant | Default | Meaning |
|---|---|---|
HIGH_WIND_THRESHOLD_MPH | 25 | Gust speed that triggers a high wind alert |
FREEZE_THRESHOLD_F | 32 | Temperature at or below which freeze alert fires |
5.2 ScooterCam_Sunset_Predictor
File: includes/class-sunset-predictor.php
Scores tonight’s sunset quality 0–100 using two OpenWeatherMap calls: one for the observer’s location and one for a horizon point ~20 miles west over the lake (42.5593, −86.6461). Scoring factors: cloud coverage (40%), visibility (25%), humidity (20%), wind speed (15%), weather type (15%), horizon contrast (20%). Weights are normalized in calculateScore().
getCurrentScore() returns an array with score (int), grade (letter), description (string), and breakdown (per-factor array). The is_sunset_score_window() method on ScooterCam_SunMoon_Display gates display, returning true only between noon and sunset.
5.3 ScooterCam_Moonset_Predictor
File: includes/class-moonset-predictor.php
Determines whether today features a notable moonset over Lake Michigan (full moon ≥80% illumination, setting 5–9 AM, in the western sky azimuth 225–315°). If notable, scores viewing conditions using OWM cloud/visibility data.
Moon phase data is read exclusively from cache/sc_moon_data.json written by the daily cron. No RapidAPI calls occur at page load. OWM weather calls are cached 30 minutes in the system temp directory.
5.4 ScooterCam_SunMoon_Display
File: includes/class-sunmoon-display.php
Renders all sun and moon HTML components: sunset score card, moonset score card, moon phase calendar, sun arc SVG, moon arc SVG, and current moon phase details. All time formatting uses the configured timezone via the private format_timestamp() helper to prevent UTC display bugs.
6. Display Widgets
Widgets are self-contained PHP includes in includes/widgets/. Each locates its own data source and renders HTML. Drop into any page template with a single include line.
| Widget file | Usage | Data source |
|---|---|---|
widget-tempest-chart.php | Temperature + wind 24-hour chart | Tempest get_historical(24) — already cached |
widget-pressure-trend.php | Pressure sparkline + trend badge | Tempest get_historical(6) — already cached |
widget-wind-rose.php | SVG 16-point wind rose, 30 days | cache/sc_wind_rose.json |
widget-sunset-quality.php | Sunset score + conditions | ScooterCam_Sunset_Predictor + Tempest (pressure, wind direction) |
All widgets check for a $tempest instance in scope before creating one, and check for $historical if already loaded, to prevent redundant instantiation when multiple widgets appear on the same page. Widget CSS is scoped with a widget-specific prefix to prevent conflicts with the main stylesheet or WordPress.
6.1 Alert Banner
File: includes/alert-banner.php
Usage: <?php include __DIR__ . '/includes/alert-banner.php'; ?>
Reads cache/sc_alerts.json and renders colored dismissible banners for active weather events. Stale cache protection: condition-based alerts are suppressed if the cache is older than 10 minutes. Lightning alerts bypass this check since they use their own epoch-based expiry.
Dismissal is tracked in sessionStorage keyed by event epoch/onset, so a new event of the same type (a new lightning strike, for example) re-displays even if the previous one was dismissed.
7. Weather Alert System
| Alert type | Trigger condition | Clears when | CSS class |
|---|---|---|---|
| Lightning | Strike detected within past 60 min | Automatically, 60 min after last strike epoch | .sc-alert--lightning |
| Rain | precip_type = 1 or precip rate > 0 | precip_type returns to 0 | .sc-alert--rain |
| Hail | precip_type = 2 | precip_type returns to 0 | .sc-alert--hail |
| High Wind | wind_gust ≥ 25 mph | Gust drops below threshold | .sc-alert--high_wind |
| Freeze | air_temp ≤ 32°F (0°C) | Temperature rises above threshold | .sc-alert--freeze |
All alert data is derived from the Tempest station observation endpoint — the same endpoint used for current conditions. No separate alert API exists. Onset timestamps are preserved across cron runs by reading the previous sc_alerts.json before writing the new one, allowing the banner to show “started 23 minutes ago” accurately.
To adjust thresholds, change the HIGH_WIND_THRESHOLD_MPH and FREEZE_THRESHOLD_F constants at the top of the ScooterCam_Tempest class in includes/tempest.php.
8. Wind Rose Data Accumulator
Step 6 of the Tempest cron appends one record to cache/sc_wind_rose.json every 5 minutes:
{ "t": epoch, "spd": mph, "gst": mph, "dir": degrees }
Records older than 30 days are pruned on each cron run. Calm entries (0/0 speed) are skipped. After 30 days the file contains approximately 8,640 entries at roughly 300KB.
The wind rose widget bins these into 16 compass sectors (22.5° each) and four speed bands, then renders a pure SVG rose scaled so the busiest direction fills 88% of the maximum radius. No JavaScript is required.
9. Moon & Sunset Scoring
9.1 Sunset Score Factors
| Factor | Weight | Optimal condition |
|---|---|---|
| Cloud coverage | 40 pts | 25–55% sky cover (partial clouds catch light best) |
| Visibility | 25 pts | 10+ km (clear air = vivid color) |
| Humidity | 20 pts | Below 45% RH (dry air = saturated color) |
| Wind speed | 15 pts | 5–15 mph (light movement) |
| Weather type | 15 pts | Partly cloudy |
| Horizon contrast | 20 pts | Different conditions at observer vs. horizon point over lake |
9.2 Notable Moonset Criteria
A moonset is flagged as notable only when all three conditions are met simultaneously:
- Moon illumination is 80% or greater (nearly or fully full)
- Moonset time is between 5:00 AM and 9:00 AM local (America/Detroit)
- Moon azimuth at setting is between 225° and 315° (western sky, over the lake)
This event occurs roughly 2–3 times per year. All moonset logic reads from cache/sc_moon_data.json — no runtime API calls.
10. Troubleshooting
10.1 Common Issues
| Symptom | Most likely cause | Fix |
|---|---|---|
| Alert banner not showing | sc_alerts.json missing or older than 10 min | Check Tempest cron is running. Run manually: php cron/tempest-cache-cron.php |
| Wind rose shows “not enough data” | sc_wind_rose.json missing or fewer than 10 entries | Normal if Step 6 was recently added. Data accumulates over 30 days. |
| Moon phase shows stale data | moon-cache-cron.php not running | Check crontab. Run manually and check logs/moon-cron.log |
| Sudden high API charges (RapidAPI) | Moon cron being called at page load | Verify ScooterCam_Moonset_Predictor has no direct API calls. getMoonPhaseData() must only read the cache file. |
| PHP fatal: Cannot redeclare method | Patch was appended instead of replacing original | Open the class file, search for duplicate method names, delete the old version. |
| Sunset score not displaying | Outside display window (before noon or after sunset) | Normal behavior. The widget is gated by is_sunset_score_window(). |
| Timezone displayed wrong | UTC timestamp formatted without timezone conversion | Use format_timestamp() helper in SunMoon_Display, or new DateTime('@'.$ts)->setTimezone($tz) |
10.2 Checking Cron Health
To verify all crons are registered:
crontab -l
Expected entries:
*/5 * * * *— Tempest cache cron (tempest-cache-cron.php)0 1 * * *— Moon data cron (moon-cache-cron.php)
To run a cron manually and see its output:
php /home/scooterc/public_html/cron/tempest-cache-cron.php
php /home/scooterc/public_html/cron/moon-cache-cron.php
10.3 Checking Cache File Health
cache/sc_alerts.json— should be less than 10 minutes old during normal operationcache/sc_wind_rose.json— should grow by one entry every 5 minutescache/sc_moon_data.json— should be less than 25 hours olddata/tempest/*.json— MD5-keyed files, auto-managed by ScooterCam_Tempest class
10.4 Log Files
| Log file | Contents |
|---|---|
logs/tempest-cron.log | Tempest cron output — timestamps, step completion, any errors |
logs/moon-cron.log | Moon cron output — phase name, moonrise/set times, HTTP status |
| PHP error log | Stale cache warnings, API errors, class instantiation failures |
11. Notes for Future Administrators
This section is written for whoever takes over maintenance of ScooterCam. No prior knowledge of the codebase is assumed.
ScooterCam has been designed with succession in mind. The goal is that a reasonably capable person — not necessarily a developer — can keep it running with straightforward maintenance. Here is the philosophy:
One config file. All credentials, paths, and settings are in includes/config.php. If an API key expires or a path changes, that is the only file that needs updating.
Cron-first data. Nothing that costs money (API calls) happens at page load. All paid API calls run on a schedule via cron scripts. Pages only read files. This prevents runaway costs from traffic spikes.
Flat JSON caches. Data is stored as plain JSON files in the cache/ directory. You can open and read these files in any text editor to verify what the site is currently seeing.
Graceful degradation. Every widget checks whether its data file exists and is fresh before displaying. If something is missing, the widget shows a friendly notice rather than a PHP error.
Atomic file writes. Cache files are written to a .tmp file first, then renamed. This means a page load can never catch a file mid-write.
11.1 Most Likely Things to Need Attention
- API keys expire or need rotation. Check
includes/config.phpfor the relevant key. - The Raspberry Pi loses connectivity. Access via Raspberry Pi Connect. The camctl scheduler auto-restarts on reboot.
- A cron job stops running. Use
crontab -lto check, then re-add the missing line. - GlowHost or Cloudflare settings change. DNS is managed through Cloudflare; hosting through the GlowHost control panel.
- WordPress blog at /blog/ needs updates. Plugin and security updates are independent of the main site.
11.2 What NOT to Change
- Do not move
cache/outside the web root without updating all cache path references inconfig.phpand the cron scripts. - Do not change the JSON structure written by the Tempest cron (Steps 5 and 6) without also updating the widgets that read those files.
- Do not add RapidAPI calls anywhere except inside
cron/moon-cache-cron.php. - Do not modify the obs_st array indices used in ScooterCam_Tempest without verifying against the current Tempest API documentation.