added LICENSE

added README
fixed pylint findings
added screenshot
This commit is contained in:
Michael Clemens 2022-05-20 17:12:55 +02:00
parent 116567b979
commit 48d7df5f74
4 changed files with 239 additions and 105 deletions

19
LICENSE.md Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2022 Michael Clemens, DK1MI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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 OR COPYRIGHT HOLDERS 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.

View File

@ -0,0 +1,60 @@
# ColorSpot
This script is a command line DX cluster client. It adds the following benefits to the default telnet stream:
* displays the DX station's country
* displays the DX station's continent
* displays if the DX station uses LotW
* downloads your LotW QSL file and marks all lines with countries that need to be confirmed
* displays lines in different colors depending on the continent or band (user configurable)
# Screnshot
![screenshot](/screenshot.png?raw=true "screenshot")
# Installation
ColorSpot needs Python 3 and the following libraries:
* colored
* requests
* Telnet
* zipfile
Furthermore, you need an account at LotW for some of teh features.
Before installing ColorSpot, please make sure that pip, setuptools and wheel are installed and up-to-date:
```
# python3 -m pip install --upgrade pip setuptools wheel
```
Finally, install ColorSpot with pip:
```
# python3 -m pip install colorspot
```
# Updating
To update colorspot, execute the following command:
```
# python3 -m pip install --upgrade colorspot
```
# Usage
* execute the application with "colorspot"
* colorspot creates a default config file and states its location (e.g. _~/.colorspot.ini_)
* adapt _~/.colorspot.ini_ to your needs. Important setting are:
* cluster/host and cluster/port: Change this if you want to use another cluster server
* cluster/user: Enter here your call sign
* lotw/user: Enter here your lotw user name (your call sign)
* lotw/password: Enter here your lotw password
* lotw/mode: Enter here the mode you would like to filter the QSL download from LotW
* execute the application again with "colorspot"
# License
see ![LICENSE](LICENSE)

View File

@ -1,28 +1,49 @@
#!/usr/bin/env python3
# pylint: disable=W1401,C0303
"""
+---------------------------------------------------------------------+
| |
| ___ _ ___ _ |
| / __|___| |___ _ _/ __|_ __ ___| |_ |
| | (__/ _ \ / _ \ '_\__ \ '_ \/ _ \ _| |
| \___\___/_\___/_| |___/ .__/\___/\__| |
| -= DK1MI =- |_| |
| |
| |
| A colorful cli DX cluster client with an LotW integration |
| |
| Author: Michael Clemens, DK1MI (colorspot@qrz.is) |
| |
| Documentation: Please see the README.md file |
| License: Please see the LICENSE file |
| Repository: https://git.qrz.is/clemens/colorspot |
| |
+---------------------------------------------------------------------+
"""
# Infos on colors: https://pypi.org/project/colored/
# Country list extracted from http://www.arrl.org/files/file/dxcclist.txt
import sys
import csv
import re
import json
import os
import time as bla
from telnetlib import Telnet
from colored import fg, bg, attr
import configparser
from collections import defaultdict
import random
import requests
import os
from os.path import exists
import configparser
import zipfile
from telnetlib import Telnet
import requests
from colored import fg, bg, attr
class ColorSpot():
"""ColorSpot class"""
def __init__(self):
"""initialize things"""
self.version = "0.1.0"
self.print_banner()
@ -37,76 +58,14 @@ class ColorSpot():
self.confirmed_entities = self.get_confirmed_entities()
if self.check_cty:
self.cty = list(csv.reader(open(self.config['files']['cty'], "r"), delimiter=","))
#self.cty = list(csv.reader(open(self.config['files']['cty'], "r"), delimiter=","))
with open(self.config['files']['cty'], encoding='us-ascii') as csvfile:
self.cty = list(csv.reader(csvfile, delimiter=','))
@staticmethod
def rnd_col():
r = lambda: random.randint(0,255)
return'#%02X%02X%02X' % (r(),r(),r())
@staticmethod
def download_file(url, local_filename):
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open(local_filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return local_filename
def check_files(self):
# check for lotw qsl information file
self.check_lotw_confirmed = exists(self.config['files']['lotw_confirmed'])
if not self.check_lotw_confirmed:
print("The file " + self.config['files']['lotw_confirmed'] + " is missing.")
user = self.config['lotw']['user']
password = self.config['lotw']['password']
mode = self.config['lotw']['mode']
url = "https://lotw.arrl.org/lotwuser/lotwreport.adi?login={}&password={}&qso_query=1&qso_qsl=yes&qso_mode={}&qso_qsldetail=yes&qso_qslsince=1970-01-01".format(user, password, mode)
print("Trying to download " + url)
file_name = self.download_file(url, self.config['files']['lotw_confirmed'])
self.check_lotw_confirmed = exists(self.config['files']['lotw_confirmed'])
if self.check_lotw_confirmed:
print("File successfully downloaded")
else:
print("something went wrong while downloading " + url)
# check for cty.csv file
self.check_cty = exists(self.config['files']['cty'])
if not self.check_cty:
url = self.config['files']['cty_url']
print("The file " + self.config['files']['cty'] + " is missing.")
print("Trying to download " + url)
zip_name = self.download_file(url, "bigcty.zip" )
with zipfile.ZipFile(zip_name, 'r') as zip_ref:
zip_ref.extract("cty.csv")
os.remove(zip_name)
self.check_cty = exists(self.config['files']['cty'])
if self.check_cty:
print("File successfully downloaded and extracted.")
else:
print("something went wrong while downloading " + url)
# check for lotw user activity file
self.check_lotw_activity = exists(self.config['files']['lotw_activity'])
if not self.check_lotw_activity:
url = self.config['files']['lotw_activity_url']
print("The file " + self.config['files']['lotw_activity'] + " is missing.")
print("Trying to download " + url)
file_name = self.download_file(url, self.config['files']['lotw_activity'])
self.check_lotw_activity = exists(self.config['files']['lotw_activity'])
if self.check_lotw_activity:
print("File successfully downloaded")
else:
print("something went wrong while downloading " + url)
def print_banner(self):
"""print an awesome banner"""
ver = self.version
# print the banner
print(fg(self.rnd_col())+" ___ _ ___ _ ")
print(fg(self.rnd_col())+" / __|___| |___ _ _/ __|_ __ ___| |_ ")
print(fg(self.rnd_col())+" | (__/ _ \ / _ \ '_\__ \ '_ \/ _ \ _|")
@ -115,6 +74,7 @@ class ColorSpot():
print("")
print(attr('reset'))
@staticmethod
def read_config(config, file_name):
"""reads the configuration from the config file or
@ -136,7 +96,7 @@ class ColorSpot():
'lotw_activity_url': 'https://lotw.arrl.org/lotw-user-activity.csv'}
config['lotw'] = {
'user': 'N0CALL',
'password': 'XXXXXXXXX',
'password': 'CHANGEME',
'mode': 'ssb'}
config['band_colors'] = {
"145": "white",
@ -171,41 +131,119 @@ class ColorSpot():
'alert_fg': 'white',
'default_bg': 'black'}
with open(file_name, 'w') as configfile:
with open(file_name, 'w', encoding='us-ascii') as configfile:
config.write(configfile)
print("\nNo configuration file found. A new configuration file has been created.")
print("\nPlease edit the file " + file_name + " and restart the application.\n" )
sys.exit()
return config
@staticmethod
def rnd_col():
"""generates a random color cod ein hex"""
rand = lambda: random.randint(0,255)
return'#%02X%02X%02X' % (rand(),rand(),rand())
@staticmethod
def download_file(url, local_filename):
"""downloads a file via HTTP and saves it to a defined file"""
with requests.get(url, stream=True) as request:
request.raise_for_status()
with open(local_filename, 'wb') as file:
for chunk in request.iter_content(chunk_size=8192):
file.write(chunk)
return local_filename
def check_files(self):
"""Checks if all necessary files are in the file system.
Downloads all files and unzips them (if necessary)"""
# check for lotw qsl information file
self.check_lotw_confirmed = exists(self.config['files']['lotw_confirmed'])
if not self.check_lotw_confirmed:
print("The file " + self.config['files']['lotw_confirmed'] + " is missing.")
user = self.config['lotw']['user']
password = self.config['lotw']['password']
mode = self.config['lotw']['mode']
url = "https://lotw.arrl.org/lotwuser/lotwreport.adi?login={}&password={}"\
"&qso_query=1&qso_qsl=yes&qso_mode={}&qso_qsldetail=yes&"\
"qso_qslsince=1970-01-01".format(user, password, mode)
print("Trying to download " + url)
self.download_file(url, self.config['files']['lotw_confirmed'])
self.check_lotw_confirmed = exists(self.config['files']['lotw_confirmed'])
if self.check_lotw_confirmed:
print("File successfully downloaded")
else:
print("something went wrong while downloading " + url)
# check for cty.csv file
self.check_cty = exists(self.config['files']['cty'])
if not self.check_cty:
url = self.config['files']['cty_url']
print("The file " + self.config['files']['cty'] + " is missing.")
print("Trying to download " + url)
zip_name = self.download_file(url, "bigcty.zip" )
with zipfile.ZipFile(zip_name, 'r') as zip_ref:
zip_ref.extract("cty.csv")
os.remove(zip_name)
self.check_cty = exists(self.config['files']['cty'])
if self.check_cty:
print("File successfully downloaded and extracted.")
else:
print("something went wrong while downloading " + url)
# check for lotw user activity file
self.check_lotw_activity = exists(self.config['files']['lotw_activity'])
if not self.check_lotw_activity:
url = self.config['files']['lotw_activity_url']
print("The file " + self.config['files']['lotw_activity'] + " is missing.")
print("Trying to download " + url)
self.download_file(url, self.config['files']['lotw_activity'])
self.check_lotw_activity = exists(self.config['files']['lotw_activity'])
if self.check_lotw_activity:
print("File successfully downloaded")
else:
print("something went wrong while downloading " + url)
def get_confirmed_entities(self):
"""Reads the file downlaoded from LotW with all confirmed QSOs,
extracts all confirmed DXCCs and puts them into a list"""
ret = []
#TODO: download file and/or tell user what to do
file = open(self.config['files']['lotw_confirmed'], "r")
for row in file:
if re.search("<DXCC:", row):
dxcc = row.partition(">")[2].lower().rstrip()
if dxcc not in ret:
ret.append(dxcc)
with open(self.config['files']['lotw_confirmed'], encoding='us-ascii') as file:
for row in file:
if re.search("<DXCC:", row):
dxcc = row.partition(">")[2].lower().rstrip()
if dxcc not in ret:
ret.append(dxcc)
return ret
def check_lotw(self, call):
"""Reads the LotW user activity file and returns the date
of the last upload date if a specific call sign"""
ret = ""
#TODO: download file and/or tell user what to do
csv_file = csv.reader(open(self.config['files']['lotw_activity'], "r"), delimiter=",")
#loop through the csv file
for row in csv_file:
if call == row[0]:
ret = row[1]
return ret
#csv_file = csv.reader(open(self.config['files']['lotw_activity'], "r"), delimiter=",")
with open(self.config['files']['lotw_activity'], encoding='us-ascii') as csvfile:
csv_file = csv.reader(csvfile, delimiter=',')
#loop through the csv file
for row in csv_file:
if call == row[0]:
ret = row[1]
return ret
return ret
def get_cty_row(self, call):
"""Parses all CTY records, tries to find the DXCC entity of a
specific call sign and returns the line as a list of strings"""
done = False
while not done:
for row in self.cty:
entities = row[9].replace(";", "").replace("=", "").split(" ")
# TODO: schauen ob = davor und match -> als special call anzeigen
# TODO: Check if it is a speciall call (=) and mark it in the list
for prefix in entities:
if call == prefix:
return row
@ -216,14 +254,19 @@ class ColorSpot():
def get_spots(self):
"""Connects to the specified telnet dx cluster, performs a login, grabs the
output row by row, enriches it with data and colorizes it depending on certain
paramaeters, e.g. by band or continent."""
with Telnet(self.config['cluster']['host'], int(self.config['cluster']['port']), \
int(self.config['cluster']['timeout'])) as telnet:
while True:
line_enc = telnet.read_until(b"\n") # Read one line
line = line_enc.decode('ascii')
# Enters the call sign if requested
if "enter your call" in line:
b_user = str.encode(self.config['cluster']['user']+"\n")
telnet.write(b_user)
# Detects the beginning of the stream and generates a header line
elif " Hello " in line:
print(fg("grey_27") + line + attr("reset"))
foreground = "white"
@ -231,20 +274,25 @@ class ColorSpot():
sep = fg("grey_27")+'|'+fg(foreground)
row = ["DE", sep, "Freq", sep, "DX", \
sep, "Country", sep, "C", sep, "L", sep, "Comment", sep, "Time"]
print(bg(background) + fg(foreground) + \
'{:9.9} {:<1} {:>7.7} {:<1} {:<10.10} {:<1} {:<16.16} {:<1} {:<2.2} {:<1} {:<1.1} {:<1} {:<30.30} {:<1} {:<4.4}'.format(*row) + attr("reset"))
'{:9.9} {:<1} {:>7.7} {:<1} {:<10.10} {:<1} '\
'{:<16.16} {:<1} {:<2.2} {:<1} {:<1.1} {:<1} {:<30.30} '\
'{:<1} {:<4.4}'.format(*row) + attr("reset"))
b_cmd = str.encode("sh/dx/50 @\n")
telnet.write(b_cmd)
# This is true for every line representing a spot
elif "DX de" in line or "Dx de" in line:
try:
band_col = ""
# Extract all necessary fields from the line and store them
# into different variables.
call_de = re.search('D(X|x) de (.+?): ', line).group(2)
freq = re.search(': +(.+?) ', line).group(1)
call_dx = re.search(freq + ' +(.+?) ', line).group(1)
time = re.search('[^ ]*$', line).group(0)[0:4]
comment = re.search(call_dx + ' +(.+?) +' + time, line).group(1)
# If the CTY file is available, further information will be
# gathered from it, e.g. continent, country, dxcc ID
if self.check_cty:
cty_details = self.get_cty_row(call_dx)
else:
@ -253,11 +301,16 @@ class ColorSpot():
areaname = cty_details[1]
continent = cty_details[3]
# If the LotW user activity file is available and the call
# sign in question is actually a LotW user, a checkmark is
# displayed in a dedicated column of the output
if self.check_lotw_activity and self.check_lotw(call_dx):
lotw = ""
else:
lotw = ""
# Depending on the user's choice, the row will be color coded
# depending on the band or the DX station's continent
try:
if self.config['colors']['color_by'] == "band":
foreground = self.config['band_colors'][freq[:-5]]
@ -268,29 +321,34 @@ class ColorSpot():
except Exception:
foreground = "white"
# Removes the trailing .0 from a frequency for better readability
freq = freq.replace('.0', '')
if self.check_lotw_confirmed and cty_details[2] not in self.confirmed_entities:
# If the DX station's entity hasn't been worked/confirmed via
# LotW yet, the row's background will be color coded.
if self.check_lotw_confirmed and \
cty_details[2] not in self.confirmed_entities:
background = self.config['colors']['alert_bg']
foreground = self.config['colors']['alert_fg']
else:
background = self.config['colors']['default_bg']
# color of the table separator
sep = fg("grey_27")+'|'+fg(foreground)
# Contructs the row that will be printed
row = [call_de, sep, freq, sep, call_dx, \
sep, areaname, sep, continent, sep, lotw, sep, comment, sep, time]
print(bg(background) + fg(foreground) + \
'{:9.9} {:<1} {:>7.7} {:<1} {:<10.10} {:<1} {:<16.16} {:<1} {:<2.2} {:<1} {:<1.1} {:<1} {:<30.30} {:<1} {:<4.4}'.format(*row) + attr("reset"))
'{:9.9} {:<1} {:>7.7} {:<1} {:<10.10} {:<1} '\
'{:<16.16} {:<1} {:<2.2} {:<1} {:<1.1} {:<1} {:<30.30} '\
'{:<1} {:<4.4}'.format(*row) + attr("reset"))
except AttributeError:
print(line)
#####################################################
# Main Routine #
#####################################################
def main():
"""main routine"""
try:
color_spot = ColorSpot()
color_spot.get_spots()
@ -302,6 +360,3 @@ if __name__ == "__main__":
sys.exit(main())
except EOFError:
pass

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB