Skip to content

Commit 113c789

Browse files
committed
Add Digitraffic weathercam publisher
1 parent 9addd1f commit 113c789

6 files changed

Lines changed: 672 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Finland Digitraffic Weathercam Live Smoke
2+
3+
Date: 2026-05-29
4+
5+
## Summary
6+
7+
Phase 2 Finland datasource is live on the OSH CSAPI server using Fintraffic Digitraffic weather camera latest-data and JPEG endpoints.
8+
9+
Resources created without `--clean`:
10+
11+
- Procedure: `urn:os4csapi:procedure:digitraffic-weathercam:v1`
12+
- Root deployment: `urn:os4csapi:deployment:digitraffic-weathercam-demo:v1`
13+
- Camera preset group deployment: `urn:os4csapi:deployment:digitraffic-weathercam-presets:v1`
14+
- Six companion `digitrafficWeatherCamImage` datastreams attached to the existing Digitraffic road-weather station systems
15+
- Six camera preset deployments linked to the existing road-weather station systems
16+
17+
The publisher uses each preset's measured time as `phenomenonTime` and publishes image-reference results with direct full-size JPEG and thumbnail URLs.
18+
19+
## Selected Cameras
20+
21+
| Road station | System ID | Camera station | Preset ID | Datastream ID | Match note |
22+
| --- | --- | --- | --- | --- | --- |
23+
| 1014 / vt25_Hanko | 05u0 | C01507 / vt25_Hanko | C0150701 | 078g2 | Co-located, 0.03 km |
24+
| 1003 / st110_Vihti_Myllylampi | 05ug | C01506 / vt25_Myllylampi | C0150601 | 07902 | Co-located, 0.04 km |
25+
| 2002 / vt8_Pyharanta_Ihode | 05v0 | C02551 / vt8_Laitila_Palttila | C0255101 | 079g2 | Nearest fresh VT8 camera, 10.88 km |
26+
| 3036 / vt6_Lappeenranta_Karki | 05vg | C03572 / vt6_Lappeenranta_Lavola | C0357201 | 07a02 | Nearby VT6 camera, 1.02 km |
27+
| 4010 / vt12_Hollola_Hameenkoski | 06002 | C04541 / kt54_Karkola_Jarvela | C0454101 | 07ag2 | Nearest fresh road camera, 13.82 km |
28+
| 12091 / kt92_Inari_Naatamo | 060g2 | C14516 / kt92_Inari_Naatamo | C1451601 | 07b02 | Co-located, 0.0 km |
29+
30+
## Live Publish Result
31+
32+
One publisher cycle posted six observations successfully:
33+
34+
- Published: 6
35+
- Skipped: 0
36+
- Errors: 0
37+
38+
Latest public API verification through the Explorer proxy confirmed `mediaType=image/jpeg`, expected `camId`, and HTTP 200 for both `imageUrl` and `thumbUrl` for all six datastreams.
39+
40+
| Station / preset | Datastream ID | Latest phenomenon time | Image | Thumbnail |
41+
| --- | --- | --- | --- | --- |
42+
| 1014 / C0150701 | 078g2 | 2026-05-29T21:15:48Z | 200 | 200 |
43+
| 1003 / C0150601 | 07902 | 2026-05-29T21:22:40Z | 200 | 200 |
44+
| 2002 / C0255101 | 079g2 | 2026-05-29T21:22:56Z | 200 | 200 |
45+
| 3036 / C0357201 | 07a02 | 2026-05-29T21:22:54Z | 200 | 200 |
46+
| 4010 / C0454101 | 07ag2 | 2026-05-29T21:14:07Z | 200 | 200 |
47+
| 12091 / C1451601 | 07b02 | 2026-05-29T21:20:55Z | 200 | 200 |
48+
49+
## Verification Commands
50+
51+
Validated:
52+
53+
- `py -m py_compile publishers/digitraffic_weathercam/bootstrap_digitraffic_weathercam.py publishers/digitraffic_weathercam/digitraffic_weathercam_publisher.py`
54+
- `py publishers/digitraffic_weathercam/digitraffic_weathercam_publisher.py --dry-run --once`
55+
- `py publishers/digitraffic_weathercam/bootstrap_digitraffic_weathercam.py --dry-run --force-sml`
56+
- `py publishers/digitraffic_weathercam/bootstrap_digitraffic_weathercam.py --force-sml`
57+
- `py publishers/digitraffic_weathercam/digitraffic_weathercam_publisher.py --once`
58+
- Public proxy latest observation checks for datastreams `078g2`, `07902`, `079g2`, `07a02`, `07ag2`, and `07b02`
59+
60+
Explorer follow-up: no app code change was needed. The existing deployed-system card camera detection matches `Digitraffic Weather Camera Image`, and the existing `FIN Road Wx` source classifier already matches Digitraffic datastream names.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Digitraffic Weathercam Publisher
2+
3+
Publishes image-reference observations for a curated set of Fintraffic Digitraffic road-weather camera presets associated with the Phase 1 Finland road-weather stations.
4+
5+
Source endpoints:
6+
7+
- Camera metadata: `https://tie.digitraffic.fi/api/weathercam/v1/stations`
8+
- Station-specific latest preset metadata: `https://tie.digitraffic.fi/api/weathercam/v1/stations/{id}/data`
9+
- Latest preset image: `https://weathercam.digitraffic.fi/{presetId}.jpg`
10+
- Latest preset thumbnail: `https://weathercam.digitraffic.fi/{presetId}.jpg?thumbnail=true`
11+
12+
The publisher attaches one companion `digitrafficWeatherCamImage` datastream to each existing Digitraffic road-weather station system. This lets deployed-system cards render the camera imagery for the same station card.
13+
14+
## Bootstrap
15+
16+
```powershell
17+
py publishers/digitraffic_weathercam/bootstrap_digitraffic_weathercam.py --dry-run
18+
py publishers/digitraffic_weathercam/bootstrap_digitraffic_weathercam.py --force-sml
19+
```
20+
21+
## Publish
22+
23+
```powershell
24+
py publishers/digitraffic_weathercam/digitraffic_weathercam_publisher.py --dry-run --once
25+
py publishers/digitraffic_weathercam/digitraffic_weathercam_publisher.py --once
26+
```
27+
28+
Default publisher interval is 300 seconds.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Digitraffic weather camera publisher package."""
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/usr/bin/env python3
2+
"""Bootstrap curated Finnish Digitraffic weather camera CSAPI resources."""
3+
4+
import argparse
5+
import json
6+
import os
7+
import sys
8+
9+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
10+
from publishers.bootstrap_helpers import (
11+
get_config, _auth_header,
12+
ensure_procedure, ensure_datastream, ensure_deployment,
13+
find_by_uid, clean_resource, add_bootstrap_args, print_summary,
14+
)
15+
16+
17+
VALID_TIME_START = "2026-01-01T00:00:00Z"
18+
PUBLISH_INTERVAL_SECONDS = 300
19+
20+
PROC_UID = "urn:os4csapi:procedure:digitraffic-weathercam:v1"
21+
DEPLOY_ROOT_UID = "urn:os4csapi:deployment:digitraffic-weathercam-demo:v1"
22+
DEPLOY_GROUP_UID = "urn:os4csapi:deployment:digitraffic-weathercam-presets:v1"
23+
DS_OUTPUT_NAME = "digitrafficWeatherCamImage"
24+
25+
DIGITRAFFIC_HOME = "https://www.digitraffic.fi/en/road-traffic/"
26+
DIGITRAFFIC_LICENSE = "https://www.digitraffic.fi/en/terms-of-use/"
27+
DIGITRAFFIC_WEATHERCAM_STATIONS = "https://tie.digitraffic.fi/api/weathercam/v1/stations"
28+
DIGITRAFFIC_WEATHERCAM_IMAGE_BASE = "https://weathercam.digitraffic.fi"
29+
30+
31+
def _load_cameras() -> list[dict]:
32+
here = os.path.dirname(os.path.abspath(__file__))
33+
with open(os.path.join(here, "cameras.json"), encoding="utf-8") as file:
34+
return json.load(file)["cameras"]
35+
36+
37+
def _system_uid(road_weather_station_id: str) -> str:
38+
return f"urn:os4csapi:system:digitraffic-road-weather:{road_weather_station_id}:v1"
39+
40+
41+
def _deploy_uid(camera: dict) -> str:
42+
return f"urn:os4csapi:deployment:digitraffic-weathercam-{camera['presetId']}:v1"
43+
44+
45+
def _datastream_uid(camera: dict) -> str:
46+
return f"urn:os4csapi:datastream:digitraffic-weathercam:{camera['presetId']}:digitrafficWeatherCamImage:v1"
47+
48+
49+
def _station_data_url(camera_station_id: str) -> str:
50+
return f"https://tie.digitraffic.fi/api/weathercam/v1/stations/{camera_station_id}/data"
51+
52+
53+
def _image_url(preset_id: str) -> str:
54+
return f"{DIGITRAFFIC_WEATHERCAM_IMAGE_BASE}/{preset_id}.jpg"
55+
56+
57+
def _thumb_url(preset_id: str) -> str:
58+
return f"{_image_url(preset_id)}?thumbnail=true"
59+
60+
61+
PROCEDURE_STUB = {
62+
"type": "Feature",
63+
"geometry": None,
64+
"properties": {
65+
"uid": PROC_UID,
66+
"featureType": "sosa:ObservingProcedure",
67+
"name": "Digitraffic Weather Camera Image v1",
68+
"description": "Publishes image-reference observations from curated Fintraffic Digitraffic road-weather camera presets.",
69+
"validTime": [VALID_TIME_START, ".."],
70+
},
71+
}
72+
73+
PROCEDURE_SML = {
74+
"type": "SimpleProcess",
75+
"id": PROC_UID,
76+
"uniqueId": PROC_UID,
77+
"definition": "sosa:ObservingProcedure",
78+
"label": "Digitraffic Weather Camera Image v1",
79+
"description": (
80+
"Polls selected Fintraffic Digitraffic road-weather camera preset metadata, "
81+
"derives direct JPEG and thumbnail URLs, and publishes image-reference observations "
82+
"on the companion Digitraffic road-weather station systems."
83+
),
84+
"keywords": ["Fintraffic", "Digitraffic", "Finland", "road weather", "weather camera", "image reference"],
85+
"documents": [
86+
{"role": "http://dbpedia.org/resource/Web_page", "name": "Digitraffic Road Traffic", "link": {"href": DIGITRAFFIC_HOME, "type": "text/html"}},
87+
{"role": "http://dbpedia.org/resource/Web_page", "name": "Digitraffic Weather Cameras", "link": {"href": DIGITRAFFIC_WEATHERCAM_STATIONS, "type": "application/json"}},
88+
{"role": "http://dbpedia.org/resource/Web_page", "name": "Digitraffic Terms of Use", "link": {"href": DIGITRAFFIC_LICENSE, "type": "text/html"}},
89+
],
90+
"contacts": [
91+
{"role": "operator", "organisationName": "Fintraffic / Digitraffic", "contactInfo": {"onlineResource": {"linkage": DIGITRAFFIC_HOME}}},
92+
{"role": "publisher", "organisationName": "OS4CSAPI", "contactInfo": {"onlineResource": {"linkage": "https://github.com/OS4CSAPI/OSHConnect-Python"}}},
93+
],
94+
}
95+
96+
97+
def _datastream_schema(camera: dict) -> dict:
98+
preset_id = camera["presetId"]
99+
return {
100+
"uid": _datastream_uid(camera),
101+
"outputName": DS_OUTPUT_NAME,
102+
"name": "Digitraffic Weather Camera Image",
103+
"description": (
104+
f"Image-reference observations for Digitraffic camera preset {preset_id} "
105+
f"at {camera['cameraStationName']}, attached to road-weather station "
106+
f"{camera['roadWeatherStationId']} ({camera['roadWeatherStationName']})."
107+
),
108+
"documentation": [
109+
{"title": "Weathercam Station Latest Data", "href": _station_data_url(camera["cameraStationId"]), "rel": "service"},
110+
{"title": "Latest JPEG", "href": _image_url(preset_id), "rel": "alternate"},
111+
{"title": "Latest Thumbnail", "href": _thumb_url(preset_id), "rel": "preview"},
112+
{"title": "Digitraffic Terms of Use", "href": DIGITRAFFIC_LICENSE, "rel": "license"},
113+
],
114+
"schema": {
115+
"obsFormat": "application/om+json",
116+
"resultSchema": {
117+
"type": "DataRecord",
118+
"label": "Digitraffic Weather Camera Image Reference",
119+
"fields": [
120+
{"type": "Text", "name": "stationId", "label": "Road Weather Station ID", "definition": "http://sensorml.com/ont/swe/property/StationID"},
121+
{"type": "Text", "name": "camId", "label": "Weather Camera Preset ID", "definition": "http://sensorml.com/ont/swe/property/SensorID"},
122+
{"type": "Text", "name": "imageUrl", "label": "Full-Size Image URL", "definition": "http://www.opengis.net/def/property/OGC/0/ImageURL"},
123+
{"type": "Text", "name": "thumbUrl", "label": "Thumbnail Image URL", "definition": "http://www.opengis.net/def/property/OGC/0/ImageURL"},
124+
{"type": "Text", "name": "latestImageUrl", "label": "Latest Image URL", "definition": "http://www.opengis.net/def/property/OGC/0/ImageURL"},
125+
{"type": "Text", "name": "mediaType", "label": "Media Type", "definition": "http://purl.org/dc/elements/1.1/format"},
126+
{"type": "Text", "name": "sourceUrl", "label": "Source URL", "definition": "http://sensorml.com/ont/swe/property/ReferenceURL"},
127+
],
128+
},
129+
},
130+
}
131+
132+
133+
def _deploy_root() -> dict:
134+
return {"type": "Feature", "geometry": {"type": "Point", "coordinates": [25.0, 63.5]}, "properties": {"uid": DEPLOY_ROOT_UID, "featureType": "sosa:Deployment", "name": "Digitraffic Weather Camera Demo", "description": "Top-level grouping for curated Finnish road-weather camera image-reference resources.", "validTime": [VALID_TIME_START, ".."]}}
135+
136+
137+
def _deploy_group() -> dict:
138+
return {"type": "Feature", "geometry": {"type": "Point", "coordinates": [25.0, 63.5]}, "properties": {"uid": DEPLOY_GROUP_UID, "featureType": "sosa:Deployment", "name": "Digitraffic Weather Camera Presets", "description": "Grouping deployment for curated Fintraffic Digitraffic weather camera presets.", "validTime": [VALID_TIME_START, ".."]}}
139+
140+
141+
def _deploy_camera(camera: dict, system_server_id: str, base_url: str) -> dict:
142+
return {
143+
"type": "Feature",
144+
"geometry": {"type": "Point", "coordinates": [camera["lon"], camera["lat"]]},
145+
"properties": {
146+
"uid": _deploy_uid(camera),
147+
"featureType": "sosa:Deployment",
148+
"name": f"Digitraffic Weather Camera {camera['cameraStationName']} {camera['presetId']}",
149+
"description": (
150+
f"Weather camera preset {camera['presetId']} attached to Finnish road-weather "
151+
f"station {camera['roadWeatherStationId']} ({camera['roadWeatherStationName']})."
152+
),
153+
"validTime": [VALID_TIME_START, ".."],
154+
"platform@link": {"href": f"{base_url.rstrip('/')}/systems/{system_server_id}", "uid": _system_uid(camera["roadWeatherStationId"]), "title": f"Digitraffic Road Weather {camera['roadWeatherStationName']}"},
155+
},
156+
}
157+
158+
159+
def clean_all(base_url: str, auth: str, *, dry_run: bool, stats: dict):
160+
for camera in _load_cameras():
161+
clean_resource(base_url, auth, "deployments", _deploy_uid(camera), dry_run=dry_run, stats=stats, cascade=True)
162+
clean_resource(base_url, auth, "deployments", DEPLOY_GROUP_UID, dry_run=dry_run, stats=stats, cascade=True)
163+
clean_resource(base_url, auth, "deployments", DEPLOY_ROOT_UID, dry_run=dry_run, stats=stats, cascade=True)
164+
clean_resource(base_url, auth, "procedures", PROC_UID, dry_run=dry_run, stats=stats)
165+
166+
167+
def bootstrap(*, clean: bool = False, clean_only: bool = False, dry_run: bool = False, force_sml: bool = False):
168+
config = get_config()
169+
base_url = config["base_url"]
170+
auth = _auth_header(config["user"], config["password"])
171+
cameras = _load_cameras()
172+
stats: dict[str, int] = {}
173+
174+
print("\n" + "=" * 70)
175+
print(" Digitraffic Weathercam -- Bootstrap")
176+
print("=" * 70)
177+
print(f" Server: {base_url}")
178+
print(f" Cameras: {len(cameras)}")
179+
print(f" Clean: {clean} Clean-only: {clean_only} Dry-run: {dry_run} Force-SML: {force_sml}\n")
180+
181+
if clean or clean_only:
182+
print(" -- Cleaning existing resources --")
183+
clean_all(base_url, auth, dry_run=dry_run, stats=stats)
184+
if clean_only:
185+
print_summary(stats, dry_run)
186+
return
187+
188+
print(" -- Procedure --")
189+
ensure_procedure(base_url, auth, PROC_UID, PROCEDURE_STUB, PROCEDURE_SML, dry_run=dry_run, stats=stats, force_sml=force_sml)
190+
191+
print(" -- Companion Datastreams --")
192+
system_ids: dict[str, str] = {}
193+
for camera in cameras:
194+
station_id = camera["roadWeatherStationId"]
195+
sys_id = find_by_uid(base_url, auth, "systems", _system_uid(station_id), no_cache=True)
196+
if not sys_id and not dry_run:
197+
print(f" [WARN] Missing road-weather system for station {station_id}; skipping camera {camera['presetId']}")
198+
continue
199+
system_ids[station_id] = sys_id or "pending"
200+
ensure_datastream(base_url, auth, sys_id or "pending", DS_OUTPUT_NAME, _datastream_schema(camera), dry_run=dry_run, stats=stats)
201+
202+
print(" -- Deployments --")
203+
root_id = ensure_deployment(base_url, auth, DEPLOY_ROOT_UID, _deploy_root(), dry_run=dry_run, stats=stats)
204+
group_id = ensure_deployment(base_url, auth, DEPLOY_GROUP_UID, _deploy_group(), parent_id=root_id, dry_run=dry_run, stats=stats)
205+
for camera in cameras:
206+
station_id = camera["roadWeatherStationId"]
207+
sys_id = system_ids.get(station_id)
208+
if not sys_id and not dry_run:
209+
continue
210+
ensure_deployment(base_url, auth, _deploy_uid(camera), _deploy_camera(camera, sys_id or "pending", base_url), parent_id=group_id, dry_run=dry_run, stats=stats)
211+
212+
print_summary(stats, dry_run)
213+
214+
215+
def main():
216+
parser = argparse.ArgumentParser(description="Bootstrap Digitraffic Weathercam resources on the CSAPI server.")
217+
add_bootstrap_args(parser)
218+
args = parser.parse_args()
219+
bootstrap(clean=args.clean, clean_only=args.clean_only, dry_run=args.dry_run, force_sml=args.force_sml)
220+
221+
222+
if __name__ == "__main__":
223+
main()
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"cameras": [
3+
{
4+
"roadWeatherStationId": "1014",
5+
"roadWeatherStationName": "vt25_Hanko",
6+
"cameraStationId": "C01507",
7+
"cameraStationName": "vt25_Hanko",
8+
"presetId": "C0150701",
9+
"region": "Uusimaa south coast",
10+
"lat": 59.856454,
11+
"lon": 23.021452,
12+
"distanceKm": 0.03,
13+
"selectionReason": "Exact road-weather camera co-located with the Hanko road-weather station."
14+
},
15+
{
16+
"roadWeatherStationId": "1003",
17+
"roadWeatherStationName": "st110_Vihti_Myllylampi",
18+
"cameraStationId": "C01506",
19+
"cameraStationName": "vt25_Myllylampi",
20+
"presetId": "C0150601",
21+
"region": "Helsinki western approach",
22+
"lat": 60.311546,
23+
"lon": 24.241007,
24+
"distanceKm": 0.04,
25+
"selectionReason": "Nearest camera station is effectively co-located with the Vihti Myllylampi road-weather station."
26+
},
27+
{
28+
"roadWeatherStationId": "2002",
29+
"roadWeatherStationName": "vt8_Pyharanta_Ihode",
30+
"cameraStationId": "C02551",
31+
"cameraStationName": "vt8_Laitila_Palttila",
32+
"presetId": "C0255101",
33+
"region": "Southwest Finland",
34+
"lat": 60.890499,
35+
"lon": 21.660864,
36+
"distanceKm": 10.88,
37+
"selectionReason": "Nearest fresh VT8 camera preset to the Pyharanta Ihode road-weather station."
38+
},
39+
{
40+
"roadWeatherStationId": "3036",
41+
"roadWeatherStationName": "vt6_Lappeenranta_Karki",
42+
"cameraStationId": "C03572",
43+
"cameraStationName": "vt6_Lappeenranta_Lavola",
44+
"presetId": "C0357201",
45+
"region": "Southeast Finland",
46+
"lat": 61.034965,
47+
"lon": 28.115215,
48+
"distanceKm": 1.02,
49+
"selectionReason": "Nearest VT6 weather camera to the Lappeenranta Karki station."
50+
},
51+
{
52+
"roadWeatherStationId": "4010",
53+
"roadWeatherStationName": "vt12_Hollola_Hameenkoski",
54+
"cameraStationId": "C04541",
55+
"cameraStationName": "kt54_Karkola_Jarvela",
56+
"presetId": "C0454101",
57+
"region": "Central southern Finland",
58+
"lat": 60.881068,
59+
"lon": 25.267874,
60+
"distanceKm": 13.82,
61+
"selectionReason": "Nearest fresh road camera to the Hollola Hameenkoski road-weather station."
62+
},
63+
{
64+
"roadWeatherStationId": "12091",
65+
"roadWeatherStationName": "kt92_Inari_Naatamo",
66+
"cameraStationId": "C14516",
67+
"cameraStationName": "kt92_Inari_Naatamo",
68+
"presetId": "C1451601",
69+
"region": "Lapland / Inari",
70+
"lat": 69.66367,
71+
"lon": 29.100175,
72+
"distanceKm": 0.0,
73+
"selectionReason": "Exact KT92 weather camera co-located with the Inari Naatamo road-weather station."
74+
}
75+
]
76+
}

0 commit comments

Comments
 (0)