diff --git a/aprsd/client.py b/aprsd/client.py index 3cfed8a..6a8b26d 100644 --- a/aprsd/client.py +++ b/aprsd/client.py @@ -137,7 +137,7 @@ class APRSISClient(Client): def decode_packet(self, *args, **kwargs): """APRS lib already decodes this.""" - return core.Packet.factory(args[0]) + return core.factory(args[0]) def setup_connection(self): user = CONF.aprs_network.login @@ -238,7 +238,7 @@ class KISSClient(Client): # LOG.debug(f"Decoding {msg}") raw = aprslib.parse(str(frame)) - packet = core.Packet.factory(raw) + packet = core.factory(raw) if isinstance(packet, core.ThirdParty): return packet.subpacket else: diff --git a/aprsd/clients/fake.py b/aprsd/clients/fake.py index b468808..c93a6c3 100644 --- a/aprsd/clients/fake.py +++ b/aprsd/clients/fake.py @@ -67,7 +67,7 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass): # Generate packets here? raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW" pkt_raw = aprslib.parse(raw) - pkt = core.Packet.factory(pkt_raw) + pkt = core.factory(pkt_raw) callback(packet=pkt) LOG.debug(f"END blocking FAKE consumer {self}") time.sleep(8) diff --git a/aprsd/packets/__init__.py b/aprsd/packets/__init__.py index dcb2064..17ecb89 100644 --- a/aprsd/packets/__init__.py +++ b/aprsd/packets/__init__.py @@ -1,6 +1,6 @@ from aprsd.packets.core import ( # noqa: F401 AckPacket, BeaconPacket, GPSPacket, MessagePacket, MicEPacket, Packet, - RejectPacket, StatusPacket, WeatherPacket, + RejectPacket, StatusPacket, ThirdPartyPacket, WeatherPacket, factory, ) from aprsd.packets.packet_list import PacketList # noqa: F401 from aprsd.packets.seen_list import SeenList # noqa: F401 diff --git a/aprsd/packets/core.py b/aprsd/packets/core.py index 17b1234..560423d 100644 --- a/aprsd/packets/core.py +++ b/aprsd/packets/core.py @@ -1,14 +1,12 @@ -import abc from dataclasses import asdict, dataclass, field from datetime import datetime import logging import re import time # Due to a failure in python 3.8 -from typing import List +from typing import List, Optional -import dacite -from dataclasses_json import dataclass_json +from dataclasses_json import DataClassJsonMixin from aprsd.utils import counter @@ -19,7 +17,8 @@ PACKET_TYPE_MESSAGE = "message" PACKET_TYPE_ACK = "ack" PACKET_TYPE_REJECT = "reject" PACKET_TYPE_MICE = "mic-e" -PACKET_TYPE_WX = "weather" +PACKET_TYPE_WX = "wx" +PACKET_TYPE_WEATHER = "weather" PACKET_TYPE_OBJECT = "object" PACKET_TYPE_UNKNOWN = "unknown" PACKET_TYPE_STATUS = "status" @@ -52,68 +51,65 @@ def _init_msgNo(): # noqa: N802 return c.value -def factory_from_dict(packet_dict): - pkt_type = get_packet_type(packet_dict) - if pkt_type: - cls = TYPE_LOOKUP[pkt_type] - return cls.from_dict(packet_dict) +def _translate_fields(raw: dict) -> dict: + translate_fields = { + "from": "from_call", + "to": "to_call", + } + # First translate some fields + for key in translate_fields: + if key in raw: + raw[translate_fields[key]] = raw[key] + del raw[key] + + # addresse overrides to_call + if "addresse" in raw: + raw["to_call"] = raw["addresse"] + + return raw -def factory_from_json(packet_dict): - pkt_type = get_packet_type(packet_dict) - if pkt_type: - return TYPE_LOOKUP[pkt_type].from_json(packet_dict) - - -@dataclass_json @dataclass(unsafe_hash=True) -class Packet(metaclass=abc.ABCMeta): - from_call: str = field(default=None) - to_call: str = field(default=None) - addresse: str = field(default=None) - format: str = field(default=None) +class Packet(DataClassJsonMixin): + _type: str = field(default="Packet", hash=False) + from_call: Optional[str] = field(default=None) + to_call: Optional[str] = field(default=None) + addresse: Optional[str] = field(default=None) + format: Optional[str] = field(default=None) msgNo: str = field(default_factory=_init_msgNo) # noqa: N815 - packet_type: str = field(default=None) + packet_type: Optional[str] = field(default=None) timestamp: float = field(default_factory=_init_timestamp, compare=False, hash=False) # Holds the raw text string to be sent over the wire # or holds the raw string from input packet - raw: str = field(default=None, compare=False, hash=False) + raw: Optional[str] = field(default=None, compare=False, hash=False) raw_dict: dict = field(repr=False, default_factory=lambda: {}, compare=False, hash=False) # Built by calling prepare(). raw needs this built first. - payload: str = field(default=None) + payload: Optional[str] = field(default=None) # Fields related to sending packets out send_count: int = field(repr=False, default=0, compare=False, hash=False) retry_count: int = field(repr=False, default=3, compare=False, hash=False) - # last_send_time: datetime = field( - # metadata=dc_json_config( - # encoder=datetime.isoformat, - # decoder=datetime.fromisoformat, - # ), - # repr=True, - # default_factory=_init_send_time, - # compare=False, - # hash=False - # ) last_send_time: float = field(repr=False, default=0, compare=False, hash=False) last_send_attempt: int = field(repr=False, default=0, compare=False, hash=False) # Do we allow this packet to be saved to send later? allow_delay: bool = field(repr=False, default=True, compare=False, hash=False) path: List[str] = field(default_factory=list, compare=False, hash=False) - via: str = field(default=None, compare=False, hash=False) - - def __post__init__(self): - LOG.warning(f"POST INIT {self}") + via: Optional[str] = field(default=None, compare=False, hash=False) @property - def json(self): - """ - get the json formated string - """ + def json(self) -> str: + """get the json formated string""" + # comes from the DataClassJsonMixin return self.to_json() - def get(self, key, default=None): + @property + def dict(self) -> dict: + """get the dict formated string""" + # comes from the DataClassJsonMixin + return self.to_dict() + + def get(self, key: str, default: Optional[str] = None): """Emulate a getter on a dict.""" if hasattr(self, key): return getattr(self, key) @@ -121,116 +117,37 @@ class Packet(metaclass=abc.ABCMeta): return default @property - def key(self): + def key(self) -> str: """Build a key for finding this packet in a dict.""" return f"{self.from_call}:{self.addresse}:{self.msgNo}" - def update_timestamp(self): + def update_timestamp(self) -> None: self.timestamp = _init_timestamp() - def prepare(self): + def prepare(self) -> None: """Do stuff here that is needed prior to sending over the air.""" # now build the raw message for sending self._build_payload() self._build_raw() - def _build_payload(self): + def _build_payload(self) -> None: """The payload is the non headers portion of the packet.""" + if not self.to_call: + raise ValueError("to_call isn't set. Must set to_call before calling prepare()") msg = self._filter_for_send().rstrip("\n") self.payload = ( f":{self.to_call.ljust(9)}" f":{msg}" ) - def _build_raw(self): + def _build_raw(self) -> None: """Build the self.raw which is what is sent over the air.""" self.raw = "{}>APZ100:{}".format( self.from_call, self.payload, ) - @staticmethod - def factory(raw_packet): - """Factory method to create a packet from a raw packet string.""" - raw = raw_packet - raw["raw_dict"] = raw.copy() - translate_fields = { - "from": "from_call", - "to": "to_call", - } - # First translate some fields - for key in translate_fields: - if key in raw: - raw[translate_fields[key]] = raw[key] - del raw[key] - - if "addresse" in raw: - raw["to_call"] = raw["addresse"] - - packet_type = get_packet_type(raw) - raw["packet_type"] = packet_type - class_name = TYPE_LOOKUP[packet_type] - if packet_type == PACKET_TYPE_THIRDPARTY: - # We have an encapsulated packet! - # So we need to decode it and return the inner packet - # as the packet we are going to process. - # This is a recursive call to the factory - subpacket_raw = raw["subpacket"] - subpacket = Packet.factory(subpacket_raw) - del raw["subpacket"] - # raw["subpacket"] = subpacket - packet = dacite.from_dict(data_class=class_name, data=raw) - packet.subpacket = subpacket - return packet - - if packet_type == PACKET_TYPE_UNKNOWN: - # Try and figure it out here - if "latitude" in raw: - class_name = GPSPacket - - if packet_type == PACKET_TYPE_WX: - # the weather information is in a dict - # this brings those values out to the outer dict - - for key in raw["weather"]: - raw[key] = raw["weather"][key] - - # If we have the broken aprslib, then we need to - # Convert the course and speed to wind_speed and wind_direction - # aprslib issue #80 - # https://github.com/rossengeorgiev/aprs-python/issues/80 - # Wind speed and course is option in the SPEC. - # For some reason aprslib multiplies the speed by 1.852. - if "wind_speed" not in raw and "wind_direction" not in raw: - # Most likely this is the broken aprslib - # So we need to convert the wind_gust speed - raw["wind_gust"] = round(raw.get("wind_gust", 0) / 0.44704, 3) - if "wind_speed" not in raw: - wind_speed = raw.get("speed") - if wind_speed: - raw["wind_speed"] = round(wind_speed / 1.852, 3) - raw["weather"]["wind_speed"] = raw["wind_speed"] - if "speed" in raw: - del raw["speed"] - # Let's adjust the rain numbers as well, since it's wrong - raw["rain_1h"] = round((raw.get("rain_1h", 0) / .254) * .01, 3) - raw["weather"]["rain_1h"] = raw["rain_1h"] - raw["rain_24h"] = round((raw.get("rain_24h", 0) / .254) * .01, 3) - raw["weather"]["rain_24h"] = raw["rain_24h"] - raw["rain_since_midnight"] = round((raw.get("rain_since_midnight", 0) / .254) * .01, 3) - raw["weather"]["rain_since_midnight"] = raw["rain_since_midnight"] - - if "wind_direction" not in raw: - wind_direction = raw.get("course") - if wind_direction: - raw["wind_direction"] = wind_direction - raw["weather"]["wind_direction"] = raw["wind_direction"] - if "course" in raw: - del raw["course"] - - return dacite.from_dict(data_class=class_name, data=raw) - - def log(self, header=None): + def log(self, header: Optional[str] = None) -> None: """LOG a packet to the logfile.""" asdict(self) log_list = ["\n"] @@ -273,29 +190,39 @@ class Packet(metaclass=abc.ABCMeta): # and ftm400-send is max 64. setting this to # 67 displays 64 on the ftm400. (+3 {01 suffix) # feature req: break long ones into two msgs + if not self.raw: + raise ValueError("No message text to send. call prepare() first.") + message = self.raw[:67] # We all miss George Carlin return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message) - def __str__(self): + def __str__(self) -> str: """Show the raw version of the packet""" self.prepare() + if not self.raw: + raise ValueError("self.raw is unset") return self.raw - def __repr__(self): + def __repr__(self) -> str: """Build the repr version of the packet.""" repr = ( f"{self.__class__.__name__}:" f" From: {self.from_call} " - " To: " + f" To: {self.to_call}" ) - return repr @dataclass(unsafe_hash=True) class AckPacket(Packet): - response: str = field(default=None) + _type: str = field(default="AckPacket", hash=False) + response: Optional[str] = field(default=None) + + @staticmethod + def from_aprslib_dict(raw: dict) -> "AckPacket": + raw = _translate_fields(raw) + return AckPacket(**raw) def __post__init__(self): if self.response: @@ -307,7 +234,13 @@ class AckPacket(Packet): @dataclass(unsafe_hash=True) class RejectPacket(Packet): - response: str = field(default=None) + _type: str = field(default="RejectPacket", hash=False) + response: Optional[str] = field(default=None) + + @staticmethod + def from_aprslib_dict(raw: dict) -> "RejectPacket": + raw = _translate_fields(raw) + return RejectPacket(**raw) def __post__init__(self): if self.response: @@ -317,10 +250,10 @@ class RejectPacket(Packet): self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}" -@dataclass_json @dataclass(unsafe_hash=True) class MessagePacket(Packet): - message_text: str = field(default=None) + _type: str = field(default="MessagePacket", hash=False) + message_text: Optional[str] = field(default=None) def _filter_for_send(self) -> str: """Filter and format message string for FCC.""" @@ -328,6 +261,9 @@ class MessagePacket(Packet): # and ftm400-send is max 64. setting this to # 67 displays 64 on the ftm400. (+3 {01 suffix) # feature req: break long ones into two msgs + if not self.message_text: + raise ValueError("No message text to send. Populate message_text field.") + message = self.message_text[:67] # We all miss George Carlin return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message) @@ -339,12 +275,23 @@ class MessagePacket(Packet): str(self.msgNo), ) + @staticmethod + def from_aprslib_dict(raw: dict) -> "MessagePacket": + raw = _translate_fields(raw) + return MessagePacket(**raw) + @dataclass(unsafe_hash=True) class StatusPacket(Packet): - status: str = field(default=None) + _type: str = field(default="StatusPacket", hash=False) + status: Optional[str] = field(default=None) messagecapable: bool = field(default=False) - comment: str = field(default=None) + comment: Optional[str] = field(default=None) + + @staticmethod + def from_aprslib_dict(raw: dict) -> "StatusPacket": + raw = _translate_fields(raw) + return StatusPacket(**raw) def _build_payload(self): raise NotImplementedError @@ -352,14 +299,27 @@ class StatusPacket(Packet): @dataclass(unsafe_hash=True) class GPSPacket(Packet): + _type: str = field(default="GPSPacket", hash=False) latitude: float = field(default=0.00) longitude: float = field(default=0.00) altitude: float = field(default=0.00) rng: float = field(default=0.00) posambiguity: int = field(default=0) - comment: str = field(default=None) + messagecapable: bool = field(default=False) + comment: Optional[str] = field(default=None) symbol: str = field(default="l") symbol_table: str = field(default="/") + raw_timestamp: Optional[str] = field(default=None) + object_name: Optional[str] = field(default=None) + object_format: Optional[str] = field(default=None) + alive: Optional[bool] = field(default=None) + course: Optional[int] = field(default=None) + speed: Optional[float] = field(default=None) + + @staticmethod + def from_aprslib_dict(raw: dict) -> "GPSPacket": + raw = _translate_fields(raw) + return GPSPacket(**raw) def decdeg2dms(self, degrees_decimal): is_positive = degrees_decimal >= 0 @@ -467,6 +427,13 @@ class GPSPacket(Packet): @dataclass(unsafe_hash=True) class BeaconPacket(GPSPacket): + _type: str = field(default="BeaconPacket", hash=False) + + @staticmethod + def from_aprslib_dict(raw: dict) -> "BeaconPacket": + raw = _translate_fields(raw) + return BeaconPacket(**raw) + def _build_payload(self): """The payload is the non headers portion of the packet.""" time_zulu = self._build_time_zulu() @@ -487,9 +454,10 @@ class BeaconPacket(GPSPacket): @dataclass class MicEPacket(GPSPacket): + _type: str = field(default="MicEPacket", hash=False) messagecapable: bool = False - mbits: str = None - mtype: str = None + mbits: Optional[str] = None + mtype: Optional[str] = None # in MPH speed: float = 0.00 # 0 to 360 @@ -501,14 +469,20 @@ class MicEPacket(GPSPacket): @dataclass class ObjectPacket(GPSPacket): + _type: str = field(default="ObjectPacket", hash=False) alive: bool = True - raw_timestamp: str = None + raw_timestamp: Optional[str] = None symbol: str = field(default="r") # in MPH speed: float = 0.00 # 0 to 360 course: int = 0 + @staticmethod + def from_aprslib_dict(raw: dict) -> "ObjectPacket": + raw = _translate_fields(raw) + return ObjectPacket(**raw) + def _build_payload(self): time_zulu = self._build_time_zulu() lat = self.convert_latitude(self.latitude) @@ -541,6 +515,7 @@ class ObjectPacket(GPSPacket): @dataclass() class WeatherPacket(GPSPacket): + _type: str = field(default="WeatherPacket", hash=False) symbol: str = "_" wind_speed: float = 0.00 wind_direction: int = 0 @@ -552,7 +527,57 @@ class WeatherPacket(GPSPacket): rain_since_midnight: float = 0.00 humidity: int = 0 pressure: float = 0.00 - comment: str = None + comment: Optional[str] = field(default=None) + luminosity: Optional[int] = field(default=None) + wx_raw_timestamp: Optional[str] = field(default=None) + course: Optional[int] = field(default=None) + speed: Optional[float] = field(default=None) + + @staticmethod + def from_aprslib_dict(raw: dict) -> "WeatherPacket": + """Create from a dictionary that has come directly from aprslib parse""" + # Because from is a reserved word in python, we need to translate it + # from -> from_call and to -> to_call + raw = _translate_fields(raw) + + for key in raw["weather"]: + raw[key] = raw["weather"][key] + + # If we have the broken aprslib, then we need to + # Convert the course and speed to wind_speed and wind_direction + # aprslib issue #80 + # https://github.com/rossengeorgiev/aprs-python/issues/80 + # Wind speed and course is option in the SPEC. + # For some reason aprslib multiplies the speed by 1.852. + if "wind_speed" not in raw and "wind_direction" not in raw: + # Most likely this is the broken aprslib + # So we need to convert the wind_gust speed + raw["wind_gust"] = round(raw.get("wind_gust", 0) / 0.44704, 3) + if "wind_speed" not in raw: + wind_speed = raw.get("speed") + if wind_speed: + raw["wind_speed"] = round(wind_speed / 1.852, 3) + raw["weather"]["wind_speed"] = raw["wind_speed"] + if "speed" in raw: + del raw["speed"] + # Let's adjust the rain numbers as well, since it's wrong + raw["rain_1h"] = round((raw.get("rain_1h", 0) / .254) * .01, 3) + raw["weather"]["rain_1h"] = raw["rain_1h"] + raw["rain_24h"] = round((raw.get("rain_24h", 0) / .254) * .01, 3) + raw["weather"]["rain_24h"] = raw["rain_24h"] + raw["rain_since_midnight"] = round((raw.get("rain_since_midnight", 0) / .254) * .01, 3) + raw["weather"]["rain_since_midnight"] = raw["rain_since_midnight"] + + if "wind_direction" not in raw: + wind_direction = raw.get("course") + if wind_direction: + raw["wind_direction"] = wind_direction + raw["weather"]["wind_direction"] = raw["wind_direction"] + if "course" in raw: + del raw["course"] + + del raw["weather"] + return WeatherPacket(**raw) def _build_payload(self): """Build an uncompressed weather packet @@ -614,9 +639,11 @@ class WeatherPacket(GPSPacket): ) -class ThirdParty(Packet): +@dataclass() +class ThirdPartyPacket(Packet): + _type: str = "ThirdPartyPacket" # Holds the encapsulated packet - subpacket: Packet = field(default=None, compare=True, hash=False) + subpacket: Optional[type[Packet]] = field(default=None, compare=True, hash=False) def __repr__(self): """Build the repr version of the packet.""" @@ -629,26 +656,50 @@ class ThirdParty(Packet): return repr_str + @staticmethod + def from_aprslib_dict(raw: dict) -> "ThirdPartyPacket": + """Create from a dictionary that has come directly from aprslib parse""" + # Because from is a reserved word in python, we need to translate it + # from -> from_call and to -> to_call + raw = _translate_fields(raw) + subpacket = raw.get("subpacket") + del raw["subpacket"] + return ThirdPartyPacket(**raw, subpacket=factory(subpacket)) -TYPE_LOOKUP = { + +TYPE_LOOKUP: dict[str, type[Packet]] = { PACKET_TYPE_WX: WeatherPacket, + PACKET_TYPE_WEATHER: WeatherPacket, PACKET_TYPE_MESSAGE: MessagePacket, PACKET_TYPE_ACK: AckPacket, PACKET_TYPE_REJECT: RejectPacket, PACKET_TYPE_MICE: MicEPacket, PACKET_TYPE_OBJECT: ObjectPacket, PACKET_TYPE_STATUS: StatusPacket, - PACKET_TYPE_BEACON: GPSPacket, + PACKET_TYPE_BEACON: BeaconPacket, PACKET_TYPE_UNKNOWN: Packet, - PACKET_TYPE_THIRDPARTY: ThirdParty, + PACKET_TYPE_THIRDPARTY: ThirdPartyPacket, +} + +OBJ_LOOKUP: dict[str, type[Packet]] = { + "MessagePacket": MessagePacket, + "AckPacket": AckPacket, + "RejectPacket": RejectPacket, + "MicEPacket": MicEPacket, + "ObjectPacket": ObjectPacket, + "StatusPacket": StatusPacket, + "GPSPacket": GPSPacket, + "BeaconPacket": BeaconPacket, + "WeatherPacket": WeatherPacket, + "ThirdPartyPacket": ThirdPartyPacket, } -def get_packet_type(packet: dict): +def get_packet_type(packet: dict) -> str: """Decode the packet type from the packet.""" - pkt_format = packet.get("format", None) - msg_response = packet.get("response", None) + pkt_format = packet.get("format") + msg_response = packet.get("response") packet_type = PACKET_TYPE_UNKNOWN if pkt_format == "message" and msg_response == "ack": packet_type = PACKET_TYPE_ACK @@ -664,9 +715,11 @@ def get_packet_type(packet: dict): packet_type = PACKET_TYPE_STATUS elif pkt_format == PACKET_TYPE_BEACON: packet_type = PACKET_TYPE_BEACON + elif pkt_format == PACKET_TYPE_WX: + packet_type = PACKET_TYPE_WEATHER elif pkt_format == PACKET_TYPE_UNCOMPRESSED: - if packet.get("symbol", None) == "_": - packet_type = PACKET_TYPE_WX + if packet.get("symbol") == "_": + packet_type = PACKET_TYPE_WEATHER elif pkt_format == PACKET_TYPE_THIRDPARTY: packet_type = PACKET_TYPE_THIRDPARTY @@ -676,13 +729,43 @@ def get_packet_type(packet: dict): return packet_type -def is_message_packet(packet): +def is_message_packet(packet: dict) -> bool: return get_packet_type(packet) == PACKET_TYPE_MESSAGE -def is_ack_packet(packet): +def is_ack_packet(packet: dict) -> bool: return get_packet_type(packet) == PACKET_TYPE_ACK -def is_mice_packet(packet): +def is_mice_packet(packet: dict) -> bool: return get_packet_type(packet) == PACKET_TYPE_MICE + + +def factory(raw_packet: dict) -> type[Packet]: + """Factory method to create a packet from a raw packet string.""" + raw = raw_packet + if "_type" in raw: + cls = globals()[raw["_type"]] + return cls.from_dict(raw) + + raw["raw_dict"] = raw.copy() + raw = _translate_fields(raw) + + packet_type = get_packet_type(raw) + + raw["packet_type"] = packet_type + class_name = TYPE_LOOKUP[packet_type] + if packet_type == PACKET_TYPE_WX: + # the weather information is in a dict + # this brings those values out to the outer dict + class_name = WeatherPacket + elif packet_type == PACKET_TYPE_OBJECT and "weather" in raw: + class_name = WeatherPacket + elif packet_type == PACKET_TYPE_UNKNOWN: + # Try and figure it out here + if "latitude" in raw: + class_name = GPSPacket + else: + raise Exception(f"Unknown packet type {packet_type} {raw}") + + return class_name.from_aprslib_dict(raw) diff --git a/requirements.in b/requirements.in index e5599fb..dcd231d 100644 --- a/requirements.in +++ b/requirements.in @@ -28,7 +28,6 @@ wrapt kiss3 attrs dataclasses -dacite2 oslo.config rpyc>=6.0.0 # Pin this here so it doesn't require a compile on diff --git a/requirements.txt b/requirements.txt index 6122800..1799187 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,6 @@ click==8.1.7 # via -r requirements.in, click-completion, click-para click-completion==0.5.2 # via -r requirements.in click-params==0.5.0 # via -r requirements.in commonmark==0.9.1 # via rich -dacite2==2.0.0 # via -r requirements.in dataclasses==0.6 # via -r requirements.in dataclasses-json==0.6.4 # via -r requirements.in debtcollector==3.0.0 # via oslo-config @@ -34,7 +33,7 @@ greenlet==3.0.3 # via eventlet, gevent h11==0.14.0 # via wsproto idna==3.6 # via requests imapclient==3.0.1 # via -r requirements.in -importlib-metadata==7.0.1 # via ax253, kiss3 +importlib-metadata==7.0.2 # via ax253, kiss3 itsdangerous==2.1.2 # via flask jinja2==3.1.3 # via click-completion, flask kiss3==8.0.0 # via -r requirements.in @@ -45,7 +44,7 @@ mypy-extensions==1.0.0 # via typing-inspect netaddr==1.2.1 # via oslo-config oslo-config==9.4.0 # via -r requirements.in oslo-i18n==6.3.0 # via oslo-config -packaging==23.2 # via marshmallow +packaging==24.0 # via marshmallow pbr==6.0.0 # via -r requirements.in, oslo-i18n, stevedore pluggy==1.4.0 # via -r requirements.in plumbum==1.8.2 # via rpyc @@ -76,7 +75,7 @@ validators==0.22.0 # via click-params werkzeug==3.0.1 # via -r requirements.in, flask wrapt==1.16.0 # via -r requirements.in, debtcollector, deprecated wsproto==1.2.0 # via simple-websocket -zipp==3.17.0 # via importlib-metadata +zipp==3.18.1 # via importlib-metadata zope-event==5.0 # via gevent zope-interface==6.2 # via gevent diff --git a/tests/cmds/test_webchat.py b/tests/cmds/test_webchat.py index 88e38e1..8280899 100644 --- a/tests/cmds/test_webchat.py +++ b/tests/cmds/test_webchat.py @@ -51,11 +51,8 @@ class TestSendMessageCommand(unittest.TestCase): ): self.config_and_init() mock_socketio.emit = mock.MagicMock() - packet = fake.fake_packet( - message="blah", - msg_number=1, - message_format=core.PACKET_TYPE_ACK, - ) + # Create an ACK packet + packet = fake.fake_ack_packet() mock_queue = mock.MagicMock() socketio = mock.MagicMock() wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio) diff --git a/tests/fake.py b/tests/fake.py index 1912157..4a7c816 100644 --- a/tests/fake.py +++ b/tests/fake.py @@ -1,4 +1,4 @@ -from aprsd import packets, plugin, threads +from aprsd import plugin, threads from aprsd.packets import core @@ -13,6 +13,7 @@ def fake_packet( message=None, msg_number=None, message_format=core.PACKET_TYPE_MESSAGE, + response=None, ): packet_dict = { "from": fromcall, @@ -27,7 +28,17 @@ def fake_packet( if msg_number: packet_dict["msgNo"] = str(msg_number) - return packets.Packet.factory(packet_dict) + if response: + packet_dict["response"] = response + + return core.factory(packet_dict) + + +def fake_ack_packet(): + return fake_packet( + msg_number=12, + response=core.PACKET_TYPE_ACK, + ) class FakeBaseNoThreadsPlugin(plugin.APRSDPluginBase): diff --git a/tests/plugins/test_weather.py b/tests/plugins/test_weather.py index 8a85e0b..c8d814d 100644 --- a/tests/plugins/test_weather.py +++ b/tests/plugins/test_weather.py @@ -11,7 +11,7 @@ from .. import fake, test_plugin CONF = cfg.CONF -class TestUSWeatherPluginPlugin(test_plugin.TestPlugin): +class TestUSWeatherPlugin(test_plugin.TestPlugin): def test_not_enabled_missing_aprs_fi_key(self): # When the aprs.fi api key isn't set, then diff --git a/tests/test_packets.py b/tests/test_packets.py index 7b8fdca..15f1433 100644 --- a/tests/test_packets.py +++ b/tests/test_packets.py @@ -1,6 +1,8 @@ import unittest from unittest import mock +import aprslib + from aprsd import packets from aprsd.packets import core @@ -55,7 +57,7 @@ class TestPluginBase(unittest.TestCase): def test_packet_factory(self): pkt_dict = self._fake_dict() - pkt = packets.Packet.factory(pkt_dict) + pkt = packets.factory(pkt_dict) self.assertIsInstance(pkt, packets.MessagePacket) self.assertEqual(fake.FAKE_FROM_CALLSIGN, pkt.from_call) @@ -71,7 +73,7 @@ class TestPluginBase(unittest.TestCase): "comment": "Home!", } pkt_dict["format"] = core.PACKET_TYPE_UNCOMPRESSED - pkt = packets.Packet.factory(pkt_dict) + pkt = packets.factory(pkt_dict) self.assertIsInstance(pkt, packets.WeatherPacket) @mock.patch("aprsd.packets.core.GPSPacket._build_time_zulu") @@ -100,3 +102,31 @@ class TestPluginBase(unittest.TestCase): wx.prepare() expected = "KFAKE>KMINE,WIDE1-1,WIDE2-1:@221450z0.0/0.0_000/000g000t000r001p000P000h00b00000" self.assertEqual(expected, wx.raw) + + def test_beacon_factory(self): + """Test to ensure a beacon packet is created.""" + packet_raw = "WB4BOR-12>APZ100,WIDE2-1:@161647z3724.15N107847.58W$ APRSD WebChat" + packet_dict = aprslib.parse(packet_raw) + packet = packets.factory(packet_dict) + self.assertIsInstance(packet, packets.BeaconPacket) + + def test_reject_factory(self): + """Test to ensure a reject packet is created.""" + packet_raw = "HB9FDL-1>APK102,HB9FM-4*,WIDE2,qAR,HB9FEF-11::REPEAT :rej4139" + packet_dict = aprslib.parse(packet_raw) + packet = packets.factory(packet_dict) + self.assertIsInstance(packet, packets.RejectPacket) + + def test_thirdparty_factory(self): + """Test to ensure a third party packet is created.""" + packet_raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW" + packet_dict = aprslib.parse(packet_raw) + packet = packets.factory(packet_dict) + self.assertIsInstance(packet, packets.ThirdPartyPacket) + + def test_weather_factory(self): + """Test to ensure a weather packet is created.""" + packet_raw = "FW9222>APRS,TCPXX*,qAX,CWOP-6:@122025z2953.94N/08423.77W_232/003g006t084r000p032P000h80b10157L745.DsWLL" + packet_dict = aprslib.parse(packet_raw) + packet = packets.factory(packet_dict) + self.assertIsInstance(packet, packets.WeatherPacket) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index e69ede3..e9a17eb 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -45,7 +45,6 @@ class TestPluginManager(unittest.TestCase): self.assertEqual([], plugin_list) pm.setup_plugins() plugin_list = pm.get_plugins() - print(plugin_list) self.assertIsInstance(plugin_list, list) self.assertIsInstance( plugin_list[0], @@ -163,9 +162,7 @@ class TestPluginBase(TestPlugin): self.assertEqual(expected, actual) mock_process.assert_not_called() - packet = fake.fake_packet( - message_format=core.PACKET_TYPE_ACK, - ) + packet = fake.fake_ack_packet() expected = packets.NULL_MESSAGE actual = p.filter(packet) self.assertEqual(expected, actual)