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,6 @@
1
+ from reticulum_telemetry_hub.lxmf_telemetry.model.persistance.telemeter import Telemeter
2
+
3
+
4
+ class FieldTelemetryStream:
5
+
6
+ telemeters: list[Telemeter]
@@ -0,0 +1,3 @@
1
+ from sqlalchemy.orm import declarative_base
2
+
3
+ Base = declarative_base()
@@ -1,19 +1,19 @@
1
- from sqlalchemy import Column, Integer, String, ForeignKey, BLOB
2
- from sqlalchemy.orm import relationship
3
- from . import Base
4
-
5
-
6
- class Appearance(Base):
7
-
8
- __tablename__ = "Appearance"
9
-
10
- id = Column(Integer, primary_key=True, autoincrement=True)
11
- icon = Column(String, nullable=False)
12
- foreground = Column(String, nullable=False)
13
- background = Column(String, nullable=False)
14
- peer_id = Column(String, ForeignKey("Peer.destination_hash"))
15
- peer = relationship("Peer", back_populates="appearance")
16
-
17
- def __init__(self, peer, icon = "Default"):
18
- self.peer = peer
19
- self.icon = icon
1
+ from sqlalchemy import Column, Integer, String, ForeignKey, BLOB
2
+ from sqlalchemy.orm import relationship
3
+ from . import Base
4
+
5
+
6
+ class Appearance(Base):
7
+
8
+ __tablename__ = "Appearance"
9
+
10
+ id = Column(Integer, primary_key=True, autoincrement=True)
11
+ icon = Column(String, nullable=False)
12
+ foreground = Column(String, nullable=False)
13
+ background = Column(String, nullable=False)
14
+ peer_id = Column(String, ForeignKey("Peer.destination_hash"))
15
+ peer = relationship("Peer", back_populates="appearance")
16
+
17
+ def __init__(self, peer, icon="Default"):
18
+ self.peer = peer
19
+ self.icon = icon
@@ -1,13 +1,17 @@
1
- from . import Base
2
- from typing import TYPE_CHECKING, Optional
3
- from sqlalchemy import Column, Integer, String
4
- from sqlalchemy.orm import Mapped, mapped_column, relationship
5
- if TYPE_CHECKING:
6
- from .telemeter import Telemeter
7
-
8
- class Peer(Base):
9
- __tablename__ = 'Peer'
10
-
11
- destination_hash: Mapped[str] = mapped_column(String, nullable=False, primary_key=True)
12
- telemeters = relationship("Telemeter")
13
- #appearance = relationship("Appearance", back_populates='peer')
1
+ from . import Base
2
+ from typing import TYPE_CHECKING, Optional
3
+ from sqlalchemy import Column, Integer, String
4
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
+
6
+ if TYPE_CHECKING:
7
+ from .telemeter import Telemeter
8
+
9
+
10
+ class Peer(Base):
11
+ __tablename__ = "Peer"
12
+
13
+ destination_hash: Mapped[str] = mapped_column(
14
+ String, nullable=False, primary_key=True
15
+ )
16
+ telemeters = relationship("Telemeter")
17
+ # appearance = relationship("Appearance", back_populates='peer')
@@ -0,0 +1,65 @@
1
+ """SQLAlchemy models for LXMF telemetry sensors."""
2
+
3
+ from .acceleration import Acceleration
4
+ from .ambient_light import AmbientLight
5
+ from .angular_velocity import AngularVelocity
6
+ from .battery import Battery
7
+ from .connection_map import ConnectionMap
8
+ from .generic import (
9
+ Custom,
10
+ Fuel,
11
+ NonVolatileMemory,
12
+ PowerConsumption,
13
+ PowerProduction,
14
+ Processor,
15
+ RandomAccessMemory,
16
+ Tank,
17
+ )
18
+ from .gravity import Gravity
19
+ from .humidity import Humidity
20
+ from .information import Information
21
+ from .location import Location
22
+ from .magnetic_field import MagneticField
23
+ from .lxmf_propagation import LXMFPropagation, LXMFPropagationPeer
24
+ from .physical_link import PhysicalLink
25
+ from .pressure import Pressure
26
+ from .proximity import Proximity
27
+ from .received import Received
28
+ from .rns_transport import RNSTransport
29
+ from .sensor import Sensor
30
+ from .sensor_enum import *
31
+ from .sensor_mapping import sid_mapping
32
+ from .temperature import Temperature
33
+ from .time import Time
34
+
35
+ __all__ = [
36
+ "Acceleration",
37
+ "AmbientLight",
38
+ "AngularVelocity",
39
+ "Battery",
40
+ "ConnectionMap",
41
+ "Custom",
42
+ "Fuel",
43
+ "Gravity",
44
+ "Humidity",
45
+ "Information",
46
+ "Location",
47
+ "LXMFPropagation",
48
+ "LXMFPropagationPeer",
49
+ "MagneticField",
50
+ "NonVolatileMemory",
51
+ "PhysicalLink",
52
+ "PowerConsumption",
53
+ "PowerProduction",
54
+ "Processor",
55
+ "Proximity",
56
+ "Pressure",
57
+ "RandomAccessMemory",
58
+ "Received",
59
+ "RNSTransport",
60
+ "Sensor",
61
+ "Temperature",
62
+ "Time",
63
+ "Tank",
64
+ "sid_mapping",
65
+ ]
@@ -0,0 +1,68 @@
1
+ """SQLAlchemy model for the Acceleration sensor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from sqlalchemy import Float, ForeignKey
8
+ from sqlalchemy.orm import Mapped, mapped_column
9
+
10
+ from .sensor import Sensor
11
+ from .sensor_enum import SID_ACCELERATION
12
+
13
+
14
+ class Acceleration(Sensor):
15
+ __tablename__ = "Acceleration"
16
+
17
+ id: Mapped[int] = mapped_column(ForeignKey("Sensor.id"), primary_key=True)
18
+ x: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
19
+ y: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
20
+ z: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
21
+
22
+ def __init__(
23
+ self,
24
+ stale_time: float | None = 1,
25
+ data: Any | None = None,
26
+ active: bool = False,
27
+ synthesized: bool = False,
28
+ last_update: float = 0,
29
+ last_read: float = 0,
30
+ ) -> None:
31
+ super().__init__(
32
+ stale_time=stale_time,
33
+ data=data,
34
+ active=active,
35
+ synthesized=synthesized,
36
+ last_update=last_update,
37
+ last_read=last_read,
38
+ )
39
+ self.sid = SID_ACCELERATION
40
+
41
+ def pack(self): # type: ignore[override]
42
+ if self.x is None and self.y is None and self.z is None:
43
+ return None
44
+ return [self.x, self.y, self.z]
45
+
46
+ def unpack(self, packed: Any): # type: ignore[override]
47
+ if packed is None:
48
+ self.x = None
49
+ self.y = None
50
+ self.z = None
51
+ return None
52
+
53
+ try:
54
+ self.x = packed[0]
55
+ self.y = packed[1]
56
+ self.z = packed[2]
57
+ except (IndexError, TypeError):
58
+ self.x = None
59
+ self.y = None
60
+ self.z = None
61
+ return None
62
+
63
+ return {"x": self.x, "y": self.y, "z": self.z}
64
+
65
+ __mapper_args__ = {
66
+ "polymorphic_identity": SID_ACCELERATION,
67
+ "with_polymorphic": "*",
68
+ }
@@ -0,0 +1,37 @@
1
+ """SQLAlchemy model for the Ambient Light sensor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from sqlalchemy import Float, ForeignKey
8
+ from sqlalchemy.orm import Mapped, mapped_column
9
+
10
+ from .sensor import Sensor
11
+ from .sensor_enum import SID_AMBIENT_LIGHT
12
+
13
+
14
+ class AmbientLight(Sensor):
15
+ __tablename__ = "AmbientLight"
16
+
17
+ id: Mapped[int] = mapped_column(ForeignKey("Sensor.id"), primary_key=True)
18
+ lux: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
19
+
20
+ def __init__(self) -> None:
21
+ super().__init__(stale_time=1)
22
+ self.sid = SID_AMBIENT_LIGHT
23
+
24
+ def pack(self): # type: ignore[override]
25
+ return self.lux
26
+
27
+ def unpack(self, packed: Any): # type: ignore[override]
28
+ if packed is None:
29
+ self.lux = None
30
+ return None
31
+ self.lux = packed
32
+ return {"lux": self.lux}
33
+
34
+ __mapper_args__ = {
35
+ "polymorphic_identity": SID_AMBIENT_LIGHT,
36
+ "with_polymorphic": "*",
37
+ }
@@ -0,0 +1,68 @@
1
+ """SQLAlchemy model for the Angular Velocity sensor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from sqlalchemy import Float, ForeignKey
8
+ from sqlalchemy.orm import Mapped, mapped_column
9
+
10
+ from .sensor import Sensor
11
+ from .sensor_enum import SID_ANGULAR_VELOCITY
12
+
13
+
14
+ class AngularVelocity(Sensor):
15
+ __tablename__ = "AngularVelocity"
16
+
17
+ id: Mapped[int] = mapped_column(ForeignKey("Sensor.id"), primary_key=True)
18
+ x: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
19
+ y: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
20
+ z: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
21
+
22
+ def __init__(
23
+ self,
24
+ stale_time: float | None = 1,
25
+ data: Any | None = None,
26
+ active: bool = False,
27
+ synthesized: bool = False,
28
+ last_update: float = 0,
29
+ last_read: float = 0,
30
+ ) -> None:
31
+ super().__init__(
32
+ stale_time=stale_time,
33
+ data=data,
34
+ active=active,
35
+ synthesized=synthesized,
36
+ last_update=last_update,
37
+ last_read=last_read,
38
+ )
39
+ self.sid = SID_ANGULAR_VELOCITY
40
+
41
+ def pack(self): # type: ignore[override]
42
+ if self.x is None and self.y is None and self.z is None:
43
+ return None
44
+ return [self.x, self.y, self.z]
45
+
46
+ def unpack(self, packed: Any): # type: ignore[override]
47
+ if packed is None:
48
+ self.x = None
49
+ self.y = None
50
+ self.z = None
51
+ return None
52
+
53
+ try:
54
+ self.x = packed[0]
55
+ self.y = packed[1]
56
+ self.z = packed[2]
57
+ except (IndexError, TypeError):
58
+ self.x = None
59
+ self.y = None
60
+ self.z = None
61
+ return None
62
+
63
+ return {"x": self.x, "y": self.y, "z": self.z}
64
+
65
+ __mapper_args__ = {
66
+ "polymorphic_identity": SID_ANGULAR_VELOCITY,
67
+ "with_polymorphic": "*",
68
+ }
@@ -0,0 +1,68 @@
1
+ """SQLAlchemy model for the Battery sensor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from sqlalchemy import Boolean, Float, ForeignKey
8
+ from sqlalchemy.orm import Mapped, mapped_column
9
+
10
+ from .sensor import Sensor
11
+ from .sensor_enum import SID_BATTERY
12
+
13
+
14
+ class Battery(Sensor):
15
+ __tablename__ = "Battery"
16
+
17
+ id: Mapped[int] = mapped_column(ForeignKey("Sensor.id"), primary_key=True)
18
+ charge_percent: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
19
+ charging: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True)
20
+ temperature: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
21
+
22
+ def __init__(self) -> None:
23
+ super().__init__(stale_time=10)
24
+ self.sid = SID_BATTERY
25
+
26
+ def pack(self): # type: ignore[override]
27
+ if (
28
+ self.charge_percent is None
29
+ and self.charging is None
30
+ and self.temperature is None
31
+ ):
32
+ return None
33
+
34
+ charge = None if self.charge_percent is None else round(self.charge_percent, 1)
35
+ return [charge, self.charging, self.temperature]
36
+
37
+ def unpack(self, packed: Any): # type: ignore[override]
38
+ if packed is None:
39
+ self.charge_percent = None
40
+ self.charging = None
41
+ self.temperature = None
42
+ return None
43
+
44
+ try:
45
+ self.charge_percent = (
46
+ None if packed[0] is None else round(float(packed[0]), 1)
47
+ )
48
+ self.charging = packed[1] if len(packed) > 1 else None
49
+ if len(packed) > 2:
50
+ self.temperature = packed[2]
51
+ else:
52
+ self.temperature = None
53
+ except (IndexError, TypeError, ValueError):
54
+ self.charge_percent = None
55
+ self.charging = None
56
+ self.temperature = None
57
+ return None
58
+
59
+ return {
60
+ "charge_percent": self.charge_percent,
61
+ "charging": self.charging,
62
+ "temperature": self.temperature,
63
+ }
64
+
65
+ __mapper_args__ = {
66
+ "polymorphic_identity": SID_BATTERY,
67
+ "with_polymorphic": "*",
68
+ }
@@ -0,0 +1,258 @@
1
+ """Connection map sensor models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from sqlalchemy import Float, ForeignKey, Integer, JSON, String, UniqueConstraint
8
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
9
+
10
+ from .. import Base
11
+ from .sensor import Sensor
12
+ from .sensor_enum import SID_CONNECTION_MAP
13
+
14
+
15
+ _UNSET = object()
16
+
17
+
18
+ class ConnectionMap(Sensor):
19
+ """Sensor representing a set of maps populated with connection points."""
20
+
21
+ __tablename__ = "ConnectionMap"
22
+
23
+ id: Mapped[int] = mapped_column(ForeignKey("Sensor.id"), primary_key=True)
24
+ maps: Mapped[list["ConnectionMapMap"]] = relationship(
25
+ "ConnectionMapMap",
26
+ back_populates="sensor",
27
+ cascade="all, delete-orphan",
28
+ order_by="ConnectionMapMap.id",
29
+ )
30
+
31
+ SID = SID_CONNECTION_MAP
32
+
33
+ def __init__(self) -> None:
34
+ super().__init__()
35
+ self.sid = SID_CONNECTION_MAP
36
+
37
+ def ensure_map(
38
+ self, map_name: str, label: Optional[str] = None
39
+ ) -> "ConnectionMapMap":
40
+ """Return an existing map entry or create a new one."""
41
+
42
+ for entry in self.maps:
43
+ if entry.map_name == map_name:
44
+ if label is not None:
45
+ entry.label = label
46
+ return entry
47
+
48
+ entry = ConnectionMapMap(map_name=map_name, label=label)
49
+ entry.sensor = self
50
+ return entry
51
+
52
+ def add_point(
53
+ self,
54
+ map_name: str,
55
+ point_hash: str,
56
+ *,
57
+ latitude: float | None | object = _UNSET,
58
+ longitude: float | None | object = _UNSET,
59
+ altitude: float | None | object = _UNSET,
60
+ point_type: str | None | object = _UNSET,
61
+ name: str | None | object = _UNSET,
62
+ signals: dict[str, Any] | None | object = _UNSET,
63
+ **extra_signals: Any,
64
+ ) -> "ConnectionMapPoint":
65
+ """Add or update a connection point within a map."""
66
+
67
+ entry = self.ensure_map(map_name)
68
+ point = entry.get_point(point_hash)
69
+ if point is None:
70
+ point = ConnectionMapPoint(point_hash=point_hash)
71
+ point.map = entry
72
+
73
+ if latitude is not _UNSET:
74
+ point.latitude = latitude # type: ignore[assignment]
75
+ if longitude is not _UNSET:
76
+ point.longitude = longitude # type: ignore[assignment]
77
+ if altitude is not _UNSET:
78
+ point.altitude = altitude # type: ignore[assignment]
79
+ if point_type is not _UNSET:
80
+ point.point_type = point_type # type: ignore[assignment]
81
+ if name is not _UNSET:
82
+ point.name = name # type: ignore[assignment]
83
+
84
+ if signals is not _UNSET or extra_signals:
85
+ if signals is _UNSET:
86
+ merged_signals: dict[str, Any] = dict(point.signals or {})
87
+ elif signals is None:
88
+ merged_signals = {}
89
+ else:
90
+ merged_signals = {k: v for k, v in signals.items() if v is not None}
91
+ for key, value in extra_signals.items():
92
+ if value is not None:
93
+ merged_signals[key] = value
94
+ point.signals = merged_signals or None
95
+
96
+ return point
97
+
98
+ def pack(self) -> Optional[dict[str, Any]]: # type: ignore[override]
99
+ maps_payload: dict[str, dict[str, Any]] = {}
100
+ for entry in self.maps:
101
+ points_payload: dict[str, dict[str, Any]] = {}
102
+ for point in entry.points:
103
+ points_payload[point.point_hash] = point.to_payload()
104
+ maps_payload[entry.map_name] = entry.to_payload(points_payload)
105
+
106
+ if not maps_payload:
107
+ return None
108
+
109
+ return {"maps": maps_payload}
110
+
111
+ def unpack(self, packed: Any) -> Optional[dict[str, Any]]: # type: ignore[override]
112
+ self.maps[:] = []
113
+
114
+ if not isinstance(packed, dict):
115
+ return None
116
+
117
+ maps_payload = packed.get("maps")
118
+ if not isinstance(maps_payload, dict):
119
+ return None
120
+
121
+ normalized: dict[str, dict[str, Any]] = {}
122
+ for map_name, payload in maps_payload.items():
123
+ if not isinstance(map_name, str):
124
+ continue
125
+
126
+ label = None
127
+ points_data: Any = None
128
+ if isinstance(payload, dict):
129
+ label = payload.get("label")
130
+ points_data = payload.get("points")
131
+
132
+ entry = ConnectionMapMap(map_name=map_name, label=label)
133
+ entry.sensor = self
134
+
135
+ if isinstance(points_data, dict):
136
+ for point_hash, point_payload in points_data.items():
137
+ if not isinstance(point_hash, str) or not isinstance(
138
+ point_payload, dict
139
+ ):
140
+ continue
141
+ point = ConnectionMapPoint.from_payload(point_hash, point_payload)
142
+ point.map = entry
143
+ normalized[map_name] = entry.to_payload(entry.pack_points())
144
+
145
+ return {"maps": normalized} if normalized else None
146
+
147
+ __mapper_args__ = {
148
+ "polymorphic_identity": SID_CONNECTION_MAP,
149
+ "with_polymorphic": "*",
150
+ }
151
+
152
+
153
+ class ConnectionMapMap(Base):
154
+ """ORM model representing a named map."""
155
+
156
+ __tablename__ = "ConnectionMapMap"
157
+ __table_args__ = (UniqueConstraint("sensor_id", "map_name"),)
158
+
159
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
160
+ sensor_id: Mapped[int] = mapped_column(
161
+ ForeignKey("ConnectionMap.id", ondelete="CASCADE")
162
+ )
163
+ map_name: Mapped[str] = mapped_column(String, nullable=False)
164
+ label: Mapped[Optional[str]] = mapped_column(String, nullable=True)
165
+
166
+ sensor: Mapped[ConnectionMap] = relationship("ConnectionMap", back_populates="maps")
167
+ points: Mapped[list["ConnectionMapPoint"]] = relationship(
168
+ "ConnectionMapPoint",
169
+ back_populates="map",
170
+ cascade="all, delete-orphan",
171
+ order_by="ConnectionMapPoint.id",
172
+ )
173
+
174
+ def get_point(self, point_hash: str) -> Optional["ConnectionMapPoint"]:
175
+ for point in self.points:
176
+ if point.point_hash == point_hash:
177
+ return point
178
+ return None
179
+
180
+ def to_payload(self, points: dict[str, dict[str, Any]]) -> dict[str, Any]:
181
+ payload: dict[str, Any] = {"points": points}
182
+ if self.label is not None:
183
+ payload["label"] = self.label
184
+ return payload
185
+
186
+ def pack_points(self) -> dict[str, dict[str, Any]]:
187
+ return {point.point_hash: point.to_payload() for point in self.points}
188
+
189
+
190
+ class ConnectionMapPoint(Base):
191
+ """ORM model for individual map points."""
192
+
193
+ __tablename__ = "ConnectionMapPoint"
194
+ __table_args__ = (UniqueConstraint("map_id", "point_hash"),)
195
+
196
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
197
+ map_id: Mapped[int] = mapped_column(
198
+ ForeignKey("ConnectionMapMap.id", ondelete="CASCADE")
199
+ )
200
+ point_hash: Mapped[str] = mapped_column(String, nullable=False)
201
+ latitude: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
202
+ longitude: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
203
+ altitude: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
204
+ point_type: Mapped[Optional[str]] = mapped_column(String, nullable=True)
205
+ name: Mapped[Optional[str]] = mapped_column(String, nullable=True)
206
+ signals: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON, nullable=True)
207
+
208
+ map: Mapped[ConnectionMapMap] = relationship(
209
+ "ConnectionMapMap", back_populates="points"
210
+ )
211
+
212
+ def to_payload(self) -> dict[str, Any]:
213
+ payload: dict[str, Any] = {}
214
+ if self.latitude is not None:
215
+ payload["lat"] = self.latitude
216
+ if self.longitude is not None:
217
+ payload["lon"] = self.longitude
218
+ if self.altitude is not None:
219
+ payload["alt"] = self.altitude
220
+ if self.point_type is not None:
221
+ payload["type"] = self.point_type
222
+ if self.name is not None:
223
+ payload["name"] = self.name
224
+ if self.signals:
225
+ payload.update(self.signals)
226
+ return payload
227
+
228
+ @classmethod
229
+ def from_payload(
230
+ cls, point_hash: str, payload: dict[str, Any]
231
+ ) -> "ConnectionMapPoint":
232
+ signals: dict[str, Any] = {}
233
+ latitude = payload.get("lat")
234
+ longitude = payload.get("lon")
235
+ altitude = payload.get("alt")
236
+ point_type = payload.get("type")
237
+ name = payload.get("name")
238
+
239
+ for key, value in payload.items():
240
+ if key not in {"lat", "lon", "alt", "type", "name"}:
241
+ signals[key] = value
242
+
243
+ return cls(
244
+ point_hash=point_hash,
245
+ latitude=latitude,
246
+ longitude=longitude,
247
+ altitude=altitude,
248
+ point_type=point_type,
249
+ name=name,
250
+ signals=signals or None,
251
+ )
252
+
253
+
254
+ __all__ = [
255
+ "ConnectionMap",
256
+ "ConnectionMapMap",
257
+ "ConnectionMapPoint",
258
+ ]