mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2025-10-27 08:49:27 +00:00
Allow adding spots. Convert timestamps in the API to UNIX seconds. #2
This commit is contained in:
@@ -150,9 +150,7 @@ def infer_mode_from_frequency(freq):
|
|||||||
|
|
||||||
|
|
||||||
# Convert objects to serialisable things. Used by JSON serialiser as a default when it encounters unserializable things.
|
# Convert objects to serialisable things. Used by JSON serialiser as a default when it encounters unserializable things.
|
||||||
# Converts datetimes to ISO.
|
# Just converts objects to dict. Try to avoid doing anything clever here when serialising spots, because we also need
|
||||||
# Anything else it tries to convert to a dict.
|
# to receive spots without complex handling.
|
||||||
def serialize_everything(obj):
|
def serialize_everything(obj):
|
||||||
if isinstance(obj, datetime):
|
|
||||||
return obj.isoformat()
|
|
||||||
return obj.__dict__
|
return obj.__dict__
|
||||||
31
data/spot.py
31
data/spot.py
@@ -57,12 +57,16 @@ class Spot:
|
|||||||
freq: float = None
|
freq: float = None
|
||||||
# Band, defined by the frequency, e.g. "40m" or "70cm"
|
# Band, defined by the frequency, e.g. "40m" or "70cm"
|
||||||
band: str = None
|
band: str = None
|
||||||
# Time of the spot
|
# Time of the spot, UTC seconds since UNIX epoch
|
||||||
time: datetime = None
|
time: float = None
|
||||||
# Time that this software received the spot. This is used with the "since_received" call to our API to receive all
|
# Time of the spot, ISO 8601
|
||||||
# data that is new to us, even if by a quirk of the API it might be older than the list time the client polled the
|
time_iso: str = None
|
||||||
# API.
|
# Time that this software received the spot, UTC seconds since UNIX epoch. This is used with the "since_received"
|
||||||
received_time: datetime = datetime.now(pytz.UTC)
|
# call to our API to receive all data that is new to us, even if by a quirk of the API it might be older than the
|
||||||
|
# list time the client polled the API.
|
||||||
|
received_time: float = None
|
||||||
|
# Time that this software received the spot, ISO 8601
|
||||||
|
received_time_iso: str = None
|
||||||
# Comment left by the spotter, if any
|
# Comment left by the spotter, if any
|
||||||
comment: str = None
|
comment: str = None
|
||||||
# Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
|
# Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
|
||||||
@@ -96,9 +100,20 @@ class Spot:
|
|||||||
# Always create a GUID
|
# Always create a GUID
|
||||||
self.guid = str(uuid.uuid4())
|
self.guid = str(uuid.uuid4())
|
||||||
|
|
||||||
# If we somehow don't have a time, set it to some far past value so it sorts at the bottom of the list
|
# If we somehow don't have a spot time, set it to zero so it sorts off the bottom of any list but
|
||||||
|
# clients can still reliably parse it as a number.
|
||||||
if not self.time:
|
if not self.time:
|
||||||
self.time = datetime.min
|
self.time = 0
|
||||||
|
|
||||||
|
# If we don't have a received time, this has just been received so set that to "now"
|
||||||
|
if not self.received_time:
|
||||||
|
self.received_time = datetime.now(pytz.UTC).timestamp()
|
||||||
|
|
||||||
|
# Fill in ISO versions of times, in case the client prefers that
|
||||||
|
if self.time and not self.time_iso:
|
||||||
|
self.time_iso = datetime.fromtimestamp(self.time, pytz.UTC).isoformat()
|
||||||
|
if self.received_time and not self.received_time_iso:
|
||||||
|
self.received_time_iso = datetime.fromtimestamp(self.received_time, pytz.UTC).isoformat()
|
||||||
|
|
||||||
# Clean up DX call if it has an SSID or -# from RBN
|
# Clean up DX call if it has an SSID or -# from RBN
|
||||||
if self.dx_call and "-" in self.dx_call:
|
if self.dx_call and "-" in self.dx_call:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class APRSIS(Provider):
|
|||||||
latitude=data["latitude"] if "latitude" in data else None,
|
latitude=data["latitude"] if "latitude" in data else None,
|
||||||
longitude=data["longitude"] if "longitude" in data else None,
|
longitude=data["longitude"] if "longitude" in data else None,
|
||||||
icon="tower-cell",
|
icon="tower-cell",
|
||||||
time=datetime.now(pytz.UTC)) # APRS-IS spots are live so we can assume spot time is "now"
|
time=datetime.now(pytz.UTC).timestamp()) # APRS-IS spots are live so we can assume spot time is "now"
|
||||||
|
|
||||||
# Add to our list
|
# Add to our list
|
||||||
self.submit(spot)
|
self.submit(spot)
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class DXCluster(Provider):
|
|||||||
freq=float(match.group(2)) * 1000,
|
freq=float(match.group(2)) * 1000,
|
||||||
comment=match.group(4).strip(),
|
comment=match.group(4).strip(),
|
||||||
icon="desktop",
|
icon="desktop",
|
||||||
time=spot_datetime)
|
time=spot_datetime.timestamp())
|
||||||
|
|
||||||
# Add to our list
|
# Add to our list
|
||||||
self.submit(spot)
|
self.submit(spot)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class GMA(HTTPProvider):
|
|||||||
sig_refs=[source_spot["REF"]],
|
sig_refs=[source_spot["REF"]],
|
||||||
sig_refs_names=[source_spot["NAME"]],
|
sig_refs_names=[source_spot["NAME"]],
|
||||||
time=datetime.strptime(source_spot["DATE"] + source_spot["TIME"], "%Y%m%d%H%M").replace(
|
time=datetime.strptime(source_spot["DATE"] + source_spot["TIME"], "%Y%m%d%H%M").replace(
|
||||||
tzinfo=pytz.UTC),
|
tzinfo=pytz.UTC).timestamp(),
|
||||||
latitude=float(source_spot["LAT"]) if (source_spot["LAT"] != "") else None,
|
latitude=float(source_spot["LAT"]) if (source_spot["LAT"] != "") else None,
|
||||||
# Seen GMA spots with no lat/lon
|
# Seen GMA spots with no lat/lon
|
||||||
longitude=float(source_spot["LON"]) if (source_spot["LON"] != "") else None)
|
longitude=float(source_spot["LON"]) if (source_spot["LON"] != "") else None)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class HEMA(HTTPProvider):
|
|||||||
sig_refs=[spot_items[3].upper()],
|
sig_refs=[spot_items[3].upper()],
|
||||||
sig_refs_names=[spot_items[4]],
|
sig_refs_names=[spot_items[4]],
|
||||||
icon="mound",
|
icon="mound",
|
||||||
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M").replace(tzinfo=pytz.UTC),
|
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M").replace(tzinfo=pytz.UTC).timestamp(),
|
||||||
latitude=float(spot_items[7]),
|
latitude=float(spot_items[7]),
|
||||||
longitude=float(spot_items[8]))
|
longitude=float(spot_items[8]))
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ParksNPeaks(HTTPProvider):
|
|||||||
sig=source_spot["actClass"],
|
sig=source_spot["actClass"],
|
||||||
sig_refs=[source_spot["actSiteID"]],
|
sig_refs=[source_spot["actSiteID"]],
|
||||||
icon="question", # todo determine from actClass
|
icon="question", # todo determine from actClass
|
||||||
time=datetime.strptime(source_spot["actTime"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC))
|
time=datetime.strptime(source_spot["actTime"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC).timestamp())
|
||||||
|
|
||||||
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore.
|
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore.
|
||||||
if spot.sig not in ["POTA", "SOTA", "WWFF"]:
|
if spot.sig not in ["POTA", "SOTA", "WWFF"]:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class POTA(HTTPProvider):
|
|||||||
sig_refs=[source_spot["reference"]],
|
sig_refs=[source_spot["reference"]],
|
||||||
sig_refs_names=[source_spot["name"]],
|
sig_refs_names=[source_spot["name"]],
|
||||||
icon="tree",
|
icon="tree",
|
||||||
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=pytz.UTC),
|
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=pytz.UTC).timestamp(),
|
||||||
grid=source_spot["grid6"],
|
grid=source_spot["grid6"],
|
||||||
latitude=source_spot["latitude"],
|
latitude=source_spot["latitude"],
|
||||||
longitude=source_spot["longitude"])
|
longitude=source_spot["longitude"])
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ class Provider:
|
|||||||
# subclasses on receiving spots.
|
# subclasses on receiving spots.
|
||||||
def submit_batch(self, spots):
|
def submit_batch(self, spots):
|
||||||
for spot in spots:
|
for spot in spots:
|
||||||
if spot.time > self.last_spot_time:
|
if datetime.fromtimestamp(spot.time, pytz.UTC) > self.last_spot_time:
|
||||||
# Fill in any blanks
|
# Fill in any blanks
|
||||||
spot.infer_missing()
|
spot.infer_missing()
|
||||||
# Add to the list
|
# Add to the list
|
||||||
self.spots.add(spot.guid, spot, expire=MAX_SPOT_AGE)
|
self.spots.add(spot.guid, spot, expire=MAX_SPOT_AGE)
|
||||||
self.last_spot_time = max(map(lambda s: s.time, spots))
|
self.last_spot_time = datetime.fromtimestamp(max(map(lambda s: s.time, spots)), pytz.UTC)
|
||||||
|
|
||||||
# Submit a single spot retrieved from the provider. This will be added to the list regardless of its age. Spots
|
# Submit a single spot retrieved from the provider. This will be added to the list regardless of its age. Spots
|
||||||
# passing the check will also have their infer_missing() method called to complete their data set. This is called by
|
# passing the check will also have their infer_missing() method called to complete their data set. This is called by
|
||||||
@@ -50,7 +50,7 @@ class Provider:
|
|||||||
spot.infer_missing()
|
spot.infer_missing()
|
||||||
# Add to the list
|
# Add to the list
|
||||||
self.spots.add(spot.guid, spot, expire=MAX_SPOT_AGE)
|
self.spots.add(spot.guid, spot, expire=MAX_SPOT_AGE)
|
||||||
self.last_spot_time = spot.time
|
self.last_spot_time = datetime.fromtimestamp(spot.time, pytz.UTC)
|
||||||
|
|
||||||
# Stop any threads and prepare for application shutdown
|
# Stop any threads and prepare for application shutdown
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class RBN(Provider):
|
|||||||
freq=float(match.group(2)) * 1000,
|
freq=float(match.group(2)) * 1000,
|
||||||
comment=match.group(4).strip(),
|
comment=match.group(4).strip(),
|
||||||
icon="tower-cell",
|
icon="tower-cell",
|
||||||
time=spot_datetime)
|
time=spot_datetime.timestamp())
|
||||||
|
|
||||||
# Add to our list
|
# Add to our list
|
||||||
self.submit(spot)
|
self.submit(spot)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class SOTA(HTTPProvider):
|
|||||||
sig_refs=[source_spot["summitCode"]],
|
sig_refs=[source_spot["summitCode"]],
|
||||||
sig_refs_names=[source_spot["summitName"]],
|
sig_refs_names=[source_spot["summitName"]],
|
||||||
icon="mountain-sun",
|
icon="mountain-sun",
|
||||||
time=datetime.fromisoformat(source_spot["timeStamp"]),
|
time=datetime.fromisoformat(source_spot["timeStamp"]).timestamp(),
|
||||||
activation_score=source_spot["points"])
|
activation_score=source_spot["points"])
|
||||||
|
|
||||||
# SOTA doesn't give summit lat/lon/grid in the main call, so we need another separate call for this
|
# SOTA doesn't give summit lat/lon/grid in the main call, so we need another separate call for this
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class WWBOTA(HTTPProvider):
|
|||||||
sig_refs=refs,
|
sig_refs=refs,
|
||||||
sig_refs_names=ref_names,
|
sig_refs_names=ref_names,
|
||||||
icon="radiation",
|
icon="radiation",
|
||||||
time=datetime.fromisoformat(source_spot["time"]),
|
time=datetime.fromisoformat(source_spot["time"]).timestamp(),
|
||||||
# WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For
|
# WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For
|
||||||
# now, we will just pick the first one to use as our grid, latitude and longitude.
|
# now, we will just pick the first one to use as our grid, latitude and longitude.
|
||||||
grid=source_spot["references"][0]["locator"],
|
grid=source_spot["references"][0]["locator"],
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class WWFF(HTTPProvider):
|
|||||||
sig_refs=[source_spot["reference"]],
|
sig_refs=[source_spot["reference"]],
|
||||||
sig_refs_names=[source_spot["reference_name"]],
|
sig_refs_names=[source_spot["reference_name"]],
|
||||||
icon="seedling",
|
icon="seedling",
|
||||||
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC),
|
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(),
|
||||||
latitude=source_spot["latitude"],
|
latitude=source_spot["latitude"],
|
||||||
longitude=source_spot["longitude"])
|
longitude=source_spot["longitude"])
|
||||||
|
|
||||||
|
|||||||
@@ -87,10 +87,11 @@ class WebServer:
|
|||||||
response.content_type = 'application/json'
|
response.content_type = 'application/json'
|
||||||
response.set_header('Cache-Control', 'no-store')
|
response.set_header('Cache-Control', 'no-store')
|
||||||
return json.dumps("OK", default=serialize_everything)
|
return json.dumps("OK", default=serialize_everything)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
response.content_type = 'application/json'
|
response.content_type = 'application/json'
|
||||||
response.status = 422
|
response.status = 422
|
||||||
return json.dumps("An error occurred parsing your spot. Check it is compliant with the API.", default=serialize_everything)
|
return json.dumps("An error occurred parsing your spot: " + str(e), default=serialize_everything)
|
||||||
|
|
||||||
# Serve a templated page
|
# Serve a templated page
|
||||||
def serve_template(self, template_name):
|
def serve_template(self, template_name):
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ paths:
|
|||||||
operationId: spot
|
operationId: spot
|
||||||
parameters:
|
parameters:
|
||||||
- name: spot
|
- name: spot
|
||||||
description: The spot data to post
|
description: The spot data to post. The structure must contain at least "time" and "dx_call" to be accepted.
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type:
|
type:
|
||||||
@@ -487,13 +487,13 @@ components:
|
|||||||
- Unknown
|
- Unknown
|
||||||
example: 40m
|
example: 40m
|
||||||
time:
|
time:
|
||||||
type: string
|
type: number
|
||||||
description: Time of the spot, ISO 8601 format
|
description: Time of the spot, UTC seconds since UNIX epoch
|
||||||
example: 2025-09-28T19:12:41Z
|
example: 1759579508
|
||||||
received_time:
|
received_time:
|
||||||
type: string
|
type: number
|
||||||
description: Time that this software received the spot, ISO 8601 format. This is used with the "since_received" call to our API to receive all data that is new to us, even if by a quirk of the API it might be older than the list time the client polled the API.
|
description: Time that this software received the spot, UTC seconds since UNIX epoch. This is used with the "since_received" call to our API to receive all data that is new to us, even if by a quirk of the API it might be older than the list time the client polled the API.
|
||||||
example: 2025-09-28T19:12:41Z
|
example: 1759579508
|
||||||
comment:
|
comment:
|
||||||
type: string
|
type: string
|
||||||
description: Comment left by the spotter, if any
|
description: Comment left by the spotter, if any
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function updateTable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Format a UTC time for display
|
// Format a UTC time for display
|
||||||
var time = moment.utc(s["time"], moment.ISO_8601);
|
var time = moment.unix(s["time"]).utc();
|
||||||
var time_formatted = time.format("HH:mm")
|
var time_formatted = time.format("HH:mm")
|
||||||
|
|
||||||
// Format dx country
|
// Format dx country
|
||||||
|
|||||||
Reference in New Issue
Block a user