The RSS feed should still work and if it doesn’t then I need to fix it. It’s low priority as it seems no one uses it much (it broke for several months after the initial SW3 transfer and no one complained )
I use the api2.sota.org.uk and wrote my own code in Python so it does what I want.
You can build your own filters for spots and alerts.
Bits it doesn’t handle well are Commas and Nulls in comments fields I generally have them fixed.
As for calling I call 145.500 and QSY. I thank the many chasers who put a spot up. I let chasers know which other bands I will be on (70.45MHz-FM etc…)
Anyhow here is the Python Code I use. I don’t think it imports too well into the reflector.
#!/usr/bin/python3
import urllib.request
import time
import json
import os.path
Windows = 1
cls = lambda: print(‘\n’ *40) # clear the console by printing 25 blank lines
ActivationWindow = 90 # Window for alerts in days
SpotWindow = 4 # Window for Spots in houra
AssociationScope = str.upper(“g”) # Set Association, this can be a part match i.e. G will select G GM GD GJ GW etc.
PollTime = 300 # Number of seconds between polling the SOTA database
DisplayLines = 15 # Number of lines to be displayed in Spots and Alerts the entrys dusplayed are the closest times
ReqSpot = urllib.request.Request(‘https://api2.sota.org.uk/api/spots/100’)
ReqAlert = urllib.request.Request(‘https://api2.sota.org.uk/api/alerts’)
VersionText = “G4VFL test version 26/11/2021 Polling period set to " + str(PollTime) + " Seconds = " + “{:.0f}”.format(PollTime / 60) + " Min”
t = time.time() - (PollTime * 2) # Set the initalise time to display data on start
Load settings from the config file, if the file does not exist create it then use the default settings.
if Windows == 1 :
import os
os.system(“mode con cols=200 lines=” + str((DisplayLines * 2) + 10))
os.system(’ color e1 ') # Yellow background
def DisplayAlert():
Pull the data from the SOTA API
html = urllib.request.urlopen(ReqAlert).read()
DisplayLinesAlert = DisplayLines
# Initslise variables for global use
print("Next " + str(ActivationWindow) + " days of " + AssociationScope + " Alerts. Polled at " + time.strftime("%H:%M",time.gmtime()) + " GMT" )
x = 0
Startstr = 2
Endstr = 0
testtext = html.decode("utf-8-sig").replace('null}','" "}') # Convert the JSON data to a string variable and replace null with an empty string
while x < len(testtext) :
print (testtext) # for testing
if testtext[x] == '{' and testtext[x - 1] == ',' : # find the beginning of a JSON data block, these filters are to avoid picking a { in a comment
Startstr = x + 1
if testtext[x] == '}' and testtext[x - 1] == '"' : # find the end of a JSON data block, these filters are to avoid picking a } in a comment
Endstr = x
# Convert JSON into Python Dict
SpotDict = json.loads(testtext[Startstr - 1 : Endstr + 1] )
# reset varables
DisplayText = ""
Summit = ""
Summit = SpotDict.get("associationCode") + "/" + SpotDict.get("summitCode") # Concatanate the Summit
# produce display text this can be altered to suit the user and the screen size
DisplayText = SpotDict.get("dateActivated")[0:10].ljust(11," ") \
+ SpotDict.get("dateActivated")[11:16].ljust(8," ") \
+ SpotDict.get("activatingCallsign").ljust(12," ") \
+ SpotDict.get("activatorName")[0:10].ljust(12," ") \
+ Summit.ljust(12," ") \
+ SpotDict.get("summitDetails").ljust(50, " ") \
+ SpotDict.get("frequency")[0:35].ljust(40," ") \
+ SpotDict.get("comments")[0:30] # This is half of the field to save space
--------------------------------------------------------------------
Set the if statement to use to decide which output will be displayed
--------------------------------------------------------------------
if SpotDict.get(“frequency”).find(“145”)) > -1 : # only display a certain band
if SpotDict.get(“activatingCallsign”).find(str.upper(“ea2”)) > -1 : # Callsign full or part match
if Summit.find(str.upper(“G/Sp-005”)) > -1 : # Spot a region or specific summit
if SpotDict.get(“associationCode”)[0:len(AssociationScope)] == str.upper(AssociationScope) : # Display an Association or summit area or specific summit note the slice lenght will need altering
# To filter out centurys that will cause errors AND Select activations in the next nn days AND Association summits in scope
if SpotDict.get("dateActivated")[0:2] == "20" \
and time.mktime(time.strptime(SpotDict.get("dateActivated")[0:10], "%Y-%m-%d" ))< time.time() + ActivationWindow*24*3600 \
and SpotDict.get("associationCode")[0:len(AssociationScope)] == str.upper(AssociationScope) \
and DisplayLinesAlert > 0 :
print( DisplayText )
DisplayLinesAlert = DisplayLinesAlert - 1
x = x + 1
def DisplaySpot():
# grab the api data
html = urllib.request.urlopen(ReqSpot).read()
DisplayLinesSpot = DisplayLines
print(AssociationScope + " Spots, polled at " + time.strftime("%H:%M",time.gmtime()) + " GMT. Spot window set at " + str(SpotWindow) + " Hours")
x = 0 # declare global variables
Startstr = 2
Endstr = 0
cuttext = ""
testtext = html.decode("utf-8-sig").replace('null','""') # convert the api grab to a string , the replace is to remove null and replace it as a string.
testtext = html.decode("utf-8-sig").replace('""','') # convert the api grab to a string , the replace is to remove null and replace it as a string.
testtext = html.decode("utf-8-sig").replace('":,"','":"No Comment","') # convert the api grab to a string , the replace is to remove null and replace it as a string.
while x < len(testtext) :
# print (testtext[x]) # for testing
# Find the begining and end of the JSON strings
if testtext[x] == '{' and testtext[x - 1] == ',' : # Filter to ensure no false { found in comments
Startstr = x + 1
if testtext[x] == '}' and testtext[x - 1] == '"' : # filter to ensure no false } found in comments
Endstr = x
print(testtext[Startstr - 1 : Endstr + 1 ] ) # For debug
# Load the JSON string into the dictionary
SpotDict = json.loads(testtext[Startstr - 1 : Endstr + 1 ] )
# clear variables
DisplayText = ""
Summit = ""
# Produce the display strings
Summit = SpotDict.get("associationCode") + "/" + SpotDict.get("summitCode")
DisplayText = SpotDict.get("timeStamp")[0:10].ljust(11," ") \
+ SpotDict.get("timeStamp")[11:16].ljust(8," ") \
+ SpotDict.get("activatorCallsign").ljust(12," ") \
+ SpotDict.get("activatorName")[0:10].ljust(12," ") \
+ Summit.ljust(12," ") \
+ SpotDict.get("summitDetails").ljust(50," ") \
+ SpotDict.get("frequency").ljust(10," ") \
+ SpotDict.get("mode").ljust(6," ")
+ SpotDict.get(“comments”)[0:30]
Select a display string
if SpotDict.get(“frequency”).find(“145”)) > -1 : # select the freq
if SpotDict.get(“activatorCallsign”).find(str.upper(“ea2”)) > -1 : # Callsign full or part match
if Summit.find(str.upper(“G/Sp-005”)) > -1 : # Spot a region or specific summit
if SpotDict.get("associationCode")[0:len(AssociationScope)] == str.upper(AssociationScope) \
and time.mktime(time.strptime(SpotDict.get("timeStamp")[0:16], "%Y-%m-%dT%H:%M" ))> time.time()- (SpotWindow * 3600) \
and DisplayLinesSpot > 0 : # Association, note they are different lengths so the slice may need altered and Only a limited number of lines are displayed to prevent over run on a busy screen
if True : # print everything
print( DisplayText)
DisplayLinesSpot = DisplayLinesSpot - 1
x = x + 1
while True:
if time.time() - t > PollTime : # once ever couple of minute run the process
try:
urllib.request.urlopen(ReqSpot)
except urllib.error.URLError :
print("Check Internet Connetion " + time.strftime("%H:%M",time.gmtime()))
else:
cls()
DisplaySpot()
print()
DisplayAlert()
print("\n" + VersionText)
finally:
t = time.time()
time.sleep(PollTime/10) # to reduce the load on the CPU
Almost always start with a CQ on 145.500.
The exception is if I’m working in the hills with a guided group and I’ve got limited time. I occasionally just have a few folk lined up and quickly qualify, but if others find me I do work them too. This is very rare and forced on me by time constraints.
Too many aspects of code syntax being interpreted as formatting instructions, particularly the leading space (which is syntactically important in Python). It might work if you put it inside a “</> Pre-formatted text” block…
Hi Gerald @MW0WML,
The aim of the game is obviously to qualify the summit but also work all the great supportive chasers that call into our SOTA stations (and any other stations that call in as well). Been quite active over the past few months, I find checking a frequency is available first before calling on S20 works well. For example: I tend to ask if freq 145.475 is free while I’m typing up my spot. Once I hear nothing, I QSY back to S20 and put a CQ SOTA call out explaining what summit I’m on and where to QSY. Then I again ask if the freq is free (put my spot on if it is), and call CQ SOTA, hoping for a nice pile-up that seems to work perfectly and once I’ve worked everyone, I put a couple of final calls out.
73, GW4BML. Ben
Many thanks for the tip on formatting,
If anyone really wants the code here is a 2nd go. Like all amateur code it is “as is”. I haven’t seen any guidence on what would be acceptable polling of the SOTA database but I think 5 min is not excessive.
I run it on a PC and a Raspberry PI with some minor differneces because of the way the consoles work.
I have a variant that works with WOTA using the RSS feeds.
Sorry nothing for HEMAs, Screen scraping is too much hassel.
73 de Andrew G4VFL
#!/usr/bin/python3
import urllib.request
import time
import json
import os.path
Windows = 1
cls = lambda: print('\n' *40) # clear the console by printing 25 blank lines
ActivationWindow = 90 # Window for alerts in days
SpotWindow = 4 # Window for Spots in houra
AssociationScope = str.upper("g") # Set Association, this can be a part match i.e. G will select G GM GD GJ GW etc.
PollTime = 300 # Number of seconds between polling the SOTA database
DisplayLines = 15 # Number of lines to be displayed in Spots and Alerts the entrys dusplayed are the closest times
ReqSpot = urllib.request.Request('https://api2.sota.org.uk/api/spots/100')
ReqAlert = urllib.request.Request('https://api2.sota.org.uk/api/alerts')
VersionText = "G4VFL test version 26/11/2021 Polling period set to " + str(PollTime) + " Seconds = " + "{:.0f}".format(PollTime / 60) + " Min"
t = time.time() - (PollTime * 2) # Set the initalise time to display data on start
# Load settings from the config file, if the file does not exist create it then use the default settings.
if Windows == 1 :
import os
os.system("mode con cols=200 lines=" + str((DisplayLines * 2) + 10))
os.system(' color e1 ') # Yellow background
def DisplayAlert():
# Pull the data from the SOTA API
html = urllib.request.urlopen(ReqAlert).read()
DisplayLinesAlert = DisplayLines
# Initslise variables for global use
print("Next " + str(ActivationWindow) + " days of " + AssociationScope + " Alerts. Polled at " + time.strftime("%H:%M",time.gmtime()) + " GMT" )
x = 0
Startstr = 2
Endstr = 0
testtext = html.decode("utf-8-sig").replace('null}','" "}') # Convert the JSON data to a string variable and replace null with an empty string
while x < len(testtext) :
# print (testtext[x]) # for testing
if testtext[x] == '{' and testtext[x - 1] == ',' : # find the beginning of a JSON data block, these filters are to avoid picking a { in a comment
Startstr = x + 1
if testtext[x] == '}' and testtext[x - 1] == '"' : # find the end of a JSON data block, these filters are to avoid picking a } in a comment
Endstr = x
# Convert JSON into Python Dict
SpotDict = json.loads(testtext[Startstr - 1 : Endstr + 1] )
# reset varables
DisplayText = ""
Summit = ""
Summit = SpotDict.get("associationCode") + "/" + SpotDict.get("summitCode") # Concatanate the Summit
# produce display text this can be altered to suit the user and the screen size
DisplayText = SpotDict.get("dateActivated")[0:10].ljust(11," ") \
+ SpotDict.get("dateActivated")[11:16].ljust(8," ") \
+ SpotDict.get("activatingCallsign").ljust(12," ") \
+ SpotDict.get("activatorName")[0:10].ljust(12," ") \
+ Summit.ljust(12," ") \
+ SpotDict.get("summitDetails").ljust(50, " ") \
+ SpotDict.get("frequency")[0:35].ljust(40," ") \
+ SpotDict.get("comments")[0:30] # This is half of the field to save space
# --------------------------------------------------------------------
# Set the if statement to use to decide which output will be displayed
# --------------------------------------------------------------------
# if SpotDict.get("frequency").find("145")) > -1 : # only display a certain band
# if SpotDict.get("activatingCallsign").find(str.upper("ea2")) > -1 : # Callsign full or part match
# if Summit.find(str.upper("G/Sp-005")) > -1 : # Spot a region or specific summit
# if SpotDict.get("associationCode")[0:len(AssociationScope)] == str.upper(AssociationScope) : # Display an Association or summit area or specific summit note the slice lenght will need altering
# To filter out centurys that will cause errors AND Select activations in the next nn days AND Association summits in scope
if SpotDict.get("dateActivated")[0:2] == "20" \
and time.mktime(time.strptime(SpotDict.get("dateActivated")[0:10], "%Y-%m-%d" ))< time.time() + ActivationWindow*24*3600 \
and SpotDict.get("associationCode")[0:len(AssociationScope)] == str.upper(AssociationScope) \
and DisplayLinesAlert > 0 :
print( DisplayText )
DisplayLinesAlert = DisplayLinesAlert - 1
x = x + 1
def DisplaySpot():
# grab the api data
html = urllib.request.urlopen(ReqSpot).read()
DisplayLinesSpot = DisplayLines
print(AssociationScope + " Spots, polled at " + time.strftime("%H:%M",time.gmtime()) + " GMT. Spot window set at " + str(SpotWindow) + " Hours")
x = 0 # declare global variables
Startstr = 2
Endstr = 0
cuttext = ""
testtext = html.decode("utf-8-sig").replace('null','""') # convert the api grab to a string , the replace is to remove null and replace it as a string.
testtext = html.decode("utf-8-sig").replace('""','') # convert the api grab to a string , the replace is to remove null and replace it as a string.
testtext = html.decode("utf-8-sig").replace('":,"','":"No Comment","') # convert the api grab to a string , the replace is to remove null and replace it as a string.
while x < len(testtext) :
# print (testtext[x]) # for testing
# Find the begining and end of the JSON strings
if testtext[x] == '{' and testtext[x - 1] == ',' : # Filter to ensure no false { found in comments
Startstr = x + 1
if testtext[x] == '}' and testtext[x - 1] == '"' : # filter to ensure no false } found in comments
Endstr = x
# print(testtext[Startstr - 1 : Endstr + 1 ] ) # For debug
# Load the JSON string into the dictionary
SpotDict = json.loads(testtext[Startstr - 1 : Endstr + 1 ] )
# clear variables
DisplayText = ""
Summit = ""
# Produce the display strings
Summit = SpotDict.get("associationCode") + "/" + SpotDict.get("summitCode")
DisplayText = SpotDict.get("timeStamp")[0:10].ljust(11," ") \
+ SpotDict.get("timeStamp")[11:16].ljust(8," ") \
+ SpotDict.get("activatorCallsign").ljust(12," ") \
+ SpotDict.get("activatorName")[0:10].ljust(12," ") \
+ Summit.ljust(12," ") \
+ SpotDict.get("summitDetails").ljust(50," ") \
+ SpotDict.get("frequency").ljust(10," ") \
+ SpotDict.get("mode").ljust(6," ")
# + SpotDict.get("comments")[0:30]
# Select a display string
# if SpotDict.get("frequency").find("145")) > -1 : # select the freq
# if SpotDict.get("activatorCallsign").find(str.upper("ea2")) > -1 : # Callsign full or part match
# if Summit.find(str.upper("G/Sp-005")) > -1 : # Spot a region or specific summit
if SpotDict.get("associationCode")[0:len(AssociationScope)] == str.upper(AssociationScope) \
and time.mktime(time.strptime(SpotDict.get("timeStamp")[0:16], "%Y-%m-%dT%H:%M" ))> time.time()- (SpotWindow * 3600) \
and DisplayLinesSpot > 0 : # Association, note they are different lengths so the slice may need altered and Only a limited number of lines are displayed to prevent over run on a busy screen
# if True : # print everything
print( DisplayText)
DisplayLinesSpot = DisplayLinesSpot - 1
x = x + 1
while True:
if time.time() - t > PollTime : # once ever couple of minute run the process
try:
urllib.request.urlopen(ReqSpot)
except urllib.error.URLError :
print("Check Internet Connetion " + time.strftime("%H:%M",time.gmtime()))
else:
cls()
DisplaySpot()
print()
DisplayAlert()
print("\n" + VersionText)
finally:
t = time.time()
time.sleep(PollTime/10) # to reduce the load on the CPU
Bearing in mind I’m new and started in winter, I like 2m for SOTA as the kit is compact, ideal in the cold & rain as it’s quickly set up & packed away. When the weather is more reliable I’ll get a dipole up and may even send some baffling, dyslexic CW before autumn.
For me, SOTA operating is 145.500MHz for CQ then move to work a clear channel. Usually get followed so calling QRZ leads to a nice clutch of contacts. If things dry up, back to CQ on 145.500MHz and then try to get back on the same working channel. Doesn’t always work. Love S2S calls. Happy to give up a channel if it gives a contact for another SOTA player. Always amazed at the range possible on a good day.
Usually a kind chaser chalks up a spot for me on SOTAwatch. Oddly I’ve never carried a mobile phone in the hills yet a radio seems ok!?!?
I think it depends very much where you are operating. In your region within sight so to speak of the Lake District and the South Pennines not to mention Snowdon you will be able to operate as you describe. In other places it is harder as I re-discovered on G/DC-001 last Friday. Only HF saved the day.
But it’s all part of the fun!
It was the Program I was using that stopped working for some reason - it appeared to be trying to call ‘home’ and failing to do so and then not loading the SOTA RSS Feed - I did try removing and re-installing but that did not help - I don’t recall it’s name.
If anyone is using an RSS Reader that works I wouldn’t mind trying another one, but I was not impressed with the ones I found from a search.
Stewart, did you try the built-in Sotawatch filtering?
I often keep a tab open with a filter set for just G* and EI summits. Go into settings on sotawatch (under your account - top right). There you can turn on the audible warnings and set the filtering (alerts too, as well as spots).
The filtering may also be controlled from the little settings area that can be made visible at the top of the spots/alerts table. This doesn’t control the sounds though.
Mostly though I listen for the alerts coming through Ham Alert on my phone. My colleagues in Zoom meetings are also very familiar with the Morse for SOTA by now!
Ok, the first sentence still stands true. However, when activating GM/CS-008 on 2m FM last week, I couldn’t get a word in on 145.500MHz due to the level of activity, nets starting etc, so I went to 145.425, spotted this frequency and started calling immediately. The Chasers found me, as did a few others. So, all good.
From a listening (or Chasing) point of view, if I have 2m FM on at home (rare) or whilst mobile (daily), I’ll always have the radio scanning constantly through memory - simplex and local repeater channels.
Isn’t that the simplest answer to the original question?
That really does not work here it stops scanning because of noise etc too often - I often have two bands of noise that randomly move around the 2m band.
Yes, thats a fair point! Not an issue for me in rural Aberdeenshire.
In some regions of the country, you absolutely should call on the calling frequency to have any hope of reaching anyone, because you are simply not going to attract a chain of callers. You will need to go back to the calling frequency between every QSO and put three or four CQ calls out or more over a one hour period in order to get anyone because the amateur radio population on 2m is so thin on the ground. If you do get a pile up, then lucky you, you won’t need to go back to the calling frequency. If a spot goes out, it should spot the frequency you are on, however, alerts on the other hand should show just the calling frequency because you don’t know what frequencies will be available until you arrive at the summit.