Admin guide

Contents

  1. System Overview
    1. Infrastructure at a Glance
    2. Key Directories
  2. Configuration File
    1. Config Blocks
    2. API Credentials
  3. Cron Jobs
    1. Tempest Weather Cron
    2. Moon Data Cron
    3. Camera / Timelapse Cron
  4. Cache Files
  5. PHP Classes
    1. ScooterCam_Tempest
    2. ScooterCam_Sunset_Predictor
    3. ScooterCam_Moonset_Predictor
    4. ScooterCam_SunMoon_Display
  6. Display Widgets
    1. Alert Banner
  7. Weather Alert System
  8. Wind Rose Data Accumulator
  9. Moon & Sunset Scoring
    1. Sunset Score Factors
    2. Notable Moonset Criteria
  10. Troubleshooting
    1. Common Issues
    2. Checking Cron Health
    3. Checking Cache File Health
    4. Log Files
  11. Notes for Future Administrators
    1. Most Likely Things to Need Attention
    2. What NOT to Change

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

ComponentDetails
Web hostGlowHost shared hosting
CDN / DNSCloudflare
Server path/home/scooterc/public_html
WordPress/home/scooterc/public_html/blog/
Pi accessRaspberry Pi Connect (remote) + SSHFS (file mount)
Dev environmentMac + BBEdit with SSHFS mount
PHP template systemCustom: header.php, footer.php, $page_config
TimezoneAmerica/Detroit (all timestamps)
Coordinates42.5593, −86.2361 (South Haven/Saugatuck area)

1.2 Key Directories

PathPurpose
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)

↑ Back to top


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

BlockPurpose
tempestTempest API token, station ID, device ID, cache durations, alert and wind rose cache paths
visual_crossingVisual Crossing API key and location string for 15-day forecast
openweatherOpenWeatherMap API key, lat/lon for marine and moonset weather data
sunmoonAll sun/moon/sunset scoring config: API keys, home + horizon coordinates, moon data file path, cache expiry
siteSite name, tagline, timezone, location name
pathsAbsolute filesystem paths for cameras, images, timelapse, logs, data directories
periodsTime period definitions for timelapse (morning, afternoon, evening, night)
camerasPer-camera configuration: name, display name, enabled flag, capture timing, capabilities

2.2 API Credentials

ServiceKey locationNotes
Tempest WeatherFlowconfig[tempest][token]Personal use token — does not expire
Visual Crossingconfig[visual_crossing][api_key]15-day forecast
OpenWeatherMapconfig[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.php once daily.

↑ Back to top


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:

StepWhat it doesOutput
1Fetch current conditions from Tempest station API$current observation array
2Fetch 10-day daily forecastdata/tempest/ cache files
3Fetch 24-hour hourly forecastdata/tempest/ cache files
4Fetch 24 hours of historical observations$historical array
5Derive weather alerts from $currentcache/sc_alerts.json
6Append wind direction/speed to rolling 30-day logcache/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.

↑ Back to top


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.

FileWritten byRead byNotes
sc_alerts.jsonTempest cron Step 5includes/alert-banner.phpRefreshed every 5 min. Stale after 10 min.
sc_wind_rose.jsonTempest cron Step 6widget-wind-rose.phpAppend-only, 30-day rolling window.
sc_moon_data.jsonmoon-cache-cron.phpScooterCam_SunMoon_Display, ScooterCam_Moonset_PredictorRefreshed 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.

↑ Back to top


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:

MethodDescription
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:

ConstantDefaultMeaning
HIGH_WIND_THRESHOLD_MPH25Gust speed that triggers a high wind alert
FREEZE_THRESHOLD_F32Temperature 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.

↑ Back to top


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 fileUsageData source
widget-tempest-chart.phpTemperature + wind 24-hour chartTempest get_historical(24) — already cached
widget-pressure-trend.phpPressure sparkline + trend badgeTempest get_historical(6) — already cached
widget-wind-rose.phpSVG 16-point wind rose, 30 dayscache/sc_wind_rose.json
widget-sunset-quality.phpSunset score + conditionsScooterCam_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.

↑ Back to top


7. Weather Alert System

Alert typeTrigger conditionClears whenCSS class
LightningStrike detected within past 60 minAutomatically, 60 min after last strike epoch.sc-alert--lightning
Rainprecip_type = 1 or precip rate > 0precip_type returns to 0.sc-alert--rain
Hailprecip_type = 2precip_type returns to 0.sc-alert--hail
High Windwind_gust ≥ 25 mphGust drops below threshold.sc-alert--high_wind
Freezeair_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.

↑ Back to top


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.

↑ Back to top


9. Moon & Sunset Scoring

9.1 Sunset Score Factors

FactorWeightOptimal condition
Cloud coverage40 pts25–55% sky cover (partial clouds catch light best)
Visibility25 pts10+ km (clear air = vivid color)
Humidity20 ptsBelow 45% RH (dry air = saturated color)
Wind speed15 pts5–15 mph (light movement)
Weather type15 ptsPartly cloudy
Horizon contrast20 ptsDifferent 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.

↑ Back to top


10. Troubleshooting

10.1 Common Issues

SymptomMost likely causeFix
Alert banner not showingsc_alerts.json missing or older than 10 minCheck 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 entriesNormal if Step 6 was recently added. Data accumulates over 30 days.
Moon phase shows stale datamoon-cache-cron.php not runningCheck crontab. Run manually and check logs/moon-cron.log
Sudden high API charges (RapidAPI)Moon cron being called at page loadVerify ScooterCam_Moonset_Predictor has no direct API calls. getMoonPhaseData() must only read the cache file.
PHP fatal: Cannot redeclare methodPatch was appended instead of replacing originalOpen the class file, search for duplicate method names, delete the old version.
Sunset score not displayingOutside display window (before noon or after sunset)Normal behavior. The widget is gated by is_sunset_score_window().
Timezone displayed wrongUTC timestamp formatted without timezone conversionUse 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 operation
  • cache/sc_wind_rose.json — should grow by one entry every 5 minutes
  • cache/sc_moon_data.json — should be less than 25 hours old
  • data/tempest/*.json — MD5-keyed files, auto-managed by ScooterCam_Tempest class

10.4 Log Files

Log fileContents
logs/tempest-cron.logTempest cron output — timestamps, step completion, any errors
logs/moon-cron.logMoon cron output — phase name, moonrise/set times, HTTP status
PHP error logStale cache warnings, API errors, class instantiation failures

↑ Back to top


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

  1. API keys expire or need rotation. Check includes/config.php for the relevant key.
  2. The Raspberry Pi loses connectivity. Access via Raspberry Pi Connect. The camctl scheduler auto-restarts on reboot.
  3. A cron job stops running. Use crontab -l to check, then re-add the missing line.
  4. GlowHost or Cloudflare settings change. DNS is managed through Cloudflare; hosting through the GlowHost control panel.
  5. 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 in config.php and 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.

↑ Back to top

Verified by MonsterInsights