ReticulumTelemetryHub 0.1.0__py3-none-any.whl → 0.143.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. reticulum_telemetry_hub/api/__init__.py +23 -0
  2. reticulum_telemetry_hub/api/models.py +323 -0
  3. reticulum_telemetry_hub/api/service.py +836 -0
  4. reticulum_telemetry_hub/api/storage.py +528 -0
  5. reticulum_telemetry_hub/api/storage_base.py +156 -0
  6. reticulum_telemetry_hub/api/storage_models.py +118 -0
  7. reticulum_telemetry_hub/atak_cot/__init__.py +49 -0
  8. reticulum_telemetry_hub/atak_cot/base.py +277 -0
  9. reticulum_telemetry_hub/atak_cot/chat.py +506 -0
  10. reticulum_telemetry_hub/atak_cot/detail.py +235 -0
  11. reticulum_telemetry_hub/atak_cot/event.py +181 -0
  12. reticulum_telemetry_hub/atak_cot/pytak_client.py +569 -0
  13. reticulum_telemetry_hub/atak_cot/tak_connector.py +848 -0
  14. reticulum_telemetry_hub/config/__init__.py +25 -0
  15. reticulum_telemetry_hub/config/constants.py +7 -0
  16. reticulum_telemetry_hub/config/manager.py +515 -0
  17. reticulum_telemetry_hub/config/models.py +215 -0
  18. reticulum_telemetry_hub/embedded_lxmd/__init__.py +5 -0
  19. reticulum_telemetry_hub/embedded_lxmd/embedded.py +418 -0
  20. reticulum_telemetry_hub/internal_api/__init__.py +21 -0
  21. reticulum_telemetry_hub/internal_api/bus.py +344 -0
  22. reticulum_telemetry_hub/internal_api/core.py +690 -0
  23. reticulum_telemetry_hub/internal_api/v1/__init__.py +74 -0
  24. reticulum_telemetry_hub/internal_api/v1/enums.py +109 -0
  25. reticulum_telemetry_hub/internal_api/v1/manifest.json +8 -0
  26. reticulum_telemetry_hub/internal_api/v1/schemas.py +478 -0
  27. reticulum_telemetry_hub/internal_api/versioning.py +63 -0
  28. reticulum_telemetry_hub/lxmf_daemon/Handlers.py +122 -0
  29. reticulum_telemetry_hub/lxmf_daemon/LXMF.py +252 -0
  30. reticulum_telemetry_hub/lxmf_daemon/LXMPeer.py +898 -0
  31. reticulum_telemetry_hub/lxmf_daemon/LXMRouter.py +4227 -0
  32. reticulum_telemetry_hub/lxmf_daemon/LXMessage.py +1006 -0
  33. reticulum_telemetry_hub/lxmf_daemon/LXStamper.py +490 -0
  34. reticulum_telemetry_hub/lxmf_daemon/__init__.py +10 -0
  35. reticulum_telemetry_hub/lxmf_daemon/_version.py +1 -0
  36. reticulum_telemetry_hub/lxmf_daemon/lxmd.py +1655 -0
  37. reticulum_telemetry_hub/lxmf_telemetry/model/fields/field_telemetry_stream.py +6 -0
  38. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/__init__.py +3 -0
  39. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/appearance.py +19 -19
  40. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/peer.py +17 -13
  41. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/__init__.py +65 -0
  42. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/acceleration.py +68 -0
  43. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/ambient_light.py +37 -0
  44. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/angular_velocity.py +68 -0
  45. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/battery.py +68 -0
  46. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/connection_map.py +258 -0
  47. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/generic.py +841 -0
  48. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/gravity.py +68 -0
  49. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/humidity.py +37 -0
  50. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/information.py +42 -0
  51. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/location.py +110 -0
  52. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/lxmf_propagation.py +429 -0
  53. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/magnetic_field.py +68 -0
  54. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/physical_link.py +53 -0
  55. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/pressure.py +37 -0
  56. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/proximity.py +37 -0
  57. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/received.py +75 -0
  58. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/rns_transport.py +209 -0
  59. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor.py +65 -0
  60. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_enum.py +27 -0
  61. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +58 -0
  62. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/temperature.py +37 -0
  63. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/sensors/time.py +36 -32
  64. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/telemeter.py +26 -23
  65. reticulum_telemetry_hub/lxmf_telemetry/sampler.py +229 -0
  66. reticulum_telemetry_hub/lxmf_telemetry/telemeter_manager.py +409 -0
  67. reticulum_telemetry_hub/lxmf_telemetry/telemetry_controller.py +804 -0
  68. reticulum_telemetry_hub/northbound/__init__.py +5 -0
  69. reticulum_telemetry_hub/northbound/app.py +195 -0
  70. reticulum_telemetry_hub/northbound/auth.py +119 -0
  71. reticulum_telemetry_hub/northbound/gateway.py +310 -0
  72. reticulum_telemetry_hub/northbound/internal_adapter.py +302 -0
  73. reticulum_telemetry_hub/northbound/models.py +213 -0
  74. reticulum_telemetry_hub/northbound/routes_chat.py +123 -0
  75. reticulum_telemetry_hub/northbound/routes_files.py +119 -0
  76. reticulum_telemetry_hub/northbound/routes_rest.py +345 -0
  77. reticulum_telemetry_hub/northbound/routes_subscribers.py +150 -0
  78. reticulum_telemetry_hub/northbound/routes_topics.py +178 -0
  79. reticulum_telemetry_hub/northbound/routes_ws.py +107 -0
  80. reticulum_telemetry_hub/northbound/serializers.py +72 -0
  81. reticulum_telemetry_hub/northbound/services.py +373 -0
  82. reticulum_telemetry_hub/northbound/websocket.py +855 -0
  83. reticulum_telemetry_hub/reticulum_server/__main__.py +2237 -0
  84. reticulum_telemetry_hub/reticulum_server/command_manager.py +1268 -0
  85. reticulum_telemetry_hub/reticulum_server/command_text.py +399 -0
  86. reticulum_telemetry_hub/reticulum_server/constants.py +1 -0
  87. reticulum_telemetry_hub/reticulum_server/event_log.py +357 -0
  88. reticulum_telemetry_hub/reticulum_server/internal_adapter.py +358 -0
  89. reticulum_telemetry_hub/reticulum_server/outbound_queue.py +312 -0
  90. reticulum_telemetry_hub/reticulum_server/services.py +422 -0
  91. reticulumtelemetryhub-0.143.0.dist-info/METADATA +181 -0
  92. reticulumtelemetryhub-0.143.0.dist-info/RECORD +97 -0
  93. {reticulumtelemetryhub-0.1.0.dist-info → reticulumtelemetryhub-0.143.0.dist-info}/WHEEL +1 -1
  94. reticulumtelemetryhub-0.143.0.dist-info/licenses/LICENSE +277 -0
  95. lxmf_telemetry/model/fields/field_telemetry_stream.py +0 -7
  96. lxmf_telemetry/model/persistance/__init__.py +0 -3
  97. lxmf_telemetry/model/persistance/sensors/location.py +0 -69
  98. lxmf_telemetry/model/persistance/sensors/magnetic_field.py +0 -36
  99. lxmf_telemetry/model/persistance/sensors/sensor.py +0 -44
  100. lxmf_telemetry/model/persistance/sensors/sensor_enum.py +0 -24
  101. lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +0 -9
  102. lxmf_telemetry/telemetry_controller.py +0 -124
  103. reticulum_server/main.py +0 -182
  104. reticulumtelemetryhub-0.1.0.dist-info/METADATA +0 -15
  105. reticulumtelemetryhub-0.1.0.dist-info/RECORD +0 -19
  106. {lxmf_telemetry → reticulum_telemetry_hub}/__init__.py +0 -0
  107. {lxmf_telemetry/model/persistance/sensors → reticulum_telemetry_hub/lxmf_telemetry}/__init__.py +0 -0
  108. {reticulum_server → reticulum_telemetry_hub/reticulum_server}/__init__.py +0 -0
@@ -0,0 +1,373 @@
1
+ """Service helpers for the northbound API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from datetime import timezone
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from typing import Callable
11
+ from typing import Dict
12
+ from typing import List
13
+ from typing import Optional
14
+
15
+ from reticulum_telemetry_hub.api.models import ChatMessage
16
+ from reticulum_telemetry_hub.api.models import Client
17
+ from reticulum_telemetry_hub.api.models import FileAttachment
18
+ from reticulum_telemetry_hub.api.models import IdentityStatus
19
+ from reticulum_telemetry_hub.api.models import ReticulumInfo
20
+ from reticulum_telemetry_hub.api.models import Subscriber
21
+ from reticulum_telemetry_hub.api.models import Topic
22
+ from reticulum_telemetry_hub.api.service import ReticulumTelemetryHubAPI
23
+ from reticulum_telemetry_hub.lxmf_telemetry.telemetry_controller import (
24
+ TelemetryController,
25
+ )
26
+ from reticulum_telemetry_hub.reticulum_server import command_text
27
+ from reticulum_telemetry_hub.reticulum_server.event_log import EventLog
28
+
29
+
30
+ def _load_supported_commands_doc() -> Optional[str]:
31
+ doc_path = Path(__file__).resolve().parents[2] / "docs" / "supportedCommands.md"
32
+ try:
33
+ return doc_path.read_text(encoding="utf-8")
34
+ except OSError:
35
+ return None
36
+
37
+
38
+ def _build_help_fallback(doc_text: str) -> str:
39
+ public_commands: list[str] = []
40
+ protected_commands: list[str] = []
41
+ section: Optional[str] = None
42
+ for line in doc_text.splitlines():
43
+ stripped = line.strip()
44
+ if stripped.lower().startswith("public commands"):
45
+ section = "public"
46
+ continue
47
+ if stripped.lower().startswith("protected commands"):
48
+ section = "protected"
49
+ continue
50
+ if stripped.startswith("| `"):
51
+ parts = [part.strip() for part in stripped.split("|")]
52
+ if len(parts) < 2:
53
+ continue
54
+ command_cell = parts[1].replace("`", "").strip()
55
+ if not command_cell:
56
+ continue
57
+ if section == "protected":
58
+ protected_commands.append(command_cell)
59
+ else:
60
+ public_commands.append(command_cell)
61
+ if not public_commands and not protected_commands:
62
+ return "# Command list\n\nCommand documentation not available."
63
+ lines = ["# Command list", ""]
64
+ if public_commands:
65
+ lines.append("Public:")
66
+ lines.extend([f"- {command}" for command in public_commands])
67
+ lines.append("")
68
+ if protected_commands:
69
+ lines.append("Protected:")
70
+ lines.extend([f"- {command}" for command in protected_commands])
71
+ return "\n".join(lines).strip() + "\n"
72
+
73
+
74
+ def _build_examples_fallback(doc_text: str) -> str:
75
+ marker = "Public commands:"
76
+ start = doc_text.find(marker)
77
+ if start == -1:
78
+ return doc_text
79
+ snippet = doc_text[start:].strip()
80
+ return f"# Command examples\n\n{snippet}\n"
81
+
82
+
83
+ @dataclass
84
+ class NorthboundServices:
85
+ """Aggregate services needed by the northbound API."""
86
+
87
+ api: ReticulumTelemetryHubAPI
88
+ telemetry: TelemetryController
89
+ event_log: EventLog
90
+ started_at: datetime
91
+ command_manager: Optional[Any] = None
92
+ routing_provider: Optional[Callable[[], List[str]]] = None
93
+ message_dispatcher: Optional[
94
+ Callable[[str, Optional[str], Optional[str], Optional[dict]], ChatMessage | None]
95
+ ] = None
96
+
97
+ def help_text(self) -> str:
98
+ """Return the Help command text.
99
+
100
+ Returns:
101
+ str: Markdown formatted help content.
102
+ """
103
+
104
+ if not self.command_manager:
105
+ doc_text = _load_supported_commands_doc()
106
+ if doc_text:
107
+ return _build_help_fallback(doc_text)
108
+ return "# Command list\n\nCommand manager is not configured."
109
+ return command_text.build_help_text(self.command_manager)
110
+
111
+ def examples_text(self) -> str:
112
+ """Return the Examples command text.
113
+
114
+ Returns:
115
+ str: Markdown formatted examples content.
116
+ """
117
+
118
+ if not self.command_manager:
119
+ doc_text = _load_supported_commands_doc()
120
+ if doc_text:
121
+ return _build_examples_fallback(doc_text)
122
+ return "# Command examples\n\nCommand manager is not configured."
123
+ return command_text.build_examples_text(self.command_manager)
124
+
125
+ def status_snapshot(self) -> Dict[str, object]:
126
+ """Return the current status snapshot.
127
+
128
+ Returns:
129
+ Dict[str, object]: Status payload for the dashboard.
130
+ """
131
+
132
+ uptime = datetime.now(timezone.utc) - self.started_at
133
+ chat_stats = self.api.chat_message_stats()
134
+ return {
135
+ "uptime_seconds": int(uptime.total_seconds()),
136
+ "clients": len(self.api.list_clients()),
137
+ "topics": len(self.api.list_topics()),
138
+ "subscribers": len(self.api.list_subscribers()),
139
+ "files": len(self.api.list_files()),
140
+ "images": len(self.api.list_images()),
141
+ "chat": {
142
+ "sent": chat_stats.get("sent", 0),
143
+ "failed": chat_stats.get("failed", 0),
144
+ "received": chat_stats.get("delivered", 0),
145
+ },
146
+ "telemetry": self.telemetry.telemetry_stats(),
147
+ }
148
+
149
+ def list_events(self, limit: Optional[int] = None) -> List[Dict[str, object]]:
150
+ """Return recent events.
151
+
152
+ Args:
153
+ limit (Optional[int]): Optional limit for returned events.
154
+
155
+ Returns:
156
+ List[Dict[str, object]]: Event entries.
157
+ """
158
+
159
+ return self.event_log.list_events(limit=limit)
160
+
161
+ def record_event(
162
+ self, event_type: str, message: str, metadata: Optional[Dict[str, object]] = None
163
+ ) -> Dict[str, object]:
164
+ """Record an event entry.
165
+
166
+ Args:
167
+ event_type (str): Event category.
168
+ message (str): Human readable description.
169
+ metadata (Optional[Dict[str, object]]): Optional structured data.
170
+
171
+ Returns:
172
+ Dict[str, object]: Event entry payload.
173
+ """
174
+
175
+ return self.event_log.add_event(event_type, message, metadata=metadata)
176
+
177
+ def list_clients(self) -> List[Client]:
178
+ """Return connected clients.
179
+
180
+ Returns:
181
+ List[Client]: Client entries.
182
+ """
183
+
184
+ return self.api.list_clients()
185
+
186
+ def list_topics(self) -> List[Topic]:
187
+ """Return topics.
188
+
189
+ Returns:
190
+ List[Topic]: Topic entries.
191
+ """
192
+
193
+ return self.api.list_topics()
194
+
195
+ def list_subscribers(self) -> List[Subscriber]:
196
+ """Return subscribers.
197
+
198
+ Returns:
199
+ List[Subscriber]: Subscriber entries.
200
+ """
201
+
202
+ return self.api.list_subscribers()
203
+
204
+ def list_files(self) -> List[FileAttachment]:
205
+ """Return file attachments.
206
+
207
+ Returns:
208
+ List[FileAttachment]: File records.
209
+ """
210
+
211
+ return self.api.list_files()
212
+
213
+ def list_images(self) -> List[FileAttachment]:
214
+ """Return image attachments.
215
+
216
+ Returns:
217
+ List[FileAttachment]: Image records.
218
+ """
219
+
220
+ return self.api.list_images()
221
+
222
+ def list_identity_statuses(self) -> List[IdentityStatus]:
223
+ """Return identity moderation statuses.
224
+
225
+ Returns:
226
+ List[IdentityStatus]: Identity status records.
227
+ """
228
+
229
+ return self.api.list_identity_statuses()
230
+
231
+ def list_chat_messages(
232
+ self,
233
+ *,
234
+ limit: int = 200,
235
+ direction: Optional[str] = None,
236
+ topic_id: Optional[str] = None,
237
+ destination: Optional[str] = None,
238
+ source: Optional[str] = None,
239
+ ) -> List[ChatMessage]:
240
+ """Return persisted chat messages."""
241
+
242
+ return self.api.list_chat_messages(
243
+ limit=limit,
244
+ direction=direction,
245
+ topic_id=topic_id,
246
+ destination=destination,
247
+ source=source,
248
+ )
249
+
250
+ def store_uploaded_attachment(
251
+ self,
252
+ *,
253
+ content: bytes,
254
+ filename: str,
255
+ media_type: Optional[str],
256
+ category: str,
257
+ topic_id: Optional[str] = None,
258
+ ) -> FileAttachment:
259
+ """Persist an uploaded attachment to storage."""
260
+
261
+ return self.api.store_uploaded_attachment(
262
+ content=content,
263
+ filename=filename,
264
+ media_type=media_type,
265
+ category=category,
266
+ topic_id=topic_id,
267
+ )
268
+
269
+ def resolve_attachments(
270
+ self,
271
+ *,
272
+ file_ids: list[int],
273
+ image_ids: list[int],
274
+ ) -> list[FileAttachment]:
275
+ """Resolve stored attachment records by ID."""
276
+
277
+ attachments: list[FileAttachment] = []
278
+ for file_id in file_ids:
279
+ attachments.append(self.api.retrieve_file(file_id))
280
+ for image_id in image_ids:
281
+ attachments.append(self.api.retrieve_image(image_id))
282
+ return attachments
283
+
284
+ def dump_routing(self) -> Dict[str, List[str]]:
285
+ """Return connected destinations.
286
+
287
+ Returns:
288
+ Dict[str, List[str]]: Routing summary payload.
289
+ """
290
+
291
+ if self.routing_provider:
292
+ return {"destinations": list(self.routing_provider())}
293
+ return {"destinations": [client.identity for client in self.api.list_clients()]}
294
+
295
+ def telemetry_entries(
296
+ self, *, since: int, topic_id: Optional[str] = None
297
+ ) -> List[Dict[str, object]]:
298
+ """Return telemetry entries for REST responses.
299
+
300
+ Args:
301
+ since (int): Unix timestamp in seconds.
302
+ topic_id (Optional[str]): Optional topic filter.
303
+
304
+ Returns:
305
+ List[Dict[str, object]]: Telemetry entries.
306
+ """
307
+
308
+ return self.telemetry.list_telemetry_entries(since=since, topic_id=topic_id)
309
+
310
+ def app_info(self) -> ReticulumInfo:
311
+ """Return application metadata.
312
+
313
+ Returns:
314
+ ReticulumInfo: Application info snapshot.
315
+ """
316
+
317
+ return self.api.get_app_info()
318
+
319
+ def send_message(
320
+ self,
321
+ content: str,
322
+ *,
323
+ topic_id: Optional[str] = None,
324
+ destination: Optional[str] = None,
325
+ ) -> None:
326
+ """Dispatch a message from northbound into the core hub."""
327
+
328
+ if not self.message_dispatcher:
329
+ raise RuntimeError("Message dispatch is not configured")
330
+ self.message_dispatcher(content, topic_id, destination, None)
331
+
332
+ def send_chat_message(
333
+ self,
334
+ *,
335
+ content: str,
336
+ scope: str,
337
+ topic_id: Optional[str],
338
+ destination: Optional[str],
339
+ attachments: list[FileAttachment],
340
+ ) -> ChatMessage:
341
+ """Send a chat message via the core hub."""
342
+
343
+ if not self.message_dispatcher:
344
+ raise RuntimeError("Message dispatch is not configured")
345
+ chat_attachments = [
346
+ self.api.chat_attachment_from_file(item) for item in attachments
347
+ ]
348
+ fields = {"attachments": attachments}
349
+ if scope:
350
+ fields["scope"] = scope
351
+ message = self.message_dispatcher(content, topic_id, destination, fields)
352
+ if message is None:
353
+ message = ChatMessage(
354
+ direction="outbound",
355
+ scope=scope,
356
+ state="failed",
357
+ content=content,
358
+ source=None,
359
+ destination=destination,
360
+ topic_id=topic_id,
361
+ attachments=chat_attachments,
362
+ )
363
+ return self.api.record_chat_message(message)
364
+ return message
365
+
366
+ def reload_config(self) -> ReticulumInfo:
367
+ """Reload configuration from disk.
368
+
369
+ Returns:
370
+ ReticulumInfo: Updated configuration snapshot.
371
+ """
372
+
373
+ return self.api.reload_config()