aprsd 4.1.2__py3-none-any.whl → 4.2.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.
aprsd/packets/core.py CHANGED
@@ -19,26 +19,26 @@ from loguru import logger
19
19
  from aprsd.utils import counter
20
20
 
21
21
  # For mypy to be happy
22
- A = TypeVar("A", bound="DataClassJsonMixin")
22
+ A = TypeVar('A', bound='DataClassJsonMixin')
23
23
  Json = Union[dict, list, str, int, float, bool, None]
24
24
 
25
25
  LOG = logging.getLogger()
26
26
  LOGU = logger
27
27
 
28
- PACKET_TYPE_BULLETIN = "bulletin"
29
- PACKET_TYPE_MESSAGE = "message"
30
- PACKET_TYPE_ACK = "ack"
31
- PACKET_TYPE_REJECT = "reject"
32
- PACKET_TYPE_MICE = "mic-e"
33
- PACKET_TYPE_WX = "wx"
34
- PACKET_TYPE_WEATHER = "weather"
35
- PACKET_TYPE_OBJECT = "object"
36
- PACKET_TYPE_UNKNOWN = "unknown"
37
- PACKET_TYPE_STATUS = "status"
38
- PACKET_TYPE_BEACON = "beacon"
39
- PACKET_TYPE_THIRDPARTY = "thirdparty"
40
- PACKET_TYPE_TELEMETRY = "telemetry-message"
41
- PACKET_TYPE_UNCOMPRESSED = "uncompressed"
28
+ PACKET_TYPE_BULLETIN = 'bulletin'
29
+ PACKET_TYPE_MESSAGE = 'message'
30
+ PACKET_TYPE_ACK = 'ack'
31
+ PACKET_TYPE_REJECT = 'reject'
32
+ PACKET_TYPE_MICE = 'mic-e'
33
+ PACKET_TYPE_WX = 'wx'
34
+ PACKET_TYPE_WEATHER = 'weather'
35
+ PACKET_TYPE_OBJECT = 'object'
36
+ PACKET_TYPE_UNKNOWN = 'unknown'
37
+ PACKET_TYPE_STATUS = 'status'
38
+ PACKET_TYPE_BEACON = 'beacon'
39
+ PACKET_TYPE_THIRDPARTY = 'thirdparty'
40
+ PACKET_TYPE_TELEMETRY = 'telemetry-message'
41
+ PACKET_TYPE_UNCOMPRESSED = 'uncompressed'
42
42
 
43
43
  NO_DATE = datetime(1900, 10, 24)
44
44
 
@@ -67,14 +67,14 @@ def _init_msgNo(): # noqa: N802
67
67
 
68
68
  def _translate_fields(raw: dict) -> dict:
69
69
  # Direct key checks instead of iteration
70
- if "from" in raw:
71
- raw["from_call"] = raw.pop("from")
72
- if "to" in raw:
73
- raw["to_call"] = raw.pop("to")
70
+ if 'from' in raw:
71
+ raw['from_call'] = raw.pop('from')
72
+ if 'to' in raw:
73
+ raw['to_call'] = raw.pop('to')
74
74
 
75
75
  # addresse overrides to_call
76
- if "addresse" in raw:
77
- raw["to_call"] = raw["addresse"]
76
+ if 'addresse' in raw:
77
+ raw['to_call'] = raw['addresse']
78
78
 
79
79
  return raw
80
80
 
@@ -82,7 +82,7 @@ def _translate_fields(raw: dict) -> dict:
82
82
  @dataclass_json
83
83
  @dataclass(unsafe_hash=True)
84
84
  class Packet:
85
- _type: str = field(default="Packet", hash=False)
85
+ _type: str = field(default='Packet', hash=False)
86
86
  from_call: Optional[str] = field(default=None)
87
87
  to_call: Optional[str] = field(default=None)
88
88
  addresse: Optional[str] = field(default=None)
@@ -120,7 +120,7 @@ class Packet:
120
120
  @property
121
121
  def key(self) -> str:
122
122
  """Build a key for finding this packet in a dict."""
123
- return f"{self.from_call}:{self.addresse}:{self.msgNo}"
123
+ return f'{self.from_call}:{self.addresse}:{self.msgNo}'
124
124
 
125
125
  def update_timestamp(self) -> None:
126
126
  self.timestamp = _init_timestamp()
@@ -133,7 +133,7 @@ class Packet:
133
133
  the human readable payload.
134
134
  """
135
135
  self.prepare()
136
- msg = self._filter_for_send(self.raw).rstrip("\n")
136
+ msg = self._filter_for_send(self.raw).rstrip('\n')
137
137
  return msg
138
138
 
139
139
  def prepare(self, create_msg_number=False) -> None:
@@ -152,11 +152,11 @@ class Packet:
152
152
  )
153
153
 
154
154
  # The base packet class has no real payload
155
- self.payload = f":{self.to_call.ljust(9)}"
155
+ self.payload = f':{self.to_call.ljust(9)}'
156
156
 
157
157
  def _build_raw(self) -> None:
158
158
  """Build the self.raw which is what is sent over the air."""
159
- self.raw = "{}>APZ100:{}".format(
159
+ self.raw = '{}>APZ100:{}'.format(
160
160
  self.from_call,
161
161
  self.payload,
162
162
  )
@@ -168,13 +168,13 @@ class Packet:
168
168
  # 67 displays 64 on the ftm400. (+3 {01 suffix)
169
169
  # feature req: break long ones into two msgs
170
170
  if not msg:
171
- return ""
171
+ return ''
172
172
 
173
173
  message = msg[:67]
174
174
  # We all miss George Carlin
175
175
  return re.sub(
176
- "fuck|shit|cunt|piss|cock|bitch",
177
- "****",
176
+ 'fuck|shit|cunt|piss|cock|bitch',
177
+ '****',
178
178
  message,
179
179
  flags=re.IGNORECASE,
180
180
  )
@@ -183,100 +183,98 @@ class Packet:
183
183
  """Show the raw version of the packet"""
184
184
  self.prepare()
185
185
  if not self.raw:
186
- raise ValueError("self.raw is unset")
186
+ raise ValueError('self.raw is unset')
187
187
  return self.raw
188
188
 
189
189
  def __repr__(self) -> str:
190
190
  """Build the repr version of the packet."""
191
191
  return (
192
- f"{self.__class__.__name__}:"
193
- f" From: {self.from_call} "
194
- f" To: {self.to_call}"
192
+ f'{self.__class__.__name__}: From: {self.from_call} To: {self.to_call}'
195
193
  )
196
194
 
197
195
 
198
196
  @dataclass_json
199
197
  @dataclass(unsafe_hash=True)
200
198
  class AckPacket(Packet):
201
- _type: str = field(default="AckPacket", hash=False)
199
+ _type: str = field(default='AckPacket', hash=False)
202
200
 
203
201
  def _build_payload(self):
204
- self.payload = f":{self.to_call: <9}:ack{self.msgNo}"
202
+ self.payload = f':{self.to_call: <9}:ack{self.msgNo}'
205
203
 
206
204
 
207
205
  @dataclass_json
208
206
  @dataclass(unsafe_hash=True)
209
207
  class BulletinPacket(Packet):
210
- _type: str = "BulletinPacket"
208
+ _type: str = 'BulletinPacket'
211
209
  # Holds the encapsulated packet
212
- bid: Optional[str] = field(default="1")
210
+ bid: Optional[str] = field(default='1')
213
211
  message_text: Optional[str] = field(default=None)
214
212
 
215
213
  @property
216
214
  def key(self) -> str:
217
215
  """Build a key for finding this packet in a dict."""
218
- return f"{self.from_call}:BLN{self.bid}"
216
+ return f'{self.from_call}:BLN{self.bid}'
219
217
 
220
218
  @property
221
219
  def human_info(self) -> str:
222
- return f"BLN{self.bid} {self.message_text}"
220
+ return f'BLN{self.bid} {self.message_text}'
223
221
 
224
222
  def _build_payload(self) -> None:
225
- self.payload = f":BLN{self.bid:<9}" f":{self.message_text}"
223
+ self.payload = f':BLN{self.bid:<9}:{self.message_text}'
226
224
 
227
225
 
228
226
  @dataclass_json
229
227
  @dataclass(unsafe_hash=True)
230
228
  class RejectPacket(Packet):
231
- _type: str = field(default="RejectPacket", hash=False)
229
+ _type: str = field(default='RejectPacket', hash=False)
232
230
  response: Optional[str] = field(default=None)
233
231
 
234
232
  def __post__init__(self):
235
233
  if self.response:
236
- LOG.warning("Response set!")
234
+ LOG.warning('Response set!')
237
235
 
238
236
  def _build_payload(self):
239
- self.payload = f":{self.to_call: <9}:rej{self.msgNo}"
237
+ self.payload = f':{self.to_call: <9}:rej{self.msgNo}'
240
238
 
241
239
 
242
240
  @dataclass_json
243
241
  @dataclass(unsafe_hash=True)
244
242
  class MessagePacket(Packet):
245
- _type: str = field(default="MessagePacket", hash=False)
243
+ _type: str = field(default='MessagePacket', hash=False)
246
244
  message_text: Optional[str] = field(default=None)
247
245
 
248
246
  @property
249
247
  def human_info(self) -> str:
250
248
  self.prepare()
251
- return self._filter_for_send(self.message_text).rstrip("\n")
249
+ return self._filter_for_send(self.message_text).rstrip('\n')
252
250
 
253
251
  def _build_payload(self):
254
252
  if self.msgNo:
255
- self.payload = ":{}:{}{{{}".format(
253
+ self.payload = ':{}:{}{{{}'.format(
256
254
  self.to_call.ljust(9),
257
- self._filter_for_send(self.message_text).rstrip("\n"),
255
+ self._filter_for_send(self.message_text).rstrip('\n'),
258
256
  str(self.msgNo),
259
257
  )
260
258
  else:
261
- self.payload = ":{}:{}".format(
259
+ self.payload = ':{}:{}'.format(
262
260
  self.to_call.ljust(9),
263
- self._filter_for_send(self.message_text).rstrip("\n"),
261
+ self._filter_for_send(self.message_text).rstrip('\n'),
264
262
  )
265
263
 
266
264
 
267
265
  @dataclass_json
268
266
  @dataclass(unsafe_hash=True)
269
267
  class StatusPacket(Packet):
270
- _type: str = field(default="StatusPacket", hash=False)
268
+ _type: str = field(default='StatusPacket', hash=False)
271
269
  status: Optional[str] = field(default=None)
272
270
  messagecapable: bool = field(default=False)
273
271
  comment: Optional[str] = field(default=None)
274
272
  raw_timestamp: Optional[str] = field(default=None)
275
273
 
276
274
  def _build_payload(self):
277
- self.payload = ":{}:{}{{{}".format(
275
+ self.payload = ':{}:{}{{{}'.format(
278
276
  self.to_call.ljust(9),
279
- self._filter_for_send(self.status).rstrip("\n"),
277
+ self._filter_for_send(self.status).rstrip('\n'),
280
278
  str(self.msgNo),
281
279
  )
282
280
 
@@ -289,7 +287,7 @@ class StatusPacket(Packet):
289
287
  @dataclass_json
290
288
  @dataclass(unsafe_hash=True)
291
289
  class GPSPacket(Packet):
292
- _type: str = field(default="GPSPacket", hash=False)
290
+ _type: str = field(default='GPSPacket', hash=False)
293
291
  latitude: float = field(default=0.00)
294
292
  longitude: float = field(default=0.00)
295
293
  altitude: float = field(default=0.00)
@@ -297,8 +295,8 @@ class GPSPacket(Packet):
297
295
  posambiguity: int = field(default=0)
298
296
  messagecapable: bool = field(default=False)
299
297
  comment: Optional[str] = field(default=None)
300
- symbol: str = field(default="l")
301
- symbol_table: str = field(default="/")
298
+ symbol: str = field(default='l')
299
+ symbol_table: str = field(default='/')
302
300
  raw_timestamp: Optional[str] = field(default=None)
303
301
  object_name: Optional[str] = field(default=None)
304
302
  object_format: Optional[str] = field(default=None)
@@ -318,7 +316,7 @@ class GPSPacket(Packet):
318
316
  def _build_time_zulu(self):
319
317
  """Build the timestamp in UTC/zulu."""
320
318
  if self.timestamp:
321
- return datetime.utcfromtimestamp(self.timestamp).strftime("%d%H%M")
319
+ return datetime.utcfromtimestamp(self.timestamp).strftime('%d%H%M')
322
320
 
323
321
  def _build_payload(self):
324
322
  """The payload is the non headers portion of the packet."""
@@ -326,7 +324,7 @@ class GPSPacket(Packet):
326
324
  lat = aprslib_util.latitude_to_ddm(self.latitude)
327
325
  long = aprslib_util.longitude_to_ddm(self.longitude)
328
326
  payload = [
329
- "@" if self.timestamp else "!",
327
+ '@' if self.timestamp else '!',
330
328
  time_zulu,
331
329
  lat,
332
330
  self.symbol_table,
@@ -337,34 +335,34 @@ class GPSPacket(Packet):
337
335
  if self.comment:
338
336
  payload.append(self._filter_for_send(self.comment))
339
337
 
340
- self.payload = "".join(payload)
338
+ self.payload = ''.join(payload)
341
339
 
342
340
  def _build_raw(self):
343
- self.raw = f"{self.from_call}>{self.to_call},WIDE2-1:" f"{self.payload}"
341
+ self.raw = f'{self.from_call}>{self.to_call},WIDE2-1:{self.payload}'
344
342
 
345
343
  @property
346
344
  def human_info(self) -> str:
347
345
  h_str = []
348
- h_str.append(f"Lat:{self.latitude:03.3f}")
349
- h_str.append(f"Lon:{self.longitude:03.3f}")
346
+ h_str.append(f'Lat:{self.latitude:03.3f}')
347
+ h_str.append(f'Lon:{self.longitude:03.3f}')
350
348
  if self.altitude:
351
- h_str.append(f"Altitude {self.altitude:03.0f}")
349
+ h_str.append(f'Altitude {self.altitude:03.0f}')
352
350
  if self.speed:
353
- h_str.append(f"Speed {self.speed:03.0f}MPH")
351
+ h_str.append(f'Speed {self.speed:03.0f}MPH')
354
352
  if self.course:
355
- h_str.append(f"Course {self.course:03.0f}")
353
+ h_str.append(f'Course {self.course:03.0f}')
356
354
  if self.rng:
357
- h_str.append(f"RNG {self.rng:03.0f}")
355
+ h_str.append(f'RNG {self.rng:03.0f}')
358
356
  if self.phg:
359
- h_str.append(f"PHG {self.phg}")
357
+ h_str.append(f'PHG {self.phg}')
360
358
 
361
- return " ".join(h_str)
359
+ return ' '.join(h_str)
362
360
 
363
361
 
364
362
  @dataclass_json
365
363
  @dataclass(unsafe_hash=True)
366
364
  class BeaconPacket(GPSPacket):
367
- _type: str = field(default="BeaconPacket", hash=False)
365
+ _type: str = field(default='BeaconPacket', hash=False)
368
366
 
369
367
  def _build_payload(self):
370
368
  """The payload is the non headers portion of the packet."""
@@ -372,42 +370,42 @@ class BeaconPacket(GPSPacket):
372
370
  lat = aprslib_util.latitude_to_ddm(self.latitude)
373
371
  lon = aprslib_util.longitude_to_ddm(self.longitude)
374
372
 
375
- self.payload = f"@{time_zulu}z{lat}{self.symbol_table}" f"{lon}"
373
+ self.payload = f'@{time_zulu}z{lat}{self.symbol_table}{lon}'
376
374
 
377
375
  if self.comment:
378
376
  comment = self._filter_for_send(self.comment)
379
- self.payload = f"{self.payload}{self.symbol}{comment}"
377
+ self.payload = f'{self.payload}{self.symbol}{comment}'
380
378
  else:
381
- self.payload = f"{self.payload}{self.symbol}APRSD Beacon"
379
+ self.payload = f'{self.payload}{self.symbol}APRSD Beacon'
382
380
 
383
381
  def _build_raw(self):
384
- self.raw = f"{self.from_call}>APZ100:" f"{self.payload}"
382
+ self.raw = f'{self.from_call}>APZ100:{self.payload}'
385
383
 
386
384
  @property
387
385
  def key(self) -> str:
388
386
  """Build a key for finding this packet in a dict."""
389
387
  if self.raw_timestamp:
390
- return f"{self.from_call}:{self.raw_timestamp}"
388
+ return f'{self.from_call}:{self.raw_timestamp}'
391
389
  else:
392
- return f"{self.from_call}:{self.human_info.replace(' ', '')}"
390
+ return f'{self.from_call}:{self.human_info.replace(" ", "")}'
393
391
 
394
392
  @property
395
393
  def human_info(self) -> str:
396
394
  h_str = []
397
- h_str.append(f"Lat:{self.latitude:03.3f}")
398
- h_str.append(f"Lon:{self.longitude:03.3f}")
399
- h_str.append(f"{self.comment}")
400
- return " ".join(h_str)
395
+ h_str.append(f'Lat:{self.latitude:03.3f}')
396
+ h_str.append(f'Lon:{self.longitude:03.3f}')
397
+ h_str.append(f'{self.comment}')
398
+ return ' '.join(h_str)
401
399
 
402
400
 
403
401
  @dataclass_json
404
402
  @dataclass(unsafe_hash=True)
405
403
  class MicEPacket(GPSPacket):
406
- _type: str = field(default="MicEPacket", hash=False)
404
+ _type: str = field(default='MicEPacket', hash=False)
407
405
  messagecapable: bool = False
408
406
  mbits: Optional[str] = None
409
407
  mtype: Optional[str] = None
410
- telemetry: Optional[dict] = field(default=None)
408
+ telemetry: Optional[dict] = field(default=None, hash=False)
411
409
  # in MPH
412
410
  speed: float = 0.00
413
411
  # 0 to 360
@@ -416,24 +414,24 @@ class MicEPacket(GPSPacket):
416
414
  @property
417
415
  def key(self) -> str:
418
416
  """Build a key for finding this packet in a dict."""
419
- return f"{self.from_call}:{self.human_info.replace(' ', '')}"
417
+ return f'{self.from_call}:{self.human_info.replace(" ", "")}'
420
418
 
421
419
  @property
422
420
  def human_info(self) -> str:
423
421
  h_info = super().human_info
424
- return f"{h_info} {self.mbits} mbits"
422
+ return f'{h_info} {self.mbits} mbits'
425
423
 
426
424
 
427
425
  @dataclass_json
428
426
  @dataclass(unsafe_hash=True)
429
427
  class TelemetryPacket(GPSPacket):
430
- _type: str = field(default="TelemetryPacket", hash=False)
428
+ _type: str = field(default='TelemetryPacket', hash=False)
431
429
  messagecapable: bool = False
432
430
  mbits: Optional[str] = None
433
431
  mtype: Optional[str] = None
434
432
  telemetry: Optional[dict] = field(default=None)
435
- tPARM: Optional[list[str]] = field(default=None) # noqa: N815
436
- tUNIT: Optional[list[str]] = field(default=None) # noqa: N815
433
+ tPARM: Optional[list[str]] = field(default=None, hash=False) # noqa: N815
434
+ tUNIT: Optional[list[str]] = field(default=None, hash=False) # noqa: N815
437
435
  # in MPH
438
436
  speed: float = 0.00
439
437
  # 0 to 360
@@ -443,23 +441,23 @@ class TelemetryPacket(GPSPacket):
443
441
  def key(self) -> str:
444
442
  """Build a key for finding this packet in a dict."""
445
443
  if self.raw_timestamp:
446
- return f"{self.from_call}:{self.raw_timestamp}"
444
+ return f'{self.from_call}:{self.raw_timestamp}'
447
445
  else:
448
- return f"{self.from_call}:{self.human_info.replace(' ', '')}"
446
+ return f'{self.from_call}:{self.human_info.replace(" ", "")}'
449
447
 
450
448
  @property
451
449
  def human_info(self) -> str:
452
450
  h_info = super().human_info
453
- return f"{h_info} {self.telemetry}"
451
+ return f'{h_info} {self.telemetry}'
454
452
 
455
453
 
456
454
  @dataclass_json
457
455
  @dataclass(unsafe_hash=True)
458
456
  class ObjectPacket(GPSPacket):
459
- _type: str = field(default="ObjectPacket", hash=False)
457
+ _type: str = field(default='ObjectPacket', hash=False)
460
458
  alive: bool = True
461
459
  raw_timestamp: Optional[str] = None
462
- symbol: str = field(default="r")
460
+ symbol: str = field(default='r')
463
461
  # in MPH
464
462
  speed: float = 0.00
465
463
  # 0 to 360
@@ -470,11 +468,11 @@ class ObjectPacket(GPSPacket):
470
468
  lat = aprslib_util.latitude_to_ddm(self.latitude)
471
469
  long = aprslib_util.longitude_to_ddm(self.longitude)
472
470
 
473
- self.payload = f"*{time_zulu}z{lat}{self.symbol_table}" f"{long}{self.symbol}"
471
+ self.payload = f'*{time_zulu}z{lat}{self.symbol_table}{long}{self.symbol}'
474
472
 
475
473
  if self.comment:
476
474
  comment = self._filter_for_send(self.comment)
477
- self.payload = f"{self.payload}{comment}"
475
+ self.payload = f'{self.payload}{comment}'
478
476
 
479
477
  def _build_raw(self):
480
478
  """
@@ -487,18 +485,18 @@ class ObjectPacket(GPSPacket):
487
485
  The frequency, uplink_tone, offset is part of the comment
488
486
  """
489
487
 
490
- self.raw = f"{self.from_call}>APZ100:;{self.to_call:9s}" f"{self.payload}"
488
+ self.raw = f'{self.from_call}>APZ100:;{self.to_call:9s}{self.payload}'
491
489
 
492
490
  @property
493
491
  def human_info(self) -> str:
494
492
  h_info = super().human_info
495
- return f"{h_info} {self.comment}"
493
+ return f'{h_info} {self.comment}'
496
494
 
497
495
 
498
496
  @dataclass(unsafe_hash=True)
499
497
  class WeatherPacket(GPSPacket, DataClassJsonMixin):
500
- _type: str = field(default="WeatherPacket", hash=False)
501
- symbol: str = "_"
498
+ _type: str = field(default='WeatherPacket', hash=False)
499
+ symbol: str = '_'
502
500
  wind_speed: float = 0.00
503
501
  wind_direction: int = 0
504
502
  wind_gust: float = 0.00
@@ -516,8 +514,8 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
516
514
  speed: Optional[float] = field(default=None)
517
515
 
518
516
  def _translate(self, raw: dict) -> dict:
519
- for key in raw["weather"]:
520
- raw[key] = raw["weather"][key]
517
+ for key in raw['weather']:
518
+ raw[key] = raw['weather'][key]
521
519
 
522
520
  # If we have the broken aprslib, then we need to
523
521
  # Convert the course and speed to wind_speed and wind_direction
@@ -525,36 +523,36 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
525
523
  # https://github.com/rossengeorgiev/aprs-python/issues/80
526
524
  # Wind speed and course is option in the SPEC.
527
525
  # For some reason aprslib multiplies the speed by 1.852.
528
- if "wind_speed" not in raw and "wind_direction" not in raw:
526
+ if 'wind_speed' not in raw and 'wind_direction' not in raw:
529
527
  # Most likely this is the broken aprslib
530
528
  # So we need to convert the wind_gust speed
531
- raw["wind_gust"] = round(raw.get("wind_gust", 0) / 0.44704, 3)
532
- if "wind_speed" not in raw:
533
- wind_speed = raw.get("speed")
529
+ raw['wind_gust'] = round(raw.get('wind_gust', 0) / 0.44704, 3)
530
+ if 'wind_speed' not in raw:
531
+ wind_speed = raw.get('speed')
534
532
  if wind_speed:
535
- raw["wind_speed"] = round(wind_speed / 1.852, 3)
536
- raw["weather"]["wind_speed"] = raw["wind_speed"]
537
- if "speed" in raw:
538
- del raw["speed"]
533
+ raw['wind_speed'] = round(wind_speed / 1.852, 3)
534
+ raw['weather']['wind_speed'] = raw['wind_speed']
535
+ if 'speed' in raw:
536
+ del raw['speed']
539
537
  # Let's adjust the rain numbers as well, since it's wrong
540
- raw["rain_1h"] = round((raw.get("rain_1h", 0) / 0.254) * 0.01, 3)
541
- raw["weather"]["rain_1h"] = raw["rain_1h"]
542
- raw["rain_24h"] = round((raw.get("rain_24h", 0) / 0.254) * 0.01, 3)
543
- raw["weather"]["rain_24h"] = raw["rain_24h"]
544
- raw["rain_since_midnight"] = round(
545
- (raw.get("rain_since_midnight", 0) / 0.254) * 0.01, 3
538
+ raw['rain_1h'] = round((raw.get('rain_1h', 0) / 0.254) * 0.01, 3)
539
+ raw['weather']['rain_1h'] = raw['rain_1h']
540
+ raw['rain_24h'] = round((raw.get('rain_24h', 0) / 0.254) * 0.01, 3)
541
+ raw['weather']['rain_24h'] = raw['rain_24h']
542
+ raw['rain_since_midnight'] = round(
543
+ (raw.get('rain_since_midnight', 0) / 0.254) * 0.01, 3
546
544
  )
547
- raw["weather"]["rain_since_midnight"] = raw["rain_since_midnight"]
545
+ raw['weather']['rain_since_midnight'] = raw['rain_since_midnight']
548
546
 
549
- if "wind_direction" not in raw:
550
- wind_direction = raw.get("course")
547
+ if 'wind_direction' not in raw:
548
+ wind_direction = raw.get('course')
551
549
  if wind_direction:
552
- raw["wind_direction"] = wind_direction
553
- raw["weather"]["wind_direction"] = raw["wind_direction"]
554
- if "course" in raw:
555
- del raw["course"]
550
+ raw['wind_direction'] = wind_direction
551
+ raw['weather']['wind_direction'] = raw['wind_direction']
552
+ if 'course' in raw:
553
+ del raw['course']
556
554
 
557
- del raw["weather"]
555
+ del raw['weather']
558
556
  return raw
559
557
 
560
558
  @classmethod
@@ -567,20 +565,20 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
567
565
  def key(self) -> str:
568
566
  """Build a key for finding this packet in a dict."""
569
567
  if self.raw_timestamp:
570
- return f"{self.from_call}:{self.raw_timestamp}"
568
+ return f'{self.from_call}:{self.raw_timestamp}'
571
569
  elif self.wx_raw_timestamp:
572
- return f"{self.from_call}:{self.wx_raw_timestamp}"
570
+ return f'{self.from_call}:{self.wx_raw_timestamp}'
573
571
 
574
572
  @property
575
573
  def human_info(self) -> str:
576
574
  h_str = []
577
- h_str.append(f"Temp {self.temperature:03.0f}F")
578
- h_str.append(f"Humidity {self.humidity}%")
579
- h_str.append(f"Wind {self.wind_speed:03.0f}MPH@{self.wind_direction}")
580
- h_str.append(f"Pressure {self.pressure}mb")
581
- h_str.append(f"Rain {self.rain_24h}in/24hr")
575
+ h_str.append(f'Temp {self.temperature:03.0f}F')
576
+ h_str.append(f'Humidity {self.humidity}%')
577
+ h_str.append(f'Wind {self.wind_speed:03.0f}MPH@{self.wind_direction}')
578
+ h_str.append(f'Pressure {self.pressure}mb')
579
+ h_str.append(f'Rain {self.rain_24h}in/24hr')
582
580
 
583
- return " ".join(h_str)
581
+ return ' '.join(h_str)
584
582
 
585
583
  def _build_payload(self):
586
584
  """Build an uncompressed weather packet
@@ -610,49 +608,49 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
610
608
  time_zulu = self._build_time_zulu()
611
609
 
612
610
  contents = [
613
- f"@{time_zulu}z{self.latitude}{self.symbol_table}",
614
- f"{self.longitude}{self.symbol}",
615
- f"{self.wind_direction:03d}",
611
+ f'@{time_zulu}z{self.latitude}{self.symbol_table}',
612
+ f'{self.longitude}{self.symbol}',
613
+ f'{self.wind_direction:03d}',
616
614
  # Speed = sustained 1 minute wind speed in mph
617
- f"{self.symbol_table}",
618
- f"{self.wind_speed:03.0f}",
615
+ f'{self.symbol_table}',
616
+ f'{self.wind_speed:03.0f}',
619
617
  # wind gust (peak wind speed in mph in the last 5 minutes)
620
- f"g{self.wind_gust:03.0f}",
618
+ f'g{self.wind_gust:03.0f}',
621
619
  # Temperature in degrees F
622
- f"t{self.temperature:03.0f}",
620
+ f't{self.temperature:03.0f}',
623
621
  # Rainfall (in hundredths of an inch) in the last hour
624
- f"r{self.rain_1h * 100:03.0f}",
622
+ f'r{self.rain_1h * 100:03.0f}',
625
623
  # Rainfall (in hundredths of an inch) in last 24 hours
626
- f"p{self.rain_24h * 100:03.0f}",
624
+ f'p{self.rain_24h * 100:03.0f}',
627
625
  # Rainfall (in hundredths of an inch) since midnigt
628
- f"P{self.rain_since_midnight * 100:03.0f}",
626
+ f'P{self.rain_since_midnight * 100:03.0f}',
629
627
  # Humidity
630
- f"h{self.humidity:02d}",
628
+ f'h{self.humidity:02d}',
631
629
  # Barometric pressure (in tenths of millibars/tenths of hPascal)
632
- f"b{self.pressure:05.0f}",
630
+ f'b{self.pressure:05.0f}',
633
631
  ]
634
632
  if self.comment:
635
- comment = self.filter_for_send(self.comment)
633
+ comment = self._filter_for_send(self.comment)
636
634
  contents.append(comment)
637
- self.payload = "".join(contents)
635
+ self.payload = ''.join(contents)
638
636
 
639
637
  def _build_raw(self):
640
- self.raw = f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:" f"{self.payload}"
638
+ self.raw = f'{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:{self.payload}'
641
639
 
642
640
 
643
641
  @dataclass(unsafe_hash=True)
644
642
  class ThirdPartyPacket(Packet, DataClassJsonMixin):
645
- _type: str = "ThirdPartyPacket"
643
+ _type: str = 'ThirdPartyPacket'
646
644
  # Holds the encapsulated packet
647
645
  subpacket: Optional[type[Packet]] = field(default=None, compare=True, hash=False)
648
646
 
649
647
  def __repr__(self):
650
648
  """Build the repr version of the packet."""
651
649
  repr_str = (
652
- f"{self.__class__.__name__}:"
653
- f" From: {self.from_call} "
654
- f" To: {self.to_call} "
655
- f" Subpacket: {repr(self.subpacket)}"
650
+ f'{self.__class__.__name__}:'
651
+ f' From: {self.from_call} '
652
+ f' To: {self.to_call} '
653
+ f' Subpacket: {repr(self.subpacket)}'
656
654
  )
657
655
 
658
656
  return repr_str
@@ -666,12 +664,12 @@ class ThirdPartyPacket(Packet, DataClassJsonMixin):
666
664
  @property
667
665
  def key(self) -> str:
668
666
  """Build a key for finding this packet in a dict."""
669
- return f"{self.from_call}:{self.subpacket.key}"
667
+ return f'{self.from_call}:{self.subpacket.key}'
670
668
 
671
669
  @property
672
670
  def human_info(self) -> str:
673
671
  sub_info = self.subpacket.human_info
674
- return f"{self.from_call}->{self.to_call} {sub_info}"
672
+ return f'{self.from_call}->{self.to_call} {sub_info}'
675
673
 
676
674
 
677
675
  @dataclass_json(undefined=Undefined.INCLUDE)
@@ -683,11 +681,12 @@ class UnknownPacket:
683
681
  """
684
682
 
685
683
  unknown_fields: CatchAll
686
- _type: str = "UnknownPacket"
684
+ _type: str = 'UnknownPacket'
687
685
  from_call: Optional[str] = field(default=None)
688
686
  to_call: Optional[str] = field(default=None)
689
687
  msgNo: str = field(default_factory=_init_msgNo) # noqa: N815
690
688
  format: Optional[str] = field(default=None)
689
+ timestamp: float = field(default_factory=_init_timestamp, compare=False, hash=False)
691
690
  raw: Optional[str] = field(default=None)
692
691
  raw_dict: dict = field(
693
692
  repr=False, default_factory=lambda: {}, compare=False, hash=False
@@ -701,7 +700,7 @@ class UnknownPacket:
701
700
  @property
702
701
  def key(self) -> str:
703
702
  """Build a key for finding this packet in a dict."""
704
- return f"{self.from_call}:{self.packet_type}:{self.to_call}"
703
+ return f'{self.from_call}:{self.packet_type}:{self.to_call}'
705
704
 
706
705
  @property
707
706
  def human_info(self) -> str:
@@ -728,20 +727,20 @@ TYPE_LOOKUP: dict[str, type[Packet]] = {
728
727
  def get_packet_type(packet: dict) -> str:
729
728
  """Decode the packet type from the packet."""
730
729
 
731
- pkt_format = packet.get("format")
732
- msg_response = packet.get("response")
730
+ pkt_format = packet.get('format')
731
+ msg_response = packet.get('response')
733
732
  packet_type = PACKET_TYPE_UNKNOWN
734
- if pkt_format == "message" and msg_response == "ack":
733
+ if pkt_format == 'message' and msg_response == 'ack':
735
734
  packet_type = PACKET_TYPE_ACK
736
- elif pkt_format == "message" and msg_response == "rej":
735
+ elif pkt_format == 'message' and msg_response == 'rej':
737
736
  packet_type = PACKET_TYPE_REJECT
738
- elif pkt_format == "message":
737
+ elif pkt_format == 'message':
739
738
  packet_type = PACKET_TYPE_MESSAGE
740
- elif pkt_format == "mic-e":
739
+ elif pkt_format == 'mic-e':
741
740
  packet_type = PACKET_TYPE_MICE
742
- elif pkt_format == "object":
741
+ elif pkt_format == 'object':
743
742
  packet_type = PACKET_TYPE_OBJECT
744
- elif pkt_format == "status":
743
+ elif pkt_format == 'status':
745
744
  packet_type = PACKET_TYPE_STATUS
746
745
  elif pkt_format == PACKET_TYPE_BULLETIN:
747
746
  packet_type = PACKET_TYPE_BULLETIN
@@ -752,13 +751,13 @@ def get_packet_type(packet: dict) -> str:
752
751
  elif pkt_format == PACKET_TYPE_WX:
753
752
  packet_type = PACKET_TYPE_WEATHER
754
753
  elif pkt_format == PACKET_TYPE_UNCOMPRESSED:
755
- if packet.get("symbol") == "_":
754
+ if packet.get('symbol') == '_':
756
755
  packet_type = PACKET_TYPE_WEATHER
757
756
  elif pkt_format == PACKET_TYPE_THIRDPARTY:
758
757
  packet_type = PACKET_TYPE_THIRDPARTY
759
758
 
760
759
  if packet_type == PACKET_TYPE_UNKNOWN:
761
- if "latitude" in packet:
760
+ if 'latitude' in packet:
762
761
  packet_type = PACKET_TYPE_BEACON
763
762
  else:
764
763
  packet_type = PACKET_TYPE_UNKNOWN
@@ -780,32 +779,32 @@ def is_mice_packet(packet: dict[Any, Any]) -> bool:
780
779
  def factory(raw_packet: dict[Any, Any]) -> type[Packet]:
781
780
  """Factory method to create a packet from a raw packet string."""
782
781
  raw = raw_packet
783
- if "_type" in raw:
784
- cls = globals()[raw["_type"]]
782
+ if '_type' in raw:
783
+ cls = globals()[raw['_type']]
785
784
  return cls.from_dict(raw)
786
785
 
787
- raw["raw_dict"] = raw.copy()
786
+ raw['raw_dict'] = raw.copy()
788
787
  raw = _translate_fields(raw)
789
788
 
790
789
  packet_type = get_packet_type(raw)
791
790
 
792
- raw["packet_type"] = packet_type
791
+ raw['packet_type'] = packet_type
793
792
  packet_class = TYPE_LOOKUP[packet_type]
794
793
  if packet_type == PACKET_TYPE_WX:
795
794
  # the weather information is in a dict
796
795
  # this brings those values out to the outer dict
797
796
  packet_class = WeatherPacket
798
- elif packet_type == PACKET_TYPE_OBJECT and "weather" in raw:
797
+ elif packet_type == PACKET_TYPE_OBJECT and 'weather' in raw:
799
798
  packet_class = WeatherPacket
800
799
  elif packet_type == PACKET_TYPE_UNKNOWN:
801
800
  # Try and figure it out here
802
- if "latitude" in raw:
801
+ if 'latitude' in raw:
803
802
  packet_class = GPSPacket
804
803
  else:
805
804
  # LOG.warning(raw)
806
805
  packet_class = UnknownPacket
807
806
 
808
- raw.get("addresse", raw.get("to_call"))
807
+ raw.get('addresse', raw.get('to_call'))
809
808
 
810
809
  # TODO: Find a global way to enable/disable this
811
810
  # LOGU.opt(colors=True).info(