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,118 @@
1
+ """SQLAlchemy models for the Reticulum Community Hub API storage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from datetime import timezone
7
+
8
+ from sqlalchemy import Boolean
9
+ from sqlalchemy import Column
10
+ from sqlalchemy import DateTime
11
+ from sqlalchemy import Integer
12
+ from sqlalchemy import JSON
13
+ from sqlalchemy import String
14
+ from sqlalchemy.orm import declarative_base
15
+
16
+
17
+ Base = declarative_base()
18
+
19
+
20
+ def _utcnow() -> datetime:
21
+ """Return the current UTC datetime with timezone information."""
22
+ return datetime.now(timezone.utc)
23
+
24
+
25
+ class TopicRecord(Base): # pylint: disable=too-few-public-methods
26
+ """SQLAlchemy record for topics."""
27
+
28
+ __tablename__ = "topics"
29
+
30
+ id = Column(String, primary_key=True)
31
+ name = Column(String, nullable=False)
32
+ path = Column(String, nullable=False)
33
+ description = Column(String, nullable=True)
34
+ created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
35
+
36
+
37
+ class SubscriberRecord(Base): # pylint: disable=too-few-public-methods
38
+ """SQLAlchemy record for subscribers."""
39
+
40
+ __tablename__ = "subscribers"
41
+
42
+ id = Column(String, primary_key=True)
43
+ destination = Column(String, nullable=False)
44
+ topic_id = Column(String, nullable=True)
45
+ reject_tests = Column(Integer, nullable=True)
46
+ metadata_json = Column("metadata", JSON, nullable=True)
47
+ created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
48
+
49
+
50
+ class ClientRecord(Base): # pylint: disable=too-few-public-methods
51
+ """SQLAlchemy record for clients."""
52
+
53
+ __tablename__ = "clients"
54
+
55
+ identity = Column(String, primary_key=True)
56
+ last_seen = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
57
+ metadata_json = Column("metadata", JSON, nullable=True)
58
+
59
+
60
+ class FileRecord(Base): # pylint: disable=too-few-public-methods
61
+ """SQLAlchemy record for stored files."""
62
+
63
+ __tablename__ = "file_records"
64
+
65
+ id = Column(Integer, primary_key=True, autoincrement=True)
66
+ name = Column(String, nullable=False)
67
+ path = Column(String, nullable=False)
68
+ media_type = Column(String, nullable=True)
69
+ category = Column(String, nullable=False)
70
+ size = Column(Integer, nullable=False)
71
+ topic_id = Column(String, nullable=True)
72
+ created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
73
+ updated_at = Column(
74
+ DateTime(timezone=True), default=_utcnow, onupdate=_utcnow, nullable=False
75
+ )
76
+
77
+
78
+ class ChatMessageRecord(Base): # pylint: disable=too-few-public-methods
79
+ """SQLAlchemy record for persisted chat messages."""
80
+
81
+ __tablename__ = "chat_messages"
82
+
83
+ id = Column(String, primary_key=True)
84
+ direction = Column(String, nullable=False)
85
+ scope = Column(String, nullable=False)
86
+ state = Column(String, nullable=False)
87
+ content = Column(String, nullable=False)
88
+ source = Column(String, nullable=True)
89
+ destination = Column(String, nullable=True)
90
+ topic_id = Column(String, nullable=True)
91
+ attachments_json = Column("attachments", JSON, nullable=True)
92
+ created_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
93
+ updated_at = Column(
94
+ DateTime(timezone=True), default=_utcnow, onupdate=_utcnow, nullable=False
95
+ )
96
+
97
+
98
+ class IdentityStateRecord(Base): # pylint: disable=too-few-public-methods
99
+ """SQLAlchemy record for identity moderation state."""
100
+
101
+ __tablename__ = "identity_states"
102
+
103
+ identity = Column(String, primary_key=True)
104
+ is_banned = Column(Boolean, nullable=False, default=False)
105
+ is_blackholed = Column(Boolean, nullable=False, default=False)
106
+ updated_at = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
107
+
108
+
109
+ class IdentityAnnounceRecord(Base): # pylint: disable=too-few-public-methods
110
+ """SQLAlchemy record for Reticulum announce metadata."""
111
+
112
+ __tablename__ = "identity_announces"
113
+
114
+ destination_hash = Column(String, primary_key=True)
115
+ display_name = Column(String, nullable=True)
116
+ first_seen = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
117
+ last_seen = Column(DateTime(timezone=True), default=_utcnow, nullable=False)
118
+ source_interface = Column(String, nullable=True)
@@ -0,0 +1,49 @@
1
+ """ATAK COT support classes and datapack utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from reticulum_telemetry_hub.atak_cot.base import Contact
6
+ from reticulum_telemetry_hub.atak_cot.base import Group
7
+ from reticulum_telemetry_hub.atak_cot.base import Point
8
+ from reticulum_telemetry_hub.atak_cot.base import Status
9
+ from reticulum_telemetry_hub.atak_cot.base import Takv
10
+ from reticulum_telemetry_hub.atak_cot.base import Track
11
+ from reticulum_telemetry_hub.atak_cot.base import Uid
12
+ from reticulum_telemetry_hub.atak_cot.chat import Chat
13
+ from reticulum_telemetry_hub.atak_cot.chat import ChatGroup
14
+ from reticulum_telemetry_hub.atak_cot.chat import ChatHierarchy
15
+ from reticulum_telemetry_hub.atak_cot.chat import ChatHierarchyContact
16
+ from reticulum_telemetry_hub.atak_cot.chat import ChatHierarchyGroup
17
+ from reticulum_telemetry_hub.atak_cot.chat import Link
18
+ from reticulum_telemetry_hub.atak_cot.chat import Marti
19
+ from reticulum_telemetry_hub.atak_cot.chat import MartiDest
20
+ from reticulum_telemetry_hub.atak_cot.chat import Remarks
21
+ from reticulum_telemetry_hub.atak_cot.detail import Detail
22
+ from reticulum_telemetry_hub.atak_cot.event import Event
23
+ from reticulum_telemetry_hub.atak_cot.event import Packable
24
+ from reticulum_telemetry_hub.atak_cot.event import pack_data
25
+ from reticulum_telemetry_hub.atak_cot.event import unpack_data
26
+
27
+ __all__ = [
28
+ "Contact",
29
+ "Group",
30
+ "Point",
31
+ "Track",
32
+ "Takv",
33
+ "Uid",
34
+ "Status",
35
+ "Chat",
36
+ "ChatGroup",
37
+ "ChatHierarchy",
38
+ "ChatHierarchyContact",
39
+ "ChatHierarchyGroup",
40
+ "Link",
41
+ "Marti",
42
+ "MartiDest",
43
+ "Remarks",
44
+ "Detail",
45
+ "Event",
46
+ "Packable",
47
+ "pack_data",
48
+ "unpack_data",
49
+ ]
@@ -0,0 +1,277 @@
1
+ """Data classes representing ATAK Cursor-on-Target primitives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import xml.etree.ElementTree as ET
7
+
8
+
9
+ @dataclass
10
+ class Point:
11
+ """A geographic point element."""
12
+
13
+ lat: float
14
+ lon: float
15
+ hae: float
16
+ ce: float
17
+ le: float
18
+
19
+ @classmethod
20
+ def from_xml(cls, elem):
21
+ """Create a :class:`Point` from an XML ``<point>`` element."""
22
+
23
+ return cls(
24
+ lat=float(elem.get("lat", 0)),
25
+ lon=float(elem.get("lon", 0)),
26
+ hae=float(elem.get("hae", 0)),
27
+ ce=float(elem.get("ce", 0)),
28
+ le=float(elem.get("le", 0)),
29
+ )
30
+
31
+ def to_element(self):
32
+ """Return an XML element representing this point."""
33
+
34
+ attrib = {
35
+ "lat": str(self.lat),
36
+ "lon": str(self.lon),
37
+ "hae": str(self.hae),
38
+ "ce": str(self.ce),
39
+ "le": str(self.le),
40
+ }
41
+ return ET.Element("point", attrib)
42
+
43
+ def to_dict(self) -> dict:
44
+ """Return a serialisable dictionary representation."""
45
+
46
+ return {
47
+ "lat": self.lat,
48
+ "lon": self.lon,
49
+ "hae": self.hae,
50
+ "ce": self.ce,
51
+ "le": self.le,
52
+ }
53
+
54
+ @classmethod
55
+ def from_dict(cls, data: dict) -> "Point":
56
+ """Create a :class:`Point` from a dictionary."""
57
+
58
+ return cls(
59
+ lat=float(data.get("lat", 0)),
60
+ lon=float(data.get("lon", 0)),
61
+ hae=float(data.get("hae", 0)),
62
+ ce=float(data.get("ce", 0)),
63
+ le=float(data.get("le", 0)),
64
+ )
65
+
66
+
67
+ @dataclass
68
+ class Contact:
69
+ """Identifies the sender of the COT message."""
70
+
71
+ callsign: str
72
+ endpoint: str | None = None
73
+
74
+ @classmethod
75
+ def from_xml(cls, elem):
76
+ """Construct a :class:`Contact` from an XML ``<contact>`` element."""
77
+
78
+ return cls(callsign=elem.get("callsign", ""), endpoint=elem.get("endpoint"))
79
+
80
+ def to_element(self):
81
+ """Return an XML element for the contact."""
82
+
83
+ attrib = {"callsign": self.callsign}
84
+ if self.endpoint:
85
+ attrib["endpoint"] = self.endpoint
86
+ return ET.Element("contact", attrib)
87
+
88
+ def to_dict(self) -> dict:
89
+ """Return a serialisable representation."""
90
+
91
+ data = {"callsign": self.callsign}
92
+ if self.endpoint:
93
+ data["endpoint"] = self.endpoint
94
+ return data
95
+
96
+ @classmethod
97
+ def from_dict(cls, data: dict) -> "Contact":
98
+ """Create a :class:`Contact` from a dictionary."""
99
+
100
+ return cls(callsign=data.get("callsign", ""), endpoint=data.get("endpoint"))
101
+
102
+
103
+ @dataclass
104
+ class Group:
105
+ """Specifies group affiliation for the sender."""
106
+
107
+ name: str
108
+ role: str
109
+
110
+ @classmethod
111
+ def from_xml(cls, elem):
112
+ """Create a :class:`Group` from an XML ``<__group>`` element."""
113
+
114
+ return cls(name=elem.get("name", ""), role=elem.get("role", ""))
115
+
116
+ def to_element(self):
117
+ """Return an XML element for the group affiliation."""
118
+
119
+ return ET.Element("__group", {"name": self.name, "role": self.role})
120
+
121
+ def to_dict(self) -> dict:
122
+ """Return a serialisable representation."""
123
+
124
+ return {"name": self.name, "role": self.role}
125
+
126
+ @classmethod
127
+ def from_dict(cls, data: dict) -> "Group":
128
+ """Create a :class:`Group` from a dictionary."""
129
+
130
+ return cls(name=data.get("name", ""), role=data.get("role", ""))
131
+
132
+
133
+ @dataclass
134
+ class Track:
135
+ """Represents movement information such as speed and bearing."""
136
+
137
+ course: float
138
+ speed: float
139
+
140
+ @classmethod
141
+ def from_xml(cls, elem):
142
+ """Parse an XML ``<track>`` element into a :class:`Track`."""
143
+
144
+ return cls(
145
+ course=float(elem.get("course", 0)), speed=float(elem.get("speed", 0))
146
+ )
147
+
148
+ def to_element(self):
149
+ """Return an XML element for the movement details."""
150
+
151
+ return ET.Element("track", {"course": str(self.course), "speed": str(self.speed)})
152
+
153
+ def to_dict(self) -> dict:
154
+ """Return a serialisable representation."""
155
+
156
+ return {"course": self.course, "speed": self.speed}
157
+
158
+ @classmethod
159
+ def from_dict(cls, data: dict) -> "Track":
160
+ """Create a :class:`Track` from a dictionary."""
161
+
162
+ return cls(
163
+ course=float(data.get("course", 0)), speed=float(data.get("speed", 0))
164
+ )
165
+
166
+
167
+ @dataclass
168
+ class Takv:
169
+ """Describes the TAK client version and platform information."""
170
+
171
+ version: str
172
+ platform: str
173
+ os: str
174
+ device: str
175
+
176
+ @classmethod
177
+ def from_xml(cls, elem):
178
+ """Create a :class:`Takv` from an XML ``<takv>`` element."""
179
+
180
+ return cls(
181
+ version=elem.get("version", ""),
182
+ platform=elem.get("platform", ""),
183
+ os=elem.get("os", ""),
184
+ device=elem.get("device", ""),
185
+ )
186
+
187
+ def to_element(self):
188
+ """Return an XML element representing this TAK client."""
189
+
190
+ return ET.Element(
191
+ "takv",
192
+ {
193
+ "version": self.version,
194
+ "platform": self.platform,
195
+ "os": self.os,
196
+ "device": self.device,
197
+ },
198
+ )
199
+
200
+ def to_dict(self) -> dict:
201
+ """Return a serialisable representation."""
202
+
203
+ return {
204
+ "version": self.version,
205
+ "platform": self.platform,
206
+ "os": self.os,
207
+ "device": self.device,
208
+ }
209
+
210
+ @classmethod
211
+ def from_dict(cls, data: dict) -> "Takv":
212
+ """Create a :class:`Takv` from a dictionary."""
213
+
214
+ return cls(
215
+ version=data.get("version", ""),
216
+ platform=data.get("platform", ""),
217
+ os=data.get("os", ""),
218
+ device=data.get("device", ""),
219
+ )
220
+
221
+
222
+ @dataclass
223
+ class Uid:
224
+ """Nested UID used by ATAK to describe the Droid identifier."""
225
+
226
+ droid: str
227
+
228
+ @classmethod
229
+ def from_xml(cls, elem):
230
+ """Construct a :class:`Uid` from an XML ``<uid>`` element."""
231
+
232
+ return cls(droid=elem.get("Droid", ""))
233
+
234
+ def to_element(self):
235
+ """Return an XML element representing the UID."""
236
+
237
+ return ET.Element("uid", {"Droid": self.droid})
238
+
239
+ def to_dict(self) -> dict:
240
+ """Return a serialisable representation."""
241
+
242
+ return {"Droid": self.droid}
243
+
244
+ @classmethod
245
+ def from_dict(cls, data: dict) -> "Uid":
246
+ """Create a :class:`Uid` from a dictionary."""
247
+
248
+ return cls(droid=data.get("Droid", ""))
249
+
250
+
251
+ @dataclass
252
+ class Status:
253
+ """Represents battery status information."""
254
+
255
+ battery: float
256
+
257
+ @classmethod
258
+ def from_xml(cls, elem):
259
+ """Construct a :class:`Status` from an XML ``<status>`` element."""
260
+
261
+ return cls(battery=float(elem.get("battery", 0)))
262
+
263
+ def to_element(self):
264
+ """Return an XML element representing status."""
265
+
266
+ return ET.Element("status", {"battery": str(self.battery)})
267
+
268
+ def to_dict(self) -> dict:
269
+ """Return a serialisable representation."""
270
+
271
+ return {"battery": self.battery}
272
+
273
+ @classmethod
274
+ def from_dict(cls, data: dict) -> "Status":
275
+ """Create a :class:`Status` from a dictionary."""
276
+
277
+ return cls(battery=float(data.get("battery", 0)))