mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2025-10-27 08:49:27 +00:00
First commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/.idea
|
||||||
|
/.venv
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
24
LICENSE
Normal file
24
LICENSE
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <https://unlicense.org>
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# MetaSpot
|
||||||
|
|
||||||
|
A utility to aggregate spots from amateur radio DX clusters and xOTA spotting sites, and provide an open JSON API as well as a website to browse the data.
|
||||||
|
|
||||||
|
Work in progress.
|
||||||
432
core/constants.py
Normal file
432
core/constants.py
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
from data.band import Band
|
||||||
|
|
||||||
|
# General software
|
||||||
|
SOFTWARE_NAME = "Metaspot by M0TRT"
|
||||||
|
SOFTWARE_VERSION = "0.1"
|
||||||
|
|
||||||
|
# Band definitions
|
||||||
|
BANDS = [
|
||||||
|
Band(name="160m", start_freq=1800, end_freq=2000, color="#7cfc00", contrast_color="black"),
|
||||||
|
Band(name="80m", start_freq=3500, end_freq=4000, color="#e550e5", contrast_color="black"),
|
||||||
|
Band(name="60m", start_freq=5250, end_freq=5410, color="#00008b", contrast_color="white"),
|
||||||
|
Band(name="40m", start_freq=7000, end_freq=7300, color="#5959ff", contrast_color="white"),
|
||||||
|
Band(name="30m", start_freq=10100, end_freq=10150, color="#62d962", contrast_color="black"),
|
||||||
|
Band(name="20m", start_freq=14000, end_freq=14350, color="#f2c40c", contrast_color="black"),
|
||||||
|
Band(name="17m", start_freq=18068, end_freq=18168, color="#f2f261", contrast_color="black"),
|
||||||
|
Band(name="15m", start_freq=21000, end_freq=21450, color="#cca166", contrast_color="black"),
|
||||||
|
Band(name="12m", start_freq=24890, end_freq=24990, color="#b22222", contrast_color="white"),
|
||||||
|
Band(name="10m", start_freq=28000, end_freq=29700, color="#ff69b4", contrast_color="black"),
|
||||||
|
Band(name="6m", start_freq=50000, end_freq=54000, color="#FF0000", contrast_color="white"),
|
||||||
|
Band(name="4m", start_freq=70000, end_freq=70500, color="#cc0044", contrast_color="white"),
|
||||||
|
Band(name="2m", start_freq=144000, end_freq=148000, color="#FF1493", contrast_color="black"),
|
||||||
|
Band(name="70cm", start_freq=420000, end_freq=450000, color="#999900", contrast_color="white"),
|
||||||
|
Band(name="23cm", start_freq=1240000, end_freq=1325000, color="#5AB8C7", contrast_color="black"),
|
||||||
|
Band(name="13cm", start_freq=2300000, end_freq=2450000, color="#FF7F50", contrast_color="black")]
|
||||||
|
UNKNOWN_BAND = Band(name="Unknown", start_freq=0, end_freq=0, color="black", contrast_color="white")
|
||||||
|
|
||||||
|
# DXCC flags (borrowed from https:#github.com/wavelog/wavelog/blob/master/application/libraries/DxccFlag.php)
|
||||||
|
DXCC_FLAGS = {
|
||||||
|
0: "", # DXCC NONE
|
||||||
|
1: "\U0001F1E8\U0001F1E6", # CANADA
|
||||||
|
2: "", # ABU AIL IS
|
||||||
|
3: "\U0001F1E6\U0001F1EB", # AFGHANISTAN
|
||||||
|
4: "", # AGALEGA & ST BRANDON ISLANDS
|
||||||
|
5: "\U0001F1E6\U0001F1FD", # ALAND ISLANDS
|
||||||
|
6: "", # ALASKA
|
||||||
|
7: "\U0001F1E6\U0001F1F1", # ALBANIA
|
||||||
|
8: "", # ALDABRA
|
||||||
|
9: "\U0001F1E6\U0001F1F8", # AMERICAN SAMOA
|
||||||
|
10: "\U0001F1F9\U0001F1EB", # AMSTERDAM & ST PAUL ISLANDS
|
||||||
|
11: "", # ANDAMAN & NICOBAR ISLANDS
|
||||||
|
12: "\U0001F1E6\U0001F1EE", # ANGUILLA
|
||||||
|
13: "\U0001F1E6\U0001F1F6", # ANTARCTICA
|
||||||
|
14: "\U0001F1E6\U0001F1F2", # ARMENIA
|
||||||
|
15: "\U0001F1F7\U0001F1FA", # ASIATIC RUSSIA
|
||||||
|
16: "", # NEW ZEALAND SUBANTARCTIC ISLANDS
|
||||||
|
17: "", # AVES ISLAND
|
||||||
|
18: "\U0001F1E6\U0001F1FF", # AZERBAIJAN
|
||||||
|
19: "", # BAJO NUEVO
|
||||||
|
20: "", # BAKER HOWLAND ISLANDS
|
||||||
|
21: "", # BALEARIC ISLANDS
|
||||||
|
22: "\U0001F1F5\U0001F1FC", # PALAU
|
||||||
|
23: "", # BLENHEIM REEF
|
||||||
|
24: "\U0001F1E7\U0001F1FB", # BOUVET ISLAND
|
||||||
|
25: "", # BRITISH NORTH BORNEO
|
||||||
|
26: "", # BRITISH SOMALILAND
|
||||||
|
27: "\U0001F1E7\U0001F1FE", # BELARUS
|
||||||
|
28: "\U0001F1E8\U0001F1FB", # CANAL ZONE
|
||||||
|
29: "", # CANARY ISLANDS
|
||||||
|
30: "", # CELEBE & MOLUCCA ISLANDS
|
||||||
|
31: "\U0001F1F0\U0001F1EE", # CENTRAL KIRIBATI
|
||||||
|
32: "", # CEUTA & MELILLA
|
||||||
|
33: "", # CHAGOS ISLANDS
|
||||||
|
34: "", # CHATHAM ISLAND
|
||||||
|
35: "\U0001F1E8\U0001F1FD", # CHRISTMAS ISLAND
|
||||||
|
36: "", # CLIPPERTON ISLAND
|
||||||
|
37: "", # COCOS ISLAND
|
||||||
|
38: "\U0001F1E8\U0001F1E8", # COCOS (KEELING) ISLAND
|
||||||
|
39: "", # COMORO ISLANDS
|
||||||
|
40: "", # CRETE
|
||||||
|
41: "\U0001F1F9\U0001F1EB", # CROZET ISLAND
|
||||||
|
42: "", #"DAMAO, DIU"
|
||||||
|
43: "", # DESECHEO ISLAND
|
||||||
|
44: "", # DESROCHES
|
||||||
|
45: "", # DODECANESE
|
||||||
|
46: "", # EAST MALAYSIA
|
||||||
|
47: "", # EASTER ISLAND
|
||||||
|
48: "\U0001F1F0\U0001F1EE", # EASTERN KIRIBATI
|
||||||
|
49: "\U0001F1EC\U0001F1F6", # EQUATORIAL GUINEA
|
||||||
|
50: "\U0001F1F2\U0001F1FD", # MEXICO
|
||||||
|
51: "\U0001F1EA\U0001F1F7", # ERITREA
|
||||||
|
52: "\U0001F1EA\U0001F1EA", # ESTONIA
|
||||||
|
53: "\U0001F1EA\U0001F1F9", # ETHIOPIA
|
||||||
|
54: "\U0001F1F7\U0001F1FA", # EUROPEAN RUSSIA
|
||||||
|
55: "", # FARQUHAR
|
||||||
|
56: "", # FERNANDO DE NORONHA
|
||||||
|
57: "", # FRENCH EQUATORIAL AFRICA
|
||||||
|
58: "", # FRENCH INDO-CHINA
|
||||||
|
59: "", # FRENCH WEST AFRICA
|
||||||
|
60: "\U0001F1E7\U0001F1F8", # BAHAMAS
|
||||||
|
61: "", # FRANZ JOSEF LAND
|
||||||
|
62: "\U0001F1E7\U0001F1E7", # BARBADOS
|
||||||
|
63: "\U0001F1EC\U0001F1EB", # FRENCH GUIANA
|
||||||
|
64: "\U0001F1E7\U0001F1F2", # BERMUDA
|
||||||
|
65: "\U0001F1FB\U0001F1EC", # BRITISH VIRGIN ISLANDS
|
||||||
|
66: "\U0001F1E7\U0001F1FF", # BELIZE
|
||||||
|
67: "", # FRENCH INDIA
|
||||||
|
68: "", # KUWAIT/SAUDI ARABIA NEUT. ZONE
|
||||||
|
69: "\U0001F1F0\U0001F1FE", # CAYMAN ISLANDS
|
||||||
|
70: "\U0001F1E8\U0001F1FA", # CUBA
|
||||||
|
71: "", # GALAPAGOS ISLANDS
|
||||||
|
72: "\U0001F1E9\U0001F1F4", # DOMINICAN REPUBLIC
|
||||||
|
74: "\U0001F1F8\U0001F1FB", # EL SALVADOR
|
||||||
|
75: "\U0001F1EC\U0001F1EA", # GEORGIA
|
||||||
|
76: "\U0001F1EC\U0001F1F9", # GUATEMALA
|
||||||
|
77: "\U0001F1EC\U0001F1E9", # GRENADA
|
||||||
|
78: "\U0001F1ED\U0001F1F9", # HAITI
|
||||||
|
79: "\U0001F1EC\U0001F1F5", # GUADELOUPE
|
||||||
|
80: "\U0001F1ED\U0001F1F3", # HONDURAS
|
||||||
|
81: "", # GERMANY
|
||||||
|
82: "\U0001F1EF\U0001F1F2", # JAMAICA
|
||||||
|
84: "\U0001F1F2\U0001F1F6", # MARTINIQUE
|
||||||
|
85: "", #"BONAIRE, CURACAO (NETH ANTILLES)"
|
||||||
|
86: "\U0001F1F3\U0001F1EE", # NICARAGUA
|
||||||
|
88: "\U0001F1F5\U0001F1E6", # PANAMA
|
||||||
|
89: "\U0001F1F9\U0001F1E8", # TURKS & CAICOS ISLANDS
|
||||||
|
90: "\U0001F1F9\U0001F1F9", # TRINIDAD & TOBAGO
|
||||||
|
91: "\U0001F1E6\U0001F1FC", # ARUBA
|
||||||
|
93: "", # GEYSER REEF
|
||||||
|
94: "\U0001F1E6\U0001F1EC", # ANTIGUA & BARBUDA
|
||||||
|
95: "\U0001F1E9\U0001F1F2", # DOMINICA
|
||||||
|
96: "\U0001F1F2\U0001F1F8", # MONTSERRAT
|
||||||
|
97: "\U0001F1F1\U0001F1E8", # SAINT LUCIA
|
||||||
|
98: "\U0001F1FB\U0001F1E8", # SAINT VINCENT
|
||||||
|
99: "", # GLORIOSO ISLAND
|
||||||
|
100: "\U0001F1E6\U0001F1F7", # ARGENTINA
|
||||||
|
101: "", # GOA
|
||||||
|
102: "", # GOLD COAST TOGOLAND
|
||||||
|
103: "\U0001F1EC\U0001F1FA", # GUAM
|
||||||
|
104: "\U0001F1E7\U0001F1F4", # BOLIVIA
|
||||||
|
105: "", # GUANTANAMO BAY
|
||||||
|
106: "\U0001F1EC\U0001F1EC", # GUERNSEY
|
||||||
|
107: "\U0001F1EC\U0001F1F3", # GUINEA
|
||||||
|
108: "\U0001F1E7\U0001F1F7", # BRAZIL
|
||||||
|
109: "\U0001F1EC\U0001F1FC", # GUINEA-BISSAU
|
||||||
|
110: "", # HAWAII
|
||||||
|
111: "\U0001F1ED\U0001F1F2", # HEARD ISLAND
|
||||||
|
112: "\U0001F1E8\U0001F1F1", # CHILE
|
||||||
|
113: "", # IFNI
|
||||||
|
114: "\U0001F1EE\U0001F1F2", # ISLE OF MAN
|
||||||
|
115: "", # ITALIAN SOMALI
|
||||||
|
116: "\U0001F1E8\U0001F1F4", # COLOMBIA
|
||||||
|
117: "", # ITU HQ
|
||||||
|
118: "", # JAN MAYEN
|
||||||
|
119: "", # JAVA
|
||||||
|
120: "\U0001F1EA\U0001F1E8", # ECUADOR
|
||||||
|
122: "\U0001F1EF\U0001F1EA", # JERSEY
|
||||||
|
123: "", # JOHNSTON ISLAND
|
||||||
|
124: "", #"JUAN DE NOVA, EUROPA"
|
||||||
|
125: "", # JUAN FERNANDEZ ISLANDS
|
||||||
|
126: "", # KALININGRAD
|
||||||
|
127: "", # KAMARAN ISLANDS
|
||||||
|
128: "", # KARELO-FINN REP
|
||||||
|
129: "\U0001F1EC\U0001F1FE", # GUYANA
|
||||||
|
130: "\U0001F1F0\U0001F1FF", # KAZAKHSTAN
|
||||||
|
131: "\U0001F1F9\U0001F1EB", # KERGUELEN ISLAND
|
||||||
|
132: "\U0001F1F5\U0001F1FE", # PARAGUAY
|
||||||
|
133: "", # KERMADEC ISLAND
|
||||||
|
134: "", # KINGMAN REEF
|
||||||
|
135: "\U0001F1F0\U0001F1EC", # KYRGYZSTAN
|
||||||
|
136: "\U0001F1F5\U0001F1EA", # PERU
|
||||||
|
137: "\U0001F1F0\U0001F1F7", # REPUBLIC OF KOREA
|
||||||
|
138: "", # KURE ISLAND
|
||||||
|
139: "", # KURIA MURIA ISLAND
|
||||||
|
140: "", # SURINAME
|
||||||
|
141: "\U0001F1EB\U0001F1F0", # FALKLAND ISLANDS
|
||||||
|
142: "", # LAKSHADWEEP ISLANDS
|
||||||
|
143: "\U0001F1F1\U0001F1E6", # LAOS
|
||||||
|
144: "\U0001F1FA\U0001F1FE", # URUGUAY
|
||||||
|
145: "\U0001F1F1\U0001F1FB", # LATVIA
|
||||||
|
146: "\U0001F1F1\U0001F1F9", # LITHUANIA
|
||||||
|
147: "", # LORD HOWE ISLAND
|
||||||
|
148: "\U0001F1FB\U0001F1EA", # VENEZUELA
|
||||||
|
149: "", # AZORES
|
||||||
|
150: "\U0001F1E6\U0001F1FA", # AUSTRALIA
|
||||||
|
151: "", # MALYJ VYSOTSKIJ ISLAND
|
||||||
|
152: "\U0001F1F2\U0001F1F4", # MACAO
|
||||||
|
153: "", # MACQUARIE ISLAND
|
||||||
|
154: "", # YEMEN ARAB REPUBLIC
|
||||||
|
155: "\U0001F1F2\U0001F1FE", # MALAYA
|
||||||
|
157: "\U0001F1F3\U0001F1F7", # NAURU
|
||||||
|
158: "\U0001F1FB\U0001F1FA", # VANUATU
|
||||||
|
159: "\U0001F1F2\U0001F1FB", # MALDIVES
|
||||||
|
160: "\U0001F1F9\U0001F1F4", # TONGA
|
||||||
|
161: "", # MALPELO ISLAND
|
||||||
|
162: "\U0001F1F3\U0001F1E8", # NEW CALEDONIA
|
||||||
|
163: "\U0001F1F5\U0001F1EC", # PAPUA NEW GUINEA
|
||||||
|
164: "", # MANCHURIA
|
||||||
|
165: "\U0001F1F2\U0001F1FA", # MAURITIUS ISLAND
|
||||||
|
166: "", # MARIANA ISLANDS
|
||||||
|
167: "", # MARKET REEF
|
||||||
|
168: "\U0001F1F2\U0001F1ED", # MARSHALL ISLANDS
|
||||||
|
169: "\U0001F1FE\U0001F1F9", # MAYOTTE
|
||||||
|
170: "\U0001F1F3\U0001F1FF", # NEW ZEALAND
|
||||||
|
171: "", # MELLISH REEF
|
||||||
|
172: "\U0001F1F5\U0001F1F3", # PITCAIRN ISLAND
|
||||||
|
173: "\U0001F1EB\U0001F1F2", # MICRONESIA
|
||||||
|
174: "", # MIDWAY ISLAND
|
||||||
|
175: "\U0001F1F5\U0001F1EB", # FRENCH POLYNESIA
|
||||||
|
176: "\U0001F1EB\U0001F1EF", # FIJI ISLANDS
|
||||||
|
177: "", # MINAMI TORISHIMA
|
||||||
|
178: "", # MINERVA REEF
|
||||||
|
179: "\U0001F1F2\U0001F1E9", # MOLDOVA
|
||||||
|
180: "", # MOUNT ATHOS
|
||||||
|
181: "\U0001F1F2\U0001F1FF", # MOZAMBIQUE
|
||||||
|
182: "", # NAVASSA ISLAND
|
||||||
|
183: "", # NETHERLANDS BORNEO
|
||||||
|
184: "", # NETHERLANDS NEW GUINEA
|
||||||
|
185: "\U0001F1F8\U0001F1E7", # SOLOMON ISLANDS
|
||||||
|
186: "", # NEWFOUNDLAND LABRADOR
|
||||||
|
187: "\U0001F1F3\U0001F1EA", # NIGER
|
||||||
|
188: "\U0001F1F3\U0001F1FA", # NIUE
|
||||||
|
189: "\U0001F1F3\U0001F1EB", # NORFOLK ISLAND
|
||||||
|
190: "\U0001F1FC\U0001F1F8", # SAMOA
|
||||||
|
191: "\U0001F1E8\U0001F1F0", # NORTH COOK ISLANDS
|
||||||
|
192: "", # OGASAWARA
|
||||||
|
193: "", # OKINAWA
|
||||||
|
194: "", # OKINO TORI-SHIMA
|
||||||
|
195: "", # ANNOBON
|
||||||
|
196: "", # PALESTINE (DELETED)
|
||||||
|
197: "", # PALMYRA & JARVIS ISLANDS
|
||||||
|
198: "", # PAPUA TERR
|
||||||
|
199: "", # PETER 1 ISLAND
|
||||||
|
200: "", # PORTUGUESE TIMOR
|
||||||
|
201: "", # PRINCE EDWARD & MARION ISLANDS
|
||||||
|
202: "\U0001F1F5\U0001F1F7", # PUERTO RICO
|
||||||
|
203: "\U0001F1E6\U0001F1E9", # ANDORRA
|
||||||
|
204: "", # REVILLAGIGEDO
|
||||||
|
205: "", # ASCENSION ISLAND
|
||||||
|
206: "\U0001F1E6\U0001F1F9", # AUSTRIA
|
||||||
|
207: "", # RODRIGUEZ ISLAND
|
||||||
|
208: "", # RUANDA-URUNDI
|
||||||
|
209: "\U0001F1E7\U0001F1EA", # BELGIUM
|
||||||
|
210: "", # SAAR
|
||||||
|
211: "", # SABLE ISLAND
|
||||||
|
212: "\U0001F1E7\U0001F1EC", # BULGARIA
|
||||||
|
213: "\U0001F1F2\U0001F1EB", # SAINT MARTIN
|
||||||
|
214: "", # CORSICA
|
||||||
|
215: "\U0001F1E8\U0001F1FE", # CYPRUS
|
||||||
|
216: "", # SAN ANDRES ISLAND
|
||||||
|
217: "", # SAN FELIX ISLANDS
|
||||||
|
218: "", # CZECHOSLOVAKIA
|
||||||
|
219: "\U0001F1F8\U0001F1F9", # SAO TOME & PRINCIPE
|
||||||
|
220: "", # SARAWAK
|
||||||
|
221: "\U0001F1E9\U0001F1F0", # DENMARK
|
||||||
|
222: "\U0001F1EB\U0001F1F4", # FAROE ISLANDS
|
||||||
|
223: "\U0001F3F4\U000E0067\U000E0062\U000E0065\U000E006E\U000E0067\U000E007F", # ENGLAND
|
||||||
|
224: "\U0001F1EB\U0001F1EE", # FINLAND
|
||||||
|
225: "", # SARDINIA
|
||||||
|
226: "", # SAUDI ARABIA/IRAQ NEUT ZONE
|
||||||
|
227: "\U0001F1EB\U0001F1F7", # FRANCE
|
||||||
|
228: "", # SERRANA BANK & RONCADOR CAY
|
||||||
|
229: "\U0001F1E9\U0001F1EA", # GERMAN DEMOCRATIC REPUBLIC
|
||||||
|
230: "\U0001F1E9\U0001F1EA", # FEDERAL REPUBLIC OF GERMANY
|
||||||
|
231: "", # SIKKIM
|
||||||
|
232: "\U0001F1F8\U0001F1F4", # SOMALIA
|
||||||
|
233: "\U0001F1EC\U0001F1EE", # GIBRALTAR
|
||||||
|
234: "\U0001F1E8\U0001F1F0", # SOUTH COOK ISLANDS
|
||||||
|
235: "\U0001F1EC\U0001F1F8", # SOUTH GEORGIA ISLAND
|
||||||
|
236: "\U0001F1EC\U0001F1F7", # GREECE
|
||||||
|
237: "\U0001F1EC\U0001F1F1", # GREENLAND
|
||||||
|
238: "", # SOUTH ORKNEY ISLANDS
|
||||||
|
239: "\U0001F1ED\U0001F1FA", # HUNGARY
|
||||||
|
240: "", # SOUTH SANDWICH ISLANDS
|
||||||
|
241: "", # SOUTH SHETLAND ISLANDS
|
||||||
|
242: "\U0001F1EE\U0001F1F8", # ICELAND
|
||||||
|
243: "", # PEOPLES DEM REP OF YEMEN
|
||||||
|
244: "\U0001F1F8\U0001F1F8", # SOUTHERN SUDAN
|
||||||
|
245: "\U0001F1EE\U0001F1EA", # IRELAND
|
||||||
|
246: "", # SOV MILITARY ORDER OF MALTA
|
||||||
|
247: "", # SPRATLY ISLANDS
|
||||||
|
248: "\U0001F1EE\U0001F1F9", # ITALY
|
||||||
|
249: "\U0001F1F0\U0001F1F3", # SAINT KITTS & NEVIS
|
||||||
|
250: "\U0001F1F8\U0001F1ED", # SAINT HELENA
|
||||||
|
251: "\U0001F1F1\U0001F1EE", # LIECHTENSTEIN
|
||||||
|
252: "", # SAINT PAUL ISLAND
|
||||||
|
253: "\U0001F1F5\U0001F1F2", # SAINT PETER AND PAUL ROCKS
|
||||||
|
254: "\U0001F1F1\U0001F1FA", # LUXEMBOURG
|
||||||
|
255: "", #"SINT MAARTEN, SABA, ST EUSTATIUS"
|
||||||
|
256: "", # MADEIRA ISLANDS
|
||||||
|
257: "\U0001F1F2\U0001F1F9", # MALTA
|
||||||
|
258: "\U0001F1F8\U0001F1F7", # SUMATRA
|
||||||
|
259: "\U0001F1F8\U0001F1EF", # SVALBARD
|
||||||
|
260: "\U0001F1F2\U0001F1E8", # MONACO
|
||||||
|
261: "", # SWAN ISLAND
|
||||||
|
262: "\U0001F1F9\U0001F1EF", # TAJIKISTAN
|
||||||
|
263: "\U0001F1F3\U0001F1F1", # NETHERLANDS
|
||||||
|
264: "", # TANGIER
|
||||||
|
265: "", # NORTHERN IRELAND
|
||||||
|
266: "\U0001F1F3\U0001F1F4", # NORWAY
|
||||||
|
267: "", # TERR NEW GUINEA
|
||||||
|
268: "", # TIBET
|
||||||
|
269: "\U0001F1F5\U0001F1F1", # POLAND
|
||||||
|
270: "\U0001F1F9\U0001F1F0", # TOKELAU ISLANDS
|
||||||
|
271: "", # TRIESTE
|
||||||
|
272: "\U0001F1F5\U0001F1F9", # PORTUGAL
|
||||||
|
273: "", # TRINDADE & MARTIM VAZ ISLANDS
|
||||||
|
274: "", # TRISTAN DA CUNHA & GOUGH ISLANDS
|
||||||
|
275: "\U0001F1F7\U0001F1F4", # ROMANIA
|
||||||
|
276: "", # TROMELIN ISLAND
|
||||||
|
277: "", # SAINT PIERRE & MIQUELON
|
||||||
|
278: "\U0001F1F8\U0001F1F2", # SAN MARINO
|
||||||
|
279: "\U0001F3F4\U000E0067\U000E0062\U000E0073\U000E0063\U000E0074\U000E007F", # SCOTLAND
|
||||||
|
280: "\U0001F1F9\U0001F1F2", # TURKMENISTAN
|
||||||
|
281: "\U0001F1EA\U0001F1F8", # SPAIN
|
||||||
|
282: "\U0001F1F9\U0001F1FB", # TUVALU
|
||||||
|
283: "", # UK BASES ON CYPRUS
|
||||||
|
284: "\U0001F1F8\U0001F1EA", # SWEDEN
|
||||||
|
285: "\U0001F1FB\U0001F1EE", # US VIRGIN ISLANDS
|
||||||
|
286: "\U0001F1FA\U0001F1EC", # UGANDA
|
||||||
|
287: "\U0001F1E8\U0001F1ED", # SWITZERLAND
|
||||||
|
288: "\U0001F1FA\U0001F1E6", # UKRAINE
|
||||||
|
289: "", # UNITED NATIONS HQ
|
||||||
|
291: "\U0001F1FA\U0001F1F8", # UNITED STATES OF AMERICA
|
||||||
|
292: "\U0001F1FA\U0001F1FF", # UZBEKISTAN
|
||||||
|
293: "\U0001F1FB\U0001F1F3", # VIET NAM
|
||||||
|
294: "\U0001F3F4\U000E0067\U000E0062\U000E0077\U000E006C\U000E0073\U000E007F", # WALES
|
||||||
|
295: "\U0001F1FB\U0001F1E6", # VATICAN CITY
|
||||||
|
296: "\U0001F1F7\U0001F1F8", # SERBIA
|
||||||
|
297: "", # WAKE ISLAND
|
||||||
|
298: "\U0001F1FC\U0001F1EB", # WALLIS & FUTUNA ISLANDS
|
||||||
|
299: "", # WEST MALAYSIA
|
||||||
|
301: "\U0001F1F0\U0001F1EE", # WESTERN KIRIBATI
|
||||||
|
302: "\U0001F1EA\U0001F1ED", # WESTERN SAHARA
|
||||||
|
303: "", # WILLIS ISLAND
|
||||||
|
304: "\U0001F1E7\U0001F1ED", # BAHRAIN
|
||||||
|
305: "\U0001F1E7\U0001F1E9", # BANGLADESH
|
||||||
|
306: "\U0001F1E7\U0001F1F9", # BHUTAN
|
||||||
|
307: "", # ZANZIBAR
|
||||||
|
308: "\U0001F1E8\U0001F1F7", # COSTA RICA
|
||||||
|
309: "\U0001F1F2\U0001F1F2", # MYANMAR
|
||||||
|
312: "\U0001F1F0\U0001F1ED", # CAMBODIA
|
||||||
|
315: "\U0001F1F1\U0001F1F0", # SRI LANKA
|
||||||
|
318: "\U0001F1E8\U0001F1F3", # CHINA
|
||||||
|
321: "\U0001F1ED\U0001F1F0", # HONG KONG
|
||||||
|
324: "\U0001F1EE\U0001F1F3", # INDIA
|
||||||
|
327: "\U0001F1EE\U0001F1E9", # INDONESIA
|
||||||
|
330: "\U0001F1EE\U0001F1F7", # IRAN
|
||||||
|
333: "\U0001F1EE\U0001F1F6", # IRAQ
|
||||||
|
336: "\U0001F1EE\U0001F1F1", # ISRAEL
|
||||||
|
339: "\U0001F1EF\U0001F1F5", # JAPAN
|
||||||
|
342: "\U0001F1EF\U0001F1F4", # JORDAN
|
||||||
|
344: "\U0001F1F0\U0001F1F5", # DPRK (NORTH KOREA)
|
||||||
|
345: "\U0001F1E7\U0001F1F3", # BRUNEI
|
||||||
|
348: "\U0001F1F0\U0001F1FC", # KUWAIT
|
||||||
|
354: "\U0001F1F1\U0001F1E7", # LEBANON
|
||||||
|
363: "\U0001F1F2\U0001F1F3", # MONGOLIA
|
||||||
|
369: "\U0001F1F3\U0001F1F5", # NEPAL
|
||||||
|
370: "\U0001F1F4\U0001F1F2", # OMAN
|
||||||
|
372: "\U0001F1F5\U0001F1F0", # PAKISTAN
|
||||||
|
375: "\U0001F1F5\U0001F1ED", # PHILIPPINES
|
||||||
|
376: "\U0001F1F6\U0001F1E6", # QATAR
|
||||||
|
378: "\U0001F1F8\U0001F1E6", # SAUDI ARABIA
|
||||||
|
379: "\U0001F1F8\U0001F1E8", # SEYCHELLES ISLANDS
|
||||||
|
381: "\U0001F1F8\U0001F1EC", # SINGAPORE
|
||||||
|
382: "\U0001F1E9\U0001F1EF", # DJIBOUTI
|
||||||
|
384: "\U0001F1F8\U0001F1FE", # SYRIA
|
||||||
|
386: "\U0001F1F9\U0001F1FC", # TAIWAN
|
||||||
|
387: "\U0001F1F9\U0001F1ED", # THAILAND
|
||||||
|
390: "\U0001F1F9\U0001F1F7", # TURKEY
|
||||||
|
391: "\U0001F1E6\U0001F1EA", # UNITED ARAB EMIRATES
|
||||||
|
400: "\U0001F1E9\U0001F1FF", # ALGERIA
|
||||||
|
401: "\U0001F1E6\U0001F1F4", # ANGOLA
|
||||||
|
402: "\U0001F1E7\U0001F1FC", # BOTSWANA
|
||||||
|
404: "\U0001F1E7\U0001F1EE", # BURUNDI
|
||||||
|
406: "\U0001F1E8\U0001F1F2", # CAMEROON
|
||||||
|
408: "\U0001F1E8\U0001F1EB", # CENTRAL AFRICAN REPUBLIC
|
||||||
|
409: "", # CAPE VERDE
|
||||||
|
410: "\U0001F1F9\U0001F1E9", # CHAD
|
||||||
|
411: "\U0001F1F0\U0001F1F2", # COMOROS
|
||||||
|
412: "\U0001F1E8\U0001F1EC", # REPUBLIC OF THE CONGO
|
||||||
|
414: "\U0001F1E8\U0001F1E9", # DEM. REP. OF THE CONGO
|
||||||
|
416: "\U0001F1E7\U0001F1EF", # BENIN
|
||||||
|
420: "\U0001F1EC\U0001F1E6", # GABON
|
||||||
|
422: "\U0001F1EC\U0001F1F2", # THE GAMBIA
|
||||||
|
424: "\U0001F1EC\U0001F1ED", # GHANA
|
||||||
|
428: "\U0001F1E8\U0001F1EE", # COTE DIVOIRE
|
||||||
|
430: "\U0001F1F0\U0001F1EA", # KENYA
|
||||||
|
432: "\U0001F1F1\U0001F1F8", # LESOTHO
|
||||||
|
434: "\U0001F1F1\U0001F1F7", # LIBERIA
|
||||||
|
436: "\U0001F1F1\U0001F1FE", # LIBYA
|
||||||
|
438: "\U0001F1F2\U0001F1EC", # MADAGASCAR
|
||||||
|
440: "\U0001F1F2\U0001F1FC", # MALAWI
|
||||||
|
442: "\U0001F1F2\U0001F1F1", # MALI
|
||||||
|
444: "\U0001F1F2\U0001F1F7", # MAURITANIA
|
||||||
|
446: "\U0001F1F2\U0001F1E6", # MOROCCO
|
||||||
|
450: "\U0001F1F3\U0001F1EC", # NIGERIA
|
||||||
|
452: "\U0001F1FF\U0001F1FC", # ZIMBABWE
|
||||||
|
453: "\U0001F1F7\U0001F1EA", # REUNION ISLAND
|
||||||
|
454: "\U0001F1F7\U0001F1FC", # RWANDA
|
||||||
|
456: "\U0001F1F8\U0001F1F3", # SENEGAL
|
||||||
|
458: "\U0001F1F8\U0001F1F1", # SIERRA LEONE
|
||||||
|
460: "", # ROTUMA
|
||||||
|
462: "\U0001F1FF\U0001F1E6", # REPUBLIC OF SOUTH AFRICA
|
||||||
|
464: "\U0001F1F3\U0001F1E6", # NAMIBIA
|
||||||
|
466: "\U0001F1F8\U0001F1E9", # SUDAN
|
||||||
|
468: "\U0001F1F8\U0001F1FF", # KINGDOM OF ESWATINI
|
||||||
|
470: "\U0001F1F9\U0001F1FF", # TANZANIA
|
||||||
|
474: "\U0001F1F9\U0001F1F3", # TUNISIA
|
||||||
|
478: "\U0001F1EA\U0001F1EC", # EGYPT
|
||||||
|
480: "\U0001F1E7\U0001F1EB", # BURKINA FASO
|
||||||
|
482: "\U0001F1FF\U0001F1F2", # ZAMBIA
|
||||||
|
483: "\U0001F1F9\U0001F1EC", # TOGO
|
||||||
|
488: "", # WALVIS BAY
|
||||||
|
489: "", # CONWAY REEF
|
||||||
|
490: "", # BANABA ISLAND
|
||||||
|
492: "\U0001F1FE\U0001F1EA", # YEMEN
|
||||||
|
493: "", # PENGUIN ISLANDS
|
||||||
|
497: "\U0001F1ED\U0001F1F7", # CROATIA
|
||||||
|
499: "\U0001F1F8\U0001F1EE", # SLOVENIA
|
||||||
|
501: "\U0001F1E7\U0001F1E6", # BOSNIA-HERZEGOVINA
|
||||||
|
502: "\U0001F1F2\U0001F1F0", # NORTH MACEDONIA
|
||||||
|
503: "\U0001F1E8\U0001F1FF", # CZECH REPUBLIC
|
||||||
|
504: "\U0001F1F8\U0001F1F0", # SLOVAK REPUBLIC
|
||||||
|
505: "", # PRATAS ISLAND
|
||||||
|
506: "", # SCARBOROUGH REEF
|
||||||
|
507: "", # TEMOTU PROVINCE
|
||||||
|
508: "", # AUSTRAL ISLANDS
|
||||||
|
509: "", # MARQUESAS ISLANDS
|
||||||
|
510: "\U0001F1F5\U0001F1F8", # PALESTINE
|
||||||
|
511: "\U0001F1F9\U0001F1F1", # TIMOR-LESTE
|
||||||
|
512: "", # CHESTERFIELD ISLANDS
|
||||||
|
513: "", # DUCIE ISLAND
|
||||||
|
514: "\U0001F1F2\U0001F1EA", # MONTENEGRO
|
||||||
|
515: "", # SWAINS ISLAND
|
||||||
|
516: "\U0001F1E7\U0001F1F1", # SAINT BARTHELEMY
|
||||||
|
517: "\U0001F1E8\U0001F1FC", # CURACAO
|
||||||
|
518: "\U0001F1F8\U0001F1FD", # SINT MAARTEN
|
||||||
|
519: "", # SABA & ST EUSTATIUS
|
||||||
|
520: "", # BONAIRE
|
||||||
|
521: "", # REPUBLIC OF SOUTH SUDAN
|
||||||
|
522: "\U0001F1FD\U0001F1F0" # REPUBLIC OF KOSOVO
|
||||||
|
}
|
||||||
58
core/utils.py
Normal file
58
core/utils.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from core.constants import BANDS, UNKNOWN_BAND
|
||||||
|
from pyhamtools import LookupLib, Callinfo
|
||||||
|
|
||||||
|
# Static lookup helpers from pyhamtools
|
||||||
|
# todo in future add QRZ as a second lookup option in case it provides more data?
|
||||||
|
lookuplib = LookupLib(lookuptype="countryfile")
|
||||||
|
callinfo = Callinfo(lookuplib)
|
||||||
|
|
||||||
|
# Infer a "mode family" from a mode.
|
||||||
|
def infer_mode_family_from_mode(mode):
|
||||||
|
if mode.upper() == "CW":
|
||||||
|
return "CW"
|
||||||
|
elif mode.upper() in ["PHONE", "SSB", "USB", "LSB", "AM", "FM", "DMR", "DSTAR", "C4FM", "M17"]:
|
||||||
|
return "PHONE"
|
||||||
|
else:
|
||||||
|
return "DIGI"
|
||||||
|
|
||||||
|
# Infer a band from a frequency in kHz
|
||||||
|
def infer_band_from_freq(freq):
|
||||||
|
for b in BANDS:
|
||||||
|
if b.start_freq <= freq <= b.end_freq:
|
||||||
|
return b
|
||||||
|
return UNKNOWN_BAND
|
||||||
|
|
||||||
|
# Infer a country name from a callsign
|
||||||
|
def infer_country_from_callsign(call):
|
||||||
|
try:
|
||||||
|
return callinfo.get_country_name(call)
|
||||||
|
except KeyError as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Infer a DXCC ID from a callsign
|
||||||
|
def infer_dxcc_id_from_callsign(call):
|
||||||
|
try:
|
||||||
|
return callinfo.get_adif_id(call)
|
||||||
|
except KeyError as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Infer a continent shortcode from a callsign
|
||||||
|
def infer_continent_from_callsign(call):
|
||||||
|
try:
|
||||||
|
return callinfo.get_continent(call)
|
||||||
|
except KeyError as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Infer a CQ zone from a callsign
|
||||||
|
def infer_cq_zone_from_callsign(call):
|
||||||
|
try:
|
||||||
|
return callinfo.get_cqz(call)
|
||||||
|
except KeyError as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Infer a ITU zone from a callsign
|
||||||
|
def infer_itu_zone_from_callsign(call):
|
||||||
|
try:
|
||||||
|
return callinfo.get_ituz(call)
|
||||||
|
except KeyError as e:
|
||||||
|
return None
|
||||||
15
data/band.py
Normal file
15
data/band.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
# Data class that defines a band.
|
||||||
|
@dataclass
|
||||||
|
class Band:
|
||||||
|
# Band name
|
||||||
|
name: str
|
||||||
|
# Start frequency, in kHz
|
||||||
|
start_freq: float
|
||||||
|
# Stop frequency, in kHz
|
||||||
|
end_freq: float
|
||||||
|
# Colour to use for this band, as per PSK Reporter
|
||||||
|
color: str
|
||||||
|
# Contrast colour to use for text against a background of the band colour
|
||||||
|
contrast_color: str
|
||||||
100
data/spot.py
Normal file
100
data/spot.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from core.constants import DXCC_FLAGS
|
||||||
|
from core.utils import infer_mode_family_from_mode, infer_band_from_freq, infer_continent_from_callsign, \
|
||||||
|
infer_country_from_callsign, infer_cq_zone_from_callsign, infer_itu_zone_from_callsign, infer_dxcc_id_from_callsign
|
||||||
|
|
||||||
|
# Data class that defines a spot.
|
||||||
|
@dataclass
|
||||||
|
class Spot:
|
||||||
|
# Callsign of the operator that has been spotted
|
||||||
|
dx_call: str = None
|
||||||
|
# Callsign of the operator that has spotted them
|
||||||
|
de_call: str = None
|
||||||
|
# Country of the DX operator
|
||||||
|
dx_country: str = None
|
||||||
|
# Country of the spotter
|
||||||
|
de_country: str = None
|
||||||
|
# Country flag of the DX operator
|
||||||
|
dx_flag: str = None
|
||||||
|
# Country flag of the spotter
|
||||||
|
de_flag: str = None
|
||||||
|
# Continent of the DX operator
|
||||||
|
dx_continent: str = None
|
||||||
|
# Continent of the spotter
|
||||||
|
de_continent: str = None
|
||||||
|
# DXCC ID of the DX operator
|
||||||
|
dx_dxcc_id: int = None
|
||||||
|
# DXCC ID of the spotter
|
||||||
|
de_dxcc_id: int = None
|
||||||
|
# CQ zone of the DX operator
|
||||||
|
dx_cq_zone: int = None
|
||||||
|
# ITU zone of the DX operator
|
||||||
|
dx_itu_zone: int = None
|
||||||
|
# Reported mode, such as SSB, PHONE, CW, FT8...
|
||||||
|
mode: str = None
|
||||||
|
# Inferred mode "family". One of "CW", "PHONE" or "DIGI".
|
||||||
|
mode_family: str = None
|
||||||
|
# Frequency, in kHz
|
||||||
|
freq: float = None
|
||||||
|
# Band, defined by the frequency, e.g. "40m" or "70cm"
|
||||||
|
band: str = None
|
||||||
|
# Colour to use for the band
|
||||||
|
band_color: str = None
|
||||||
|
# Contrast colour to use for text on a background of band_color
|
||||||
|
band_contrast_color: str = None
|
||||||
|
# Time of the spot
|
||||||
|
time: datetime = None
|
||||||
|
# Comment left by the spotter, if any
|
||||||
|
comment: str = None
|
||||||
|
# Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
|
||||||
|
sig: str = None
|
||||||
|
# SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
|
||||||
|
sig_refs: list = None
|
||||||
|
# SIG reference names
|
||||||
|
sig_refs_names: list = None
|
||||||
|
# Maidenhead grid locator for the spot. This could be from a geographical reference e.g. POTA, or just from the country
|
||||||
|
grid: str = None
|
||||||
|
# Latitude & longitude, in degrees. This could be from a geographical reference e.g. POTA, or just from the country
|
||||||
|
latitude: float = None
|
||||||
|
longitude: float = None
|
||||||
|
# QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
|
||||||
|
qrt: bool = None
|
||||||
|
# Where we got the spot from, e.g. "POTA", "Cluster"...
|
||||||
|
source: str = None
|
||||||
|
# The ID the source gave it, if any.
|
||||||
|
source_id: str = None
|
||||||
|
|
||||||
|
# Infer missing parameters where possible
|
||||||
|
def infer_missing(self):
|
||||||
|
if self.dx_call and not self.dx_country:
|
||||||
|
self.dx_country = infer_country_from_callsign(self.dx_call)
|
||||||
|
if self.dx_call and not self.dx_continent:
|
||||||
|
self.dx_continent = infer_continent_from_callsign(self.dx_call)
|
||||||
|
if self.dx_call and not self.dx_cq_zone:
|
||||||
|
self.dx_cq_zone = infer_cq_zone_from_callsign(self.dx_call)
|
||||||
|
if self.dx_call and not self.dx_itu_zone:
|
||||||
|
self.dx_itu_zone = infer_itu_zone_from_callsign(self.dx_call)
|
||||||
|
if self.dx_call and not self.dx_dxcc_id:
|
||||||
|
self.dx_dxcc_id = infer_dxcc_id_from_callsign(self.dx_call)
|
||||||
|
if self.dx_dxcc_id and not self.dx_flag:
|
||||||
|
self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id]
|
||||||
|
|
||||||
|
if self.de_call and not self.de_country:
|
||||||
|
self.de_country = infer_country_from_callsign(self.de_call)
|
||||||
|
if self.de_call and not self.de_continent:
|
||||||
|
self.de_continent = infer_continent_from_callsign(self.de_call)
|
||||||
|
if self.de_call and not self.de_dxcc_id:
|
||||||
|
self.de_dxcc_id = infer_dxcc_id_from_callsign(self.de_call)
|
||||||
|
if self.de_dxcc_id and not self.de_flag:
|
||||||
|
self.de_flag = DXCC_FLAGS[self.de_dxcc_id]
|
||||||
|
|
||||||
|
if self.freq and not self.band:
|
||||||
|
band = infer_band_from_freq(self.freq)
|
||||||
|
self.band = band.name
|
||||||
|
self.band_color = band.color
|
||||||
|
self.band_contrast_color = band.contrast_color
|
||||||
|
|
||||||
|
if self.mode and not self.mode_family:
|
||||||
|
self.mode_family=infer_mode_family_from_mode(self.mode)
|
||||||
42
main.py
Normal file
42
main.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Main script
|
||||||
|
import signal
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from providers.pota import POTA
|
||||||
|
|
||||||
|
|
||||||
|
# Shutdown function
|
||||||
|
def shutdown(sig, frame):
|
||||||
|
# Start data providers
|
||||||
|
for p in providers: p.stop()
|
||||||
|
|
||||||
|
|
||||||
|
# Main function
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Shut down gracefully on SIGINT
|
||||||
|
signal.signal(signal.SIGINT, shutdown)
|
||||||
|
|
||||||
|
# Create providers
|
||||||
|
providers = [POTA()] # todo all other providers
|
||||||
|
# Set up spot list
|
||||||
|
spot_list = []
|
||||||
|
# Set up data providers
|
||||||
|
for p in providers: p.setup(spot_list=spot_list)
|
||||||
|
# Start data providers
|
||||||
|
for p in providers: p.start()
|
||||||
|
|
||||||
|
# todo thread to clear spot list of old data
|
||||||
|
|
||||||
|
# Todo serve spot API
|
||||||
|
# Todo serve status API
|
||||||
|
# Todo serve apidocs
|
||||||
|
# Todo serve website
|
||||||
|
|
||||||
|
sleep(2)
|
||||||
|
print(len(spot_list))
|
||||||
|
print(spot_list[0])
|
||||||
|
|
||||||
|
|
||||||
|
# NOTES FOR FIELD SPOTTER
|
||||||
|
# Still need to de-dupe spots
|
||||||
|
# Still need to do QSY checking
|
||||||
65
providers/pota.py
Normal file
65
providers/pota.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
import pytz
|
||||||
|
from data.spot import Spot
|
||||||
|
from providers.provider import Provider
|
||||||
|
from threading import Timer
|
||||||
|
import requests
|
||||||
|
|
||||||
|
class POTA(Provider):
|
||||||
|
POLL_INTERVAL_SEC = 120
|
||||||
|
SPOTS_URL = "https://api.pota.app/spot/activator"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.poll_timer = None
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return "POTA"
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.poll()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.poll_timer.cancel()
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
try:
|
||||||
|
# Request data from API
|
||||||
|
source_data = requests.get(self.SPOTS_URL, headers=self.HTTP_HEADERS).json()
|
||||||
|
# Build a list of spots we haven't seen before
|
||||||
|
new_spots = []
|
||||||
|
# Iterate through source data
|
||||||
|
for source_spot in source_data:
|
||||||
|
# Convert to our spot format
|
||||||
|
spot = Spot(source="POTA",
|
||||||
|
source_id=source_spot["spotId"],
|
||||||
|
dx_call=source_spot["activator"],
|
||||||
|
de_call=source_spot["spotter"],
|
||||||
|
freq=float(source_spot["frequency"]),
|
||||||
|
mode=source_spot["mode"],
|
||||||
|
comment=source_spot["comments"],
|
||||||
|
sig="POTA",
|
||||||
|
sig_refs=[source_spot["reference"]],
|
||||||
|
sig_refs_names=[source_spot["name"]],
|
||||||
|
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=pytz.UTC),
|
||||||
|
grid=source_spot["grid6"],
|
||||||
|
latitude=source_spot["latitude"],
|
||||||
|
longitude=source_spot["longitude"],
|
||||||
|
qrt="QRT" in source_spot["comments"].upper())
|
||||||
|
# Fill in any blanks
|
||||||
|
spot.infer_missing()
|
||||||
|
# Add to our list
|
||||||
|
new_spots.append(spot)
|
||||||
|
|
||||||
|
# Submit the new spots for processing
|
||||||
|
self.submit(new_spots)
|
||||||
|
|
||||||
|
self.status = "OK"
|
||||||
|
self.last_update_time = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.status = "Error"
|
||||||
|
|
||||||
|
self.poll_timer = Timer(self.POLL_INTERVAL_SEC, self.poll)
|
||||||
|
self.poll_timer.start()
|
||||||
41
providers/provider.py
Normal file
41
providers/provider.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import pytz
|
||||||
|
from core.constants import SOFTWARE_NAME, SOFTWARE_VERSION
|
||||||
|
|
||||||
|
# Generic data provider class. Subclasses of this query the individual APIs for data.
|
||||||
|
class Provider:
|
||||||
|
|
||||||
|
# HTTP headers used for providers that use HTTP
|
||||||
|
HTTP_HEADERS = { "User-Agent": SOFTWARE_NAME + " " + SOFTWARE_VERSION }
|
||||||
|
|
||||||
|
# Constructor
|
||||||
|
def __init__(self):
|
||||||
|
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
|
||||||
|
self.last_spot_time = datetime.min.replace(tzinfo=pytz.UTC)
|
||||||
|
self.status = "Not Started"
|
||||||
|
self.spot_list = None
|
||||||
|
|
||||||
|
# Return the name of the provider
|
||||||
|
def name(self):
|
||||||
|
raise NotImplementedError("Subclasses must implement this method")
|
||||||
|
|
||||||
|
# Set up the provider, e.g. giving it the spot list to work from
|
||||||
|
def setup(self, spot_list):
|
||||||
|
self.spot_list = spot_list
|
||||||
|
|
||||||
|
# Start the provider. This should return immediately after spawning threads to access the remote resources
|
||||||
|
def start(self):
|
||||||
|
raise NotImplementedError("Subclasses must implement this method")
|
||||||
|
|
||||||
|
# Submit one or more new spots retrieved from the provider. Only spots that are newer than the last spot retrieved
|
||||||
|
# by this provider will be added to the spot list, to prevent duplications. This is called by the subclasses on
|
||||||
|
# receiving spots.
|
||||||
|
def submit(self, spots):
|
||||||
|
for spot in spots:
|
||||||
|
if spot.time > self.last_spot_time:
|
||||||
|
self.spot_list.append(spot)
|
||||||
|
self.last_spot_time = max(map(lambda s: s.time, spots))
|
||||||
|
|
||||||
|
# Stop any threads and prepare for application shutdown
|
||||||
|
def stop(self):
|
||||||
|
raise NotImplementedError("Subclasses must implement this method")
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
requests~=2.32.5
|
||||||
Reference in New Issue
Block a user