mirror of
https://github.com/craigerl/aprsd.git
synced 2026-04-08 16:15:46 -04:00
Update WatchList and NotifySeenPlugin
The watchList was updating the last seen during RX time. This happens before the NotifySeenPlugin even sees the packet, so the callsign is never 'old'. this patch fixes that, so the watch list works.
This commit is contained in:
parent
a4b13c0c53
commit
9ac881c56c
@ -34,6 +34,12 @@ log_options = [
|
||||
is_flag=True,
|
||||
help='Show log level in log format (disabled by default for listen).',
|
||||
),
|
||||
click.option(
|
||||
'--show-location',
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help='Show location in log format (disabled by default for listen).',
|
||||
),
|
||||
]
|
||||
|
||||
common_options = [
|
||||
@ -124,9 +130,9 @@ def add_options(options):
|
||||
def setup_logging_with_options(
|
||||
show_thread: bool,
|
||||
show_level: bool,
|
||||
show_location: bool,
|
||||
loglevel: str,
|
||||
quiet: bool,
|
||||
include_location: bool = True,
|
||||
) -> None:
|
||||
"""Setup logging with custom format based on show_thread and show_level options.
|
||||
|
||||
@ -153,7 +159,7 @@ def setup_logging_with_options(
|
||||
# Message is always included
|
||||
parts.append(conf_log.DEFAULT_LOG_FORMAT_MESSAGE)
|
||||
|
||||
if include_location:
|
||||
if show_location:
|
||||
parts.append(conf_log.DEFAULT_LOG_FORMAT_LOCATION)
|
||||
|
||||
# Set the custom log format
|
||||
@ -172,6 +178,7 @@ def process_standard_options(f: F) -> F:
|
||||
# Extract show_thread and show_level
|
||||
show_thread = kwargs.get('show_thread', False)
|
||||
show_level = kwargs.get('show_level', False)
|
||||
show_location = kwargs.get('show_location', False)
|
||||
|
||||
if kwargs['config_file']:
|
||||
default_config_files = [kwargs['config_file']]
|
||||
@ -196,9 +203,9 @@ def process_standard_options(f: F) -> F:
|
||||
setup_logging_with_options(
|
||||
show_thread=show_thread,
|
||||
show_level=show_level,
|
||||
show_location=show_location,
|
||||
loglevel=ctx.obj['loglevel'],
|
||||
quiet=ctx.obj['quiet'],
|
||||
include_location=True,
|
||||
)
|
||||
if CONF.trace_enabled:
|
||||
trace.setup_tracing(['method', 'api'])
|
||||
@ -213,6 +220,7 @@ def process_standard_options(f: F) -> F:
|
||||
del kwargs['quiet']
|
||||
del kwargs['show_thread']
|
||||
del kwargs['show_level']
|
||||
del kwargs['show_location']
|
||||
|
||||
# Enable profiling if requested
|
||||
if profile_output is not None:
|
||||
@ -247,6 +255,7 @@ def process_standard_options_no_config(f: F) -> F:
|
||||
# Extract show_thread and show_level
|
||||
show_thread = kwargs.get('show_thread', False)
|
||||
show_level = kwargs.get('show_level', False)
|
||||
show_location = kwargs.get('show_location', False)
|
||||
|
||||
# Initialize CONF without config file for log format access
|
||||
try:
|
||||
@ -268,9 +277,9 @@ def process_standard_options_no_config(f: F) -> F:
|
||||
setup_logging_with_options(
|
||||
show_thread=show_thread,
|
||||
show_level=show_level,
|
||||
show_location=show_location,
|
||||
loglevel=ctx.obj['loglevel'],
|
||||
quiet=ctx.obj['quiet'],
|
||||
include_location=True,
|
||||
)
|
||||
|
||||
profile_output = kwargs.pop('profile_output', None)
|
||||
@ -279,6 +288,7 @@ def process_standard_options_no_config(f: F) -> F:
|
||||
del kwargs['quiet']
|
||||
del kwargs['show_thread']
|
||||
del kwargs['show_level']
|
||||
del kwargs['show_location']
|
||||
|
||||
# Enable profiling if requested
|
||||
if profile_output is not None:
|
||||
|
||||
@ -5,11 +5,10 @@ from oslo_config import cfg
|
||||
|
||||
from aprsd import utils
|
||||
from aprsd.packets import core
|
||||
from aprsd.utils import objectstore
|
||||
|
||||
from aprsd.utils import objectstore, trace
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
LOG = logging.getLogger('APRSD')
|
||||
|
||||
|
||||
class WatchList(objectstore.ObjectStoreMixin):
|
||||
@ -17,40 +16,47 @@ class WatchList(objectstore.ObjectStoreMixin):
|
||||
|
||||
_instance = None
|
||||
data = {}
|
||||
initialized = False
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
@trace.no_trace
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._update_from_conf()
|
||||
if not self.initialized:
|
||||
self._update_from_conf()
|
||||
self.initialized = True
|
||||
|
||||
def _update_from_conf(self, config=None):
|
||||
with self.lock:
|
||||
if CONF.watch_list.enabled and CONF.watch_list.callsigns:
|
||||
for callsign in CONF.watch_list.callsigns:
|
||||
call = callsign.replace("*", "")
|
||||
call = callsign.replace('*', '')
|
||||
# FIXME(waboring) - we should fetch the last time we saw
|
||||
# a beacon from a callsign or some other mechanism to find
|
||||
# last time a message was seen by aprs-is. For now this
|
||||
# is all we can do.
|
||||
if call not in self.data:
|
||||
self.data[call] = {
|
||||
"last": None,
|
||||
"packet": None,
|
||||
'last': None,
|
||||
'packet': None,
|
||||
'was_old_before_update': False,
|
||||
}
|
||||
|
||||
@trace.no_trace
|
||||
def stats(self, serializable=False) -> dict:
|
||||
stats = {}
|
||||
return self.data
|
||||
with self.lock:
|
||||
for callsign in self.data:
|
||||
stats[callsign] = {
|
||||
"last": self.data[callsign]["last"],
|
||||
"packet": self.data[callsign]["packet"],
|
||||
"age": self.age(callsign),
|
||||
"old": self.is_old(callsign),
|
||||
'last': self.data[callsign]['last'],
|
||||
'packet': self.data[callsign]['packet'],
|
||||
'age': self.age(callsign),
|
||||
'old': self.is_old(callsign),
|
||||
}
|
||||
return stats
|
||||
|
||||
@ -67,8 +73,25 @@ class WatchList(objectstore.ObjectStoreMixin):
|
||||
|
||||
if self.callsign_in_watchlist(callsign):
|
||||
with self.lock:
|
||||
self.data[callsign]["last"] = datetime.datetime.now()
|
||||
self.data[callsign]["packet"] = packet
|
||||
# Check if callsign was old BEFORE updating the timestamp
|
||||
# This allows plugins to check if it was old before this update
|
||||
# We check directly here to avoid nested locking
|
||||
was_old = False
|
||||
last_seen_time = self.data[callsign].get('last')
|
||||
if last_seen_time:
|
||||
age_delta = datetime.datetime.now() - last_seen_time
|
||||
max_delta = self.max_delta()
|
||||
if age_delta > max_delta:
|
||||
was_old = True
|
||||
|
||||
# Now update the timestamp and packet
|
||||
if last_seen_time:
|
||||
self.data[callsign]['age'] = str(
|
||||
datetime.datetime.now() - last_seen_time
|
||||
)
|
||||
self.data[callsign]['last'] = datetime.datetime.now()
|
||||
self.data[callsign]['packet'] = packet
|
||||
self.data[callsign]['was_old_before_update'] = was_old
|
||||
|
||||
def tx(self, packet: type[core.Packet]) -> None:
|
||||
"""We don't care about TX packets."""
|
||||
@ -76,7 +99,7 @@ class WatchList(objectstore.ObjectStoreMixin):
|
||||
def last_seen(self, callsign):
|
||||
with self.lock:
|
||||
if self.callsign_in_watchlist(callsign):
|
||||
return self.data[callsign]["last"]
|
||||
return self.data[callsign]['last']
|
||||
|
||||
def age(self, callsign):
|
||||
now = datetime.datetime.now()
|
||||
@ -89,9 +112,32 @@ class WatchList(objectstore.ObjectStoreMixin):
|
||||
def max_delta(self, seconds=None):
|
||||
if not seconds:
|
||||
seconds = CONF.watch_list.alert_time_seconds
|
||||
max_timeout = {"seconds": seconds}
|
||||
max_timeout = {'seconds': seconds}
|
||||
return datetime.timedelta(**max_timeout)
|
||||
|
||||
def was_old_before_last_update(self, callsign):
|
||||
"""Check if the callsign was old before the last rx() update.
|
||||
|
||||
This is useful for plugins that need to know if a callsign was
|
||||
old before the current packet updated the timestamp. This allows
|
||||
plugins to determine if they should notify about a callsign that
|
||||
was previously old but is now being seen again.
|
||||
"""
|
||||
with self.lock:
|
||||
if not self.callsign_in_watchlist(callsign):
|
||||
return False
|
||||
return self.data[callsign].get('was_old_before_update', False)
|
||||
|
||||
def mark_as_new(self, callsign):
|
||||
"""Mark a callsign as new, resetting the was_old_before_update flag.
|
||||
|
||||
This is useful after sending a notification to prevent duplicate
|
||||
notifications until the callsign becomes old again.
|
||||
"""
|
||||
with self.lock:
|
||||
if self.callsign_in_watchlist(callsign):
|
||||
self.data[callsign]['was_old_before_update'] = False
|
||||
|
||||
def is_old(self, callsign, seconds=None):
|
||||
"""Watch list callsign last seen is old compared to now?
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ from oslo_config import cfg
|
||||
from aprsd import packets, plugin
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
LOG = logging.getLogger('APRSD')
|
||||
|
||||
|
||||
class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
||||
@ -17,10 +17,10 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
||||
seen was older than the configured age limit.
|
||||
"""
|
||||
|
||||
short_description = "Notify me when a CALLSIGN is recently seen on APRS-IS"
|
||||
short_description = 'Notify me when a CALLSIGN is recently seen on APRS-IS'
|
||||
|
||||
def process(self, packet: packets.MessagePacket):
|
||||
LOG.info("NotifySeenPlugin")
|
||||
LOG.info('NotifySeenPlugin')
|
||||
|
||||
notify_callsign = CONF.watch_list.alert_callsign
|
||||
fromcall = packet.from_call
|
||||
@ -29,14 +29,19 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
||||
age = wl.age(fromcall)
|
||||
|
||||
if fromcall != notify_callsign:
|
||||
if wl.is_old(fromcall):
|
||||
# Check if the callsign was old BEFORE WatchList.rx() updated the timestamp
|
||||
# This ensures we can detect when a previously old callsign is seen again
|
||||
if wl.was_old_before_last_update(fromcall):
|
||||
LOG.info(
|
||||
"NOTIFY {} last seen {} max age={}".format(
|
||||
'NOTIFY {} last seen {} max age={}'.format(
|
||||
fromcall,
|
||||
age,
|
||||
wl.max_delta(),
|
||||
),
|
||||
)
|
||||
# Mark the callsign as new to prevent duplicate notifications
|
||||
# until it becomes old again
|
||||
wl.mark_as_new(fromcall)
|
||||
packet_type = packet.__class__.__name__
|
||||
# we shouldn't notify the alert user that they are online.
|
||||
pkt = packets.MessagePacket(
|
||||
@ -49,10 +54,10 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
||||
return pkt
|
||||
else:
|
||||
LOG.debug(
|
||||
"Not old enough to notify on callsign "
|
||||
'Not old enough to notify on callsign '
|
||||
f"'{fromcall}' : {age} < {wl.max_delta()}",
|
||||
)
|
||||
return packets.NULL_MESSAGE
|
||||
else:
|
||||
LOG.debug("fromcall and notify_callsign are the same, ignoring")
|
||||
LOG.debug('fromcall and notify_callsign are the same, ignoring')
|
||||
return packets.NULL_MESSAGE
|
||||
|
||||
@ -140,9 +140,7 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
|
||||
expected = packets.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch('aprsd.packets.WatchList.is_old')
|
||||
def test_callsign_in_watchlist_not_old(self, mock_is_old):
|
||||
mock_is_old.return_value = False
|
||||
def test_callsign_in_watchlist_not_old(self):
|
||||
self.config_and_init(
|
||||
watchlist_enabled=True,
|
||||
watchlist_callsigns=['WB4BOR'],
|
||||
@ -154,46 +152,77 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
|
||||
message='ping',
|
||||
msg_number=1,
|
||||
)
|
||||
# Simulate WatchList.rx() being called first (with recent timestamp)
|
||||
# This will set was_old_before_update to False since it's not old
|
||||
packets.WatchList().rx(packet)
|
||||
actual = plugin.filter(packet)
|
||||
expected = packets.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch('aprsd.packets.WatchList.is_old')
|
||||
def test_callsign_in_watchlist_old_same_alert_callsign(self, mock_is_old):
|
||||
mock_is_old.return_value = True
|
||||
def test_callsign_in_watchlist_old_same_alert_callsign(self):
|
||||
import datetime
|
||||
|
||||
self.config_and_init(
|
||||
watchlist_enabled=True,
|
||||
watchlist_alert_callsign='WB4BOR',
|
||||
watchlist_callsigns=['WB4BOR'],
|
||||
watchlist_alert_time_seconds=60,
|
||||
)
|
||||
plugin = notify_plugin.NotifySeenPlugin()
|
||||
|
||||
# Set up WatchList with an old timestamp
|
||||
wl = packets.WatchList()
|
||||
old_time = datetime.datetime.now() - datetime.timedelta(seconds=120)
|
||||
with wl.lock:
|
||||
wl.data['WB4BOR'] = {
|
||||
'last': old_time,
|
||||
'packet': None,
|
||||
'was_old_before_update': False,
|
||||
}
|
||||
|
||||
packet = fake.fake_packet(
|
||||
fromcall='WB4BOR',
|
||||
message='ping',
|
||||
msg_number=1,
|
||||
)
|
||||
# Simulate WatchList.rx() being called first
|
||||
# This will set was_old_before_update to True since it was old
|
||||
wl.rx(packet)
|
||||
actual = plugin.filter(packet)
|
||||
expected = packets.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch('aprsd.packets.WatchList.is_old')
|
||||
def test_callsign_in_watchlist_old_send_alert(self, mock_is_old):
|
||||
mock_is_old.return_value = True
|
||||
def test_callsign_in_watchlist_old_send_alert(self):
|
||||
import datetime
|
||||
|
||||
notify_callsign = fake.FAKE_TO_CALLSIGN
|
||||
fromcall = 'WB4BOR'
|
||||
self.config_and_init(
|
||||
watchlist_enabled=True,
|
||||
watchlist_alert_callsign=notify_callsign,
|
||||
watchlist_callsigns=['WB4BOR'],
|
||||
watchlist_alert_time_seconds=60,
|
||||
)
|
||||
plugin = notify_plugin.NotifySeenPlugin()
|
||||
|
||||
# Set up WatchList with an old timestamp
|
||||
wl = packets.WatchList()
|
||||
old_time = datetime.datetime.now() - datetime.timedelta(seconds=120)
|
||||
with wl.lock:
|
||||
wl.data[fromcall] = {
|
||||
'last': old_time,
|
||||
'packet': None,
|
||||
'was_old_before_update': False,
|
||||
}
|
||||
|
||||
packet = fake.fake_packet(
|
||||
fromcall=fromcall,
|
||||
message='ping',
|
||||
msg_number=1,
|
||||
)
|
||||
# Simulate WatchList.rx() being called first
|
||||
# This will set was_old_before_update to True since it was old
|
||||
wl.rx(packet)
|
||||
packet_type = packet.__class__.__name__
|
||||
actual = plugin.filter(packet)
|
||||
msg = f"{fromcall} was just seen by type:'{packet_type}'"
|
||||
@ -202,3 +231,6 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
|
||||
self.assertEqual(fake.FAKE_FROM_CALLSIGN, actual.from_call)
|
||||
self.assertEqual(notify_callsign, actual.to_call)
|
||||
self.assertEqual(msg, actual.message_text)
|
||||
# Verify that mark_as_new was called to prevent duplicate notifications
|
||||
# by checking that was_old_before_update is now False
|
||||
self.assertFalse(wl.was_old_before_last_update(fromcall))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user