mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2025-10-27 00:39:26 +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