mostlyright.weather.cwop
mostlyright.weather.cwop
Section titled “mostlyright.weather.cwop”mostlyright.weather.cwop — standalone CWOP (Citizen Weather Observer
Program) live adapter.
CWOP is Personal Weather Station (PWS) data delivered over the APRS-IS TCP
network — ~7K active North American stations, the ONLY real-time path being a
socket stream (no REST). This module is deliberately STANDALONE: it never feeds
research(), the merge layer, or live._sources, and it carries its own
schema.cwop.v1 (NOT schema.observation.v1). A hot-rooftop or indoor PWS
must never silently corrupt a Kalshi NHIGH/NLOW settlement target, so CWOP stays
on its own island with a 6-layer QC pipeline (see weather.qc._cwop).
Public surface (all source-tagged "cwop.live"):
nearby()— the “start here” entry point: resolve an ICAO/NWS station to coordinates and scan around it.scan()— discover stations near a lat/lon by listening on APRS-IS.stream()— async generator of fresh observations.snapshot()— collect a fixed window into aschema.cwop.v1DataFrame (passpersist=Trueto also write it to the backtest cache).latest()— the most-recent observation for one station.
Persistence + backtest (v0.2):
history()— replay persisted CWOP observations for a date range as aschema.cwop.v1DataFrame (source="cwop.cache"). The parity-safe access path for ML strategies — CWOP is NEVER wired intoresearch()/ merge /live._sources; a strategy joins this frame itself.persist_observations()— write a list of observations to the monthly parquet cache (the collection seam forsnapshot()/stream()output).
Data classes: CWOPStation, CWOPObservation.
Errors: NoCWOPDataError.
Importing this module registers schema.cwop.v1 with the validator (lazy —
a base install that never touches CWOP pays nothing).
class mostlyright.weather.cwop.CWOPObservation(station_id, observed_at, knowledge_time, lat, lon, raw_aprs, temp_f=None, humidity=None, pressure_mb=None, wind_speed_mph=None, wind_dir_deg=None, wind_gust_mph=None, rain_1h_in=None, luminosity=None, source=‘cwop.live’, qc_score=None, qc_status=‘unknown’)
Section titled “class mostlyright.weather.cwop.CWOPObservation(station_id, observed_at, knowledge_time, lat, lon, raw_aprs, temp_f=None, humidity=None, pressure_mb=None, wind_speed_mph=None, wind_dir_deg=None, wind_gust_mph=None, rain_1h_in=None, luminosity=None, source=‘cwop.live’, qc_score=None, qc_status=‘unknown’)”Bases: object
One CWOP weather observation (the row the public surface yields).
observed_at is the APRS packet timestamp (event-time); knowledge_time
is when our socket received the packet (the event_time + lag convention
— for CWOP the lag is the network/queue delay). qc_score/qc_status
are populated by the QC pipeline when qc=True; otherwise they stay
None/"unknown".
- Parameters:
- station_id (str)
- observed_at (datetime)
- knowledge_time (datetime)
- lat (float | None)
- lon (float | None)
- raw_aprs (str)
- temp_f (float | None)
- humidity (int | None)
- pressure_mb (float | None)
- wind_speed_mph (float | None)
- wind_dir_deg (int | None)
- wind_gust_mph (float | None)
- rain_1h_in (float | None)
- luminosity (int | None)
- source (str)
- qc_score (float | None)
- qc_status (str)
humidity : int | None = None
Section titled “humidity : int | None = None”knowledge_time : datetime
Section titled “knowledge_time : datetime”luminosity : int | None = None
Section titled “luminosity : int | None = None”observed_at : datetime
Section titled “observed_at : datetime”pressure_mb : float | None = None
Section titled “pressure_mb : float | None = None”qc_score : float | None = None
Section titled “qc_score : float | None = None”qc_status : str = ‘unknown’
Section titled “qc_status : str = ‘unknown’”rain_1h_in : float | None = None
Section titled “rain_1h_in : float | None = None”raw_aprs : str
Section titled “raw_aprs : str”source : str = ‘cwop.live’
Section titled “source : str = ‘cwop.live’”station_id : str
Section titled “station_id : str”temp_f : float | None = None
Section titled “temp_f : float | None = None”wind_dir_deg : int | None = None
Section titled “wind_dir_deg : int | None = None”wind_gust_mph : float | None = None
Section titled “wind_gust_mph : float | None = None”wind_speed_mph : float | None = None
Section titled “wind_speed_mph : float | None = None”class mostlyright.weather.cwop.CWOPStation(id, lat, lon, distance_km, last_obs, obs_count, last_temp_f, report_cadence_s, qc_status=‘unknown’)
Section titled “class mostlyright.weather.cwop.CWOPStation(id, lat, lon, distance_km, last_obs, obs_count, last_temp_f, report_cadence_s, qc_status=‘unknown’)”Bases: object
A CWOP station discovered on the APRS-IS stream.
distance_km is the great-circle distance from the scan centre.
report_cadence_s is the median seconds between this station’s reports
(None until it has reported twice). qc_status is a forward-looking trust
signal, "unknown" until a QC pass annotates the station.
- Parameters:
distance_km : float
Section titled “distance_km : float”last_obs : datetime
Section titled “last_obs : datetime”last_temp_f : float | None
Section titled “last_temp_f : float | None”obs_count : int
Section titled “obs_count : int”qc_status : str = ‘unknown’
Section titled “qc_status : str = ‘unknown’”report_cadence_s : float | None
Section titled “report_cadence_s : float | None”exception mostlyright.weather.cwop.NoCWOPDataError(station, reason, , request_id=None, error_code=None)
Section titled “exception mostlyright.weather.cwop.NoCWOPDataError(station, reason, , request_id=None, error_code=None)”Bases: NoLiveDataError
No CWOP (APRS-IS) data available for the requested station/area.
Raised by the standalone mostlyright.weather.cwop surface
(scan/nearby/stream/snapshot/latest) instead of
returning []/None, so callers get an actionable signal — CWOP is
a live-only TCP stream and “nothing arrived” is the common failure mode
(the station never reported, the area is empty, or the listen window was
too short). Carries the resolved station/area identifier, the fixed
source="cwop.live" tag, and a human-readable reason.
default_error_code : str = ‘NO_CWOP_DATA’
Section titled “default_error_code : str = ‘NO_CWOP_DATA’”Subclass override — the stable string enum surfaced via error_code.
error_code : str
Section titled “error_code : str”message : str
Section titled “message : str”reason : str
Section titled “reason : str”request_id : str | None
Section titled “request_id : str | None”source : str | None
Section titled “source : str | None”station : str
Section titled “station : str”mostlyright.weather.cwop.history(station, from_date, to_date, , qc_status=None)
Section titled “mostlyright.weather.cwop.history(station, from_date, to_date, , qc_status=None)”Return persisted CWOP observations for station in [from_date, to_date].
The backtest-replay entry point: reads the monthly parquet cache and returns
a schema.cwop.v1 DataFrame (source="cwop.cache", validated before
return) that an ML strategy can join against its official-obs features. The
date range is inclusive on both ends; bare date arguments cover the
whole calendar day in UTC.
- Parameters:
- station (
str) – CWOP station id (e.g."CW0875"). - from_date (
date|datetime) – window start (inclusive).dateordatetime. - to_date (
date|datetime) – window end (inclusive).dateordatetime. - qc_status (
str|None) – optional filter — keep only rows with this QC verdict ("clean"/"flagged"/"dropped"/"unknown"). A backtest typically passes"clean"to drop QC-rejected PWS rows.
- station (
- Return type:
DataFrame - Returns:
A
schema.cwop.v1DataFrame sorted byobserved_at. - Raises:
- ValueError –
from_dateis afterto_date, orqc_statusis not a recognized QC verdict. - NoCWOPDataError – no persisted CWOP data in range (after the optional
qc_statusfilter) — never returns an empty frame.
- ValueError –
mostlyright.weather.cwop.latest(station, , timeout_seconds=120)
Section titled “mostlyright.weather.cwop.latest(station, , timeout_seconds=120)”Return the most-recent observation for a CWOP station. Synchronous.
Blocks until the first packet arrives or timeout_seconds elapses.
- Raises: NoCWOPDataError – nothing received within the timeout.
- Return type:
CWOPObservation - Parameters:
mostlyright.weather.cwop.nearby(station, , radius_km=25.0, listen_seconds=60)
Section titled “mostlyright.weather.cwop.nearby(station, , radius_km=25.0, listen_seconds=60)”Resolve an ICAO/NWS station to coordinates, then scan() around it.
The “start here” entry point for most users.
- Raises: NoCWOPDataError – the station code is unknown, or no CWOP stations reported nearby.
- Return type:
list[CWOPStation] - Parameters:
mostlyright.weather.cwop.persist_observations(observations)
Section titled “mostlyright.weather.cwop.persist_observations(observations)”Persist live CWOPObservation rows to the monthly parquet cache.
Groups observations by (station_id, year, month) of their UTC
observed_at (so a window straddling a month boundary lands in the right
partitions) and merge-writes each partition. Returns the number of distinct
observations written (post-dedup is applied per partition by
write_cwop_cache(); this count is the number SUBMITTED).
This is the collection-side seam: feed it the output of snapshot() /
stream() to build a persisted CWOP history for backtesting.
- Return type:
int - Parameters: observations (list [CWOPObservation ])
mostlyright.weather.cwop.scan(lat, lon, radius_km=25.0, , listen_seconds=60, min_reports=1)
Section titled “mostlyright.weather.cwop.scan(lat, lon, radius_km=25.0, , listen_seconds=60, min_reports=1)”Scan APRS-IS for CWOP stations near (lat, lon). Synchronous.
Cache-then-augment: when stations.json already holds at least
min_reports individually-fresh (<24h since their own last_obs)
stations inside radius_km, they are returned immediately; a stale or
sparse cache triggers a live listen of up to listen_seconds (early-exit
once min_reports stations are seen). Live results are merged back into
the registry.
Returns stations sorted nearest-first.
- Raises: NoCWOPDataError – no stations found after the listen window.
- Return type:
list[CWOPStation] - Parameters:
mostlyright.weather.cwop.snapshot(station, , duration_seconds=60, qc=True, persist=False)
Section titled “mostlyright.weather.cwop.snapshot(station, , duration_seconds=60, qc=True, persist=False)”Collect CWOP observations for duration_seconds into a DataFrame.
Synchronous. Returns a schema.cwop.v1 frame (validated before return),
with QC scores/status populated when qc=True.
When persist=True the collected observations are also written to the
monthly parquet cache ($HOME/.mostlyright/cache/cwop/...) so they can be
replayed later via mostlyright.weather.cwop.history() for backtesting.
Persistence is a side effect — the returned (live) frame is unchanged and
still tagged source="cwop.live"; only the on-disk rows carry the
"cwop.cache" provenance tag.
- Raises: NoCWOPDataError – no observations arrived during the window.
- Return type:
DataFrame - Parameters:
async mostlyright.weather.cwop.stream(station, , qc=True)
Section titled “async mostlyright.weather.cwop.stream(station, , qc=True)”Yield fresh CWOP observations for station (one or many). Async.
Dedups by (station_id, observed_at); applies the QC pipeline when
qc=True (default). Source tag is "cwop.live".
- Raises:
NoCWOPDataError – the station never reported within
FIRST_OBS_TIMEOUT_S(raised before the first yield only). - Return type:
AsyncIterator[CWOPObservation] - Parameters: