1+ #!/usr/bin/env python3
2+ """Add refresh metadata to legacy scenario/demo systems.
3+
4+ These systems are not normal scheduled public-data publishers. They are legacy
5+ scenario resources whose observations are produced by simulator/demo runs or by
6+ operator action. This script preserves the current SensorML and adds a
7+ card-readable refresh metadata capability so Explorer does not imply a false
8+ polling cadence.
9+ """
10+
11+ import argparse
12+ import json
13+ import os
14+ import sys
15+ from urllib .error import HTTPError
16+ from urllib .request import Request , urlopen
17+
18+ sys .path .insert (0 , os .path .join (os .path .dirname (__file__ ), ".." ))
19+ from publishers .bootstrap_helpers import get_config , _auth_header
20+
21+
22+ SYSTEMS = [
23+ {
24+ "id" : "040g" ,
25+ "label" : "SET-A" ,
26+ "refresh_rate" : "Scenario-driven / not scheduled" ,
27+ "query_mode" : "Operator-authored SENREP reports are created by scenario workflow, not a fixed poller" ,
28+ },
29+ {
30+ "id" : "0410" ,
31+ "label" : "Monitoring Site 001" ,
32+ "refresh_rate" : "Scenario-driven / not scheduled" ,
33+ "query_mode" : "Monitoring-site support metadata changes only during scenario updates" ,
34+ },
35+ {
36+ "id" : "041g" ,
37+ "label" : "Relay" ,
38+ "refresh_rate" : "Scenario-driven / not scheduled" ,
39+ "query_mode" : "Relay support metadata changes only during scenario updates" ,
40+ },
41+ {
42+ "id" : "0420" ,
43+ "label" : "ODAS Mic Array Node AZ-MA-1" ,
44+ "refresh_rate" : "Scenario-driven / simulator active" ,
45+ "query_mode" : "ODAS observations are emitted by scenario simulator runs when active" ,
46+ },
47+ {
48+ "id" : "0490" ,
49+ "label" : "ODAS Mic Array Node AZ-MA-2" ,
50+ "refresh_rate" : "Scenario-driven / simulator active" ,
51+ "query_mode" : "ODAS observations are emitted by scenario simulator runs when active" ,
52+ },
53+ {
54+ "id" : "049g" ,
55+ "label" : "ODAS Mic Array Node AZ-MA-3" ,
56+ "refresh_rate" : "Scenario-driven / simulator active" ,
57+ "query_mode" : "ODAS observations are emitted by scenario simulator runs when active" ,
58+ },
59+ {
60+ "id" : "04o0" ,
61+ "label" : "AZ String Alpha Localizer" ,
62+ "refresh_rate" : "Scenario-driven / simulator active" ,
63+ "query_mode" : "Localizer estimates are emitted when recent scenario LOB observations are available" ,
64+ },
65+ ]
66+
67+
68+ def _request_json (url : str , auth : str , * , method : str = "GET" , body : dict | None = None ) -> dict :
69+ data = json .dumps (body ).encode () if body is not None else None
70+ req = Request (url , data = data , method = method , headers = {
71+ "Authorization" : auth ,
72+ "Accept" : "application/sml+json" ,
73+ "Content-Type" : "application/sml+json" ,
74+ })
75+ try :
76+ with urlopen (req , timeout = 30 ) as resp :
77+ raw = resp .read ().decode ()
78+ return json .loads (raw ) if raw .strip () else {}
79+ except HTTPError as exc :
80+ raw = exc .read ().decode (errors = "replace" )
81+ raise RuntimeError (f"HTTP { exc .code } { method } { url } : { raw [:400 ]} " ) from exc
82+
83+
84+ def _refresh_capability (entry : dict ) -> dict :
85+ return {
86+ "definition" : "http://www.w3.org/ns/ssn/systems/SystemCapability" ,
87+ "label" : "Refresh Metadata" ,
88+ "capabilities" : [
89+ {
90+ "type" : "Text" ,
91+ "name" : "refresh_rate" ,
92+ "definition" : "http://sensorml.com/ont/swe/property/ReportingFrequency" ,
93+ "label" : "Refresh Rate" ,
94+ "value" : entry ["refresh_rate" ],
95+ },
96+ {
97+ "type" : "Text" ,
98+ "name" : "source_query_mode" ,
99+ "definition" : "http://sensorml.com/ont/swe/property/ReportingFrequency" ,
100+ "label" : "Source Query Mode" ,
101+ "value" : entry ["query_mode" ],
102+ },
103+ ],
104+ }
105+
106+
107+ def _merge_refresh_metadata (sml : dict , entry : dict ) -> dict :
108+ capabilities = [
109+ item for item in sml .get ("capabilities" , [])
110+ if item .get ("label" ) != "Refresh Metadata" and item .get ("name" ) != "refresh_metadata"
111+ ]
112+ capabilities .append (_refresh_capability (entry ))
113+ updated = dict (sml )
114+ updated ["capabilities" ] = capabilities
115+ return updated
116+
117+
118+ def main () -> int :
119+ parser = argparse .ArgumentParser (description = "Enrich legacy demo systems with truthful refresh metadata." )
120+ parser .add_argument ("--dry-run" , action = "store_true" , help = "Fetch and show intended updates without PUT" )
121+ args = parser .parse_args ()
122+
123+ config = get_config ()
124+ base_url = config ["base_url" ].rstrip ("/" )
125+ auth = _auth_header (config ["user" ], config ["password" ])
126+
127+ for entry in SYSTEMS :
128+ url = f"{ base_url } /systems/{ entry ['id' ]} ?f=sml3"
129+ sml = _request_json (url , auth )
130+ label = sml .get ("label" ) or sml .get ("name" ) or entry ["label" ]
131+ updated = _merge_refresh_metadata (sml , entry )
132+ if args .dry_run :
133+ print (f"[DRY] { entry ['id' ]} { label } : Refresh Rate={ entry ['refresh_rate' ]} " )
134+ continue
135+ try :
136+ _request_json (f"{ base_url } /systems/{ entry ['id' ]} " , auth , method = "PUT" , body = updated )
137+ print (f"[SML] { entry ['id' ]} { label } : Refresh Rate={ entry ['refresh_rate' ]} " )
138+ except RuntimeError as exc :
139+ print (f"[WARN] PUT returned warning for { entry ['id' ]} { label } : { exc } " )
140+
141+ return 0
142+
143+
144+ if __name__ == "__main__" :
145+ raise SystemExit (main ())
0 commit comments