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,409 @@
1
+ """Runtime helpers for configuring locally collected telemetry snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from configparser import ConfigParser
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any, Callable, Mapping, MutableMapping, Protocol
10
+
11
+ from reticulum_telemetry_hub.config.manager import HubConfigurationManager
12
+ from reticulum_telemetry_hub.lxmf_telemetry.model.persistance.sensors.sensor import (
13
+ Sensor,
14
+ )
15
+ from reticulum_telemetry_hub.lxmf_telemetry.model.persistance.sensors.sensor_mapping import (
16
+ sid_mapping,
17
+ )
18
+ from reticulum_telemetry_hub.lxmf_telemetry.model.persistance.sensors.sensor_enum import (
19
+ SID_ACCELERATION,
20
+ SID_AMBIENT_LIGHT,
21
+ SID_ANGULAR_VELOCITY,
22
+ SID_BATTERY,
23
+ SID_CONNECTION_MAP,
24
+ SID_CUSTOM,
25
+ SID_FUEL,
26
+ SID_GRAVITY,
27
+ SID_HUMIDITY,
28
+ SID_INFORMATION,
29
+ SID_LOCATION,
30
+ SID_LXMF_PROPAGATION,
31
+ SID_MAGNETIC_FIELD,
32
+ SID_NVM,
33
+ SID_PHYSICAL_LINK,
34
+ SID_POWER_CONSUMPTION,
35
+ SID_POWER_PRODUCTION,
36
+ SID_PRESSURE,
37
+ SID_PROCESSOR,
38
+ SID_PROXIMITY,
39
+ SID_RAM,
40
+ SID_RECEIVED,
41
+ SID_RNS_TRANSPORT,
42
+ SID_TANK,
43
+ SID_TEMPERATURE,
44
+ SID_TIME,
45
+ )
46
+ from reticulum_telemetry_hub.lxmf_telemetry.model.persistance.telemeter import Telemeter
47
+
48
+
49
+ def _utcnow() -> datetime:
50
+ return datetime.now(timezone.utc).replace(tzinfo=None)
51
+
52
+
53
+ SnapshotMutator = Callable[[Telemeter, dict[int, Any]], None]
54
+
55
+
56
+ class TelemetryPlugin(Protocol):
57
+ """Protocol describing telemetry plugins that can customize snapshots."""
58
+
59
+ def setup(self, manager: "TelemeterManager") -> None: ...
60
+
61
+
62
+ @dataclass
63
+ class StaticInformationConfig:
64
+ """Configuration describing synthesized Information sensor contents."""
65
+
66
+ enabled: bool = True
67
+ contents: str = ""
68
+
69
+
70
+ @dataclass
71
+ class StaticLocationConfig:
72
+ """Configuration describing synthesized Location sensor coordinates."""
73
+
74
+ enabled: bool = True
75
+ latitude: float | None = None
76
+ longitude: float | None = None
77
+ altitude: float | None = None
78
+ speed: float | None = None
79
+ bearing: float | None = None
80
+ accuracy: float | None = None
81
+
82
+
83
+ SENSOR_NAME_TO_SID = {
84
+ "time": SID_TIME,
85
+ "location": SID_LOCATION,
86
+ "pressure": SID_PRESSURE,
87
+ "battery": SID_BATTERY,
88
+ "physical_link": SID_PHYSICAL_LINK,
89
+ "acceleration": SID_ACCELERATION,
90
+ "temperature": SID_TEMPERATURE,
91
+ "humidity": SID_HUMIDITY,
92
+ "magnetic_field": SID_MAGNETIC_FIELD,
93
+ "ambient_light": SID_AMBIENT_LIGHT,
94
+ "gravity": SID_GRAVITY,
95
+ "angular_velocity": SID_ANGULAR_VELOCITY,
96
+ "proximity": SID_PROXIMITY,
97
+ "information": SID_INFORMATION,
98
+ "received": SID_RECEIVED,
99
+ "power_consumption": SID_POWER_CONSUMPTION,
100
+ "power_production": SID_POWER_PRODUCTION,
101
+ "processor": SID_PROCESSOR,
102
+ "ram": SID_RAM,
103
+ "nvm": SID_NVM,
104
+ "tank": SID_TANK,
105
+ "fuel": SID_FUEL,
106
+ "lxmf_propagation": SID_LXMF_PROPAGATION,
107
+ "rns_transport": SID_RNS_TRANSPORT,
108
+ "connection_map": SID_CONNECTION_MAP,
109
+ "custom": SID_CUSTOM,
110
+ }
111
+
112
+ DEFAULT_SENSOR_ORDER = (
113
+ "time",
114
+ "location",
115
+ "information",
116
+ "battery",
117
+ "pressure",
118
+ "temperature",
119
+ "humidity",
120
+ "magnetic_field",
121
+ "ambient_light",
122
+ "gravity",
123
+ "angular_velocity",
124
+ "acceleration",
125
+ "proximity",
126
+ "physical_link",
127
+ "received",
128
+ "power_consumption",
129
+ "power_production",
130
+ "processor",
131
+ "ram",
132
+ "nvm",
133
+ "tank",
134
+ "fuel",
135
+ "lxmf_propagation",
136
+ "rns_transport",
137
+ "connection_map",
138
+ "custom",
139
+ )
140
+
141
+
142
+ @dataclass
143
+ class TelemetryRuntimeConfig:
144
+ """Runtime configuration controlling synthesized sensors and toggles."""
145
+
146
+ enabled_sensors: MutableMapping[int, bool] = field(default_factory=dict)
147
+ static_information: StaticInformationConfig | None = None
148
+ static_location: StaticLocationConfig | None = None
149
+
150
+ @classmethod
151
+ def from_manager(
152
+ cls,
153
+ manager: HubConfigurationManager | None,
154
+ *,
155
+ filename: str = "telemetry.ini",
156
+ ) -> "TelemetryRuntimeConfig":
157
+ if manager is None:
158
+ return cls()
159
+
160
+ telemetry_filename = manager.runtime_config.telemetry_filename or filename
161
+
162
+ if manager.config_parser.has_section("telemetry"):
163
+ return cls.from_section(manager.config_parser["telemetry"])
164
+
165
+ path = Path(manager.storage_path) / telemetry_filename
166
+ return cls.from_file(path)
167
+
168
+ @classmethod
169
+ def from_file(cls, path: Path | None) -> "TelemetryRuntimeConfig":
170
+ if path is None:
171
+ return cls()
172
+
173
+ parser = ConfigParser()
174
+ if path.exists():
175
+ parser.read(path)
176
+
177
+ if parser.has_section("telemetry"):
178
+ section: Mapping[str, str] = parser["telemetry"]
179
+ else:
180
+ section = {}
181
+
182
+ return cls.from_section(section)
183
+
184
+ @classmethod
185
+ def from_section(cls, section: Mapping[str, str]) -> "TelemetryRuntimeConfig":
186
+ if section is None:
187
+ section = {}
188
+
189
+ enabled: MutableMapping[int, bool] = {}
190
+ for name, sid in SENSOR_NAME_TO_SID.items():
191
+ flag_key = f"enable_{name}"
192
+ if flag_key in section:
193
+ enabled[sid] = _get_bool(section, flag_key, True)
194
+
195
+ info_cfg = None
196
+ info_text = section.get("static_information", "").strip()
197
+ if info_text:
198
+ info_enabled = _get_bool(section, "enable_information", True)
199
+ info_cfg = StaticInformationConfig(enabled=info_enabled, contents=info_text)
200
+
201
+ location_cfg = None
202
+ if _get_bool(section, "synthesize_location", False):
203
+ latitude = _get_float(section, "location_latitude")
204
+ longitude = _get_float(section, "location_longitude")
205
+ if latitude is not None and longitude is not None:
206
+ location_cfg = StaticLocationConfig(
207
+ enabled=True,
208
+ latitude=latitude,
209
+ longitude=longitude,
210
+ altitude=_get_float(section, "location_altitude"),
211
+ speed=_get_float(section, "location_speed"),
212
+ bearing=_get_float(section, "location_bearing"),
213
+ accuracy=_get_float(section, "location_accuracy"),
214
+ )
215
+
216
+ return cls(
217
+ enabled_sensors=enabled,
218
+ static_information=info_cfg,
219
+ static_location=location_cfg,
220
+ )
221
+
222
+ def is_enabled(self, sid: int) -> bool:
223
+ return self.enabled_sensors.get(sid, True)
224
+
225
+
226
+ def _get_bool(section: Mapping[str, str], key: str, default: bool) -> bool:
227
+ value = section.get(key)
228
+ if value is None:
229
+ return default
230
+ return str(value).strip().lower() in {"1", "true", "yes", "on"}
231
+
232
+
233
+ def _get_float(section: Mapping[str, str], key: str) -> float | None:
234
+ value = section.get(key)
235
+ if value is None:
236
+ return None
237
+ try:
238
+ return float(value)
239
+ except (TypeError, ValueError):
240
+ return None
241
+
242
+
243
+ class TelemeterManager:
244
+ """Own a long-lived ``Telemeter`` configured via hub settings."""
245
+
246
+ def __init__(
247
+ self,
248
+ *,
249
+ config_manager: HubConfigurationManager | None = None,
250
+ config: TelemetryRuntimeConfig | None = None,
251
+ telemeter: Telemeter | None = None,
252
+ ) -> None:
253
+ self._config_manager = config_manager
254
+ self._config = config or TelemetryRuntimeConfig.from_manager(config_manager)
255
+ self._telemeter = telemeter or Telemeter(peer_dest="")
256
+ self._sensors: MutableMapping[int, Sensor] = {}
257
+ self._sensor_order: list[int] = []
258
+ self._enabled: MutableMapping[int, bool] = {}
259
+ self._mutators: list[SnapshotMutator] = []
260
+ self._initialize_default_sensors()
261
+
262
+ @property
263
+ def telemeter(self) -> Telemeter:
264
+ return self._telemeter
265
+
266
+ def get_sensor(self, sid_or_name: int | str) -> Sensor | None:
267
+ sid = self._normalize_sid(sid_or_name)
268
+ return self._sensors.get(sid)
269
+
270
+ def enable_sensor(self, sid_or_name: int | str) -> None:
271
+ sid = self._normalize_sid(sid_or_name)
272
+ if sid not in self._sensors:
273
+ self._add_sensor_instance(sid)
274
+ self._enabled[sid] = True
275
+
276
+ def disable_sensor(self, sid_or_name: int | str) -> None:
277
+ sid = self._normalize_sid(sid_or_name)
278
+ self._enabled[sid] = False
279
+
280
+ def add_sensor(self, sensor: Sensor, *, enabled: bool = True) -> Sensor:
281
+ sid = sensor.sid
282
+ self._sensors[sid] = sensor
283
+ if sid not in self._sensor_order:
284
+ self._sensor_order.append(sid)
285
+ if sensor not in self._telemeter.sensors:
286
+ self._telemeter.sensors.append(sensor)
287
+ self._enabled[sid] = enabled
288
+ return sensor
289
+
290
+ def add_snapshot_mutator(self, mutator: SnapshotMutator) -> None:
291
+ self._mutators.append(mutator)
292
+
293
+ def register_plugin(
294
+ self, plugin: TelemetryPlugin | Callable[["TelemeterManager"], None]
295
+ ) -> None:
296
+ if hasattr(plugin, "setup"):
297
+ plugin.setup(self) # type: ignore[attr-defined]
298
+ elif callable(plugin):
299
+ plugin(self)
300
+ else: # pragma: no cover - defensive guard
301
+ raise TypeError(
302
+ "Telemetry plugins must be callables or expose a setup() method"
303
+ )
304
+
305
+ def snapshot(self) -> dict[int, Any]:
306
+ """Pack enabled sensors and return a telemetry snapshot."""
307
+
308
+ payload: dict[int, Any] = {}
309
+ now = _utcnow()
310
+ self._synthesize_information()
311
+ self._synthesize_location(now)
312
+
313
+ for sid in self._sensor_order:
314
+ if not self._enabled.get(sid, True):
315
+ continue
316
+ sensor = self._sensors.get(sid)
317
+ if sensor is None:
318
+ continue
319
+ if sid == SID_TIME and hasattr(sensor, "utc"):
320
+ sensor.utc = now
321
+ packed = sensor.pack()
322
+ if packed is None:
323
+ continue
324
+ payload[sid] = packed
325
+
326
+ for mutator in self._mutators:
327
+ mutator(self._telemeter, payload)
328
+
329
+ return payload
330
+
331
+ # ------------------------------------------------------------------ #
332
+ # private helpers
333
+ # ------------------------------------------------------------------ #
334
+ def _initialize_default_sensors(self) -> None:
335
+ for name in DEFAULT_SENSOR_ORDER:
336
+ sid = SENSOR_NAME_TO_SID.get(name)
337
+ if sid is None:
338
+ continue
339
+ self._add_sensor_instance(sid)
340
+
341
+ def _add_sensor_instance(self, sid: int) -> None:
342
+ sensor_cls = sid_mapping.get(sid)
343
+ if sensor_cls is None:
344
+ return
345
+ sensor = sensor_cls()
346
+ self._sensors[sid] = sensor
347
+ self._sensor_order.append(sid)
348
+ self._telemeter.sensors.append(sensor)
349
+ self._enabled[sid] = self._config.is_enabled(sid)
350
+
351
+ def _normalize_sid(self, sid_or_name: int | str) -> int:
352
+ if isinstance(sid_or_name, int):
353
+ return sid_or_name
354
+ if isinstance(sid_or_name, str):
355
+ key = sid_or_name.strip().lower()
356
+ if key in SENSOR_NAME_TO_SID:
357
+ return SENSOR_NAME_TO_SID[key]
358
+ raise KeyError(f"Unknown telemetry sensor '{sid_or_name}'")
359
+ raise TypeError("Sensor identifiers must be int or str")
360
+
361
+ def _synthesize_information(self) -> None:
362
+ cfg = self._config.static_information
363
+ if cfg is None or not cfg.enabled or not cfg.contents:
364
+ return
365
+ self.enable_sensor(SID_INFORMATION)
366
+ sensor = self._sensors.get(SID_INFORMATION)
367
+ if sensor is None:
368
+ return
369
+ setattr(sensor, "contents", cfg.contents)
370
+ setattr(sensor, "synthesized", True)
371
+
372
+ def _synthesize_location(self, timestamp: datetime) -> None:
373
+ cfg = self._config.static_location
374
+ if cfg is None or not cfg.enabled:
375
+ return
376
+ if cfg.latitude is None or cfg.longitude is None:
377
+ return
378
+ self.enable_sensor(SID_LOCATION)
379
+ sensor = self._sensors.get(SID_LOCATION)
380
+ if sensor is None:
381
+ return
382
+ defaults: Mapping[str, float] = {
383
+ "altitude": 0.0,
384
+ "speed": 0.0,
385
+ "bearing": 0.0,
386
+ "accuracy": 0.0,
387
+ }
388
+ setattr(sensor, "latitude", cfg.latitude)
389
+ setattr(sensor, "longitude", cfg.longitude)
390
+ setattr(
391
+ sensor,
392
+ "altitude",
393
+ cfg.altitude if cfg.altitude is not None else defaults["altitude"],
394
+ )
395
+ setattr(
396
+ sensor, "speed", cfg.speed if cfg.speed is not None else defaults["speed"]
397
+ )
398
+ setattr(
399
+ sensor,
400
+ "bearing",
401
+ cfg.bearing if cfg.bearing is not None else defaults["bearing"],
402
+ )
403
+ setattr(
404
+ sensor,
405
+ "accuracy",
406
+ cfg.accuracy if cfg.accuracy is not None else defaults["accuracy"],
407
+ )
408
+ setattr(sensor, "last_update", timestamp)
409
+ setattr(sensor, "synthesized", True)