Compare commits

...

10 commits

Author SHA1 Message Date
Ed Braaten
c5c0081976
Removing hoip_subscribers.xlsx. 2026-01-26 05:46:44 -08:00
Ed Braaten
e4792d157e
Adding .gitignore to ignore *.xlsx files. 2026-01-26 05:45:23 -08:00
Ed Braaten
2bc38e42ac
Added a shell.nix file to project directory. Run
nix-shell and this will create the necessary Python
3.12 environment needed to run the python script on
a NixOS (25.11) system.
2026-01-26 05:42:53 -08:00
Ed Braaten
b0e92b7647
Updated path to reflect new syncthing directory hierarchy. 2026-01-18 20:45:07 -08:00
Ed Braaten
0a8de6215a
Adding shell.nix so we can run python under NixOS for
this project.
2026-01-12 06:03:59 -08:00
Ed Braaten
84bc0ec7db
Added #HOIP to footer of weekly report. 2025-11-23 20:32:55 -08:00
Ed Braaten
f75cc6f475
Updated script to handle HOIP (replacement for Hamshack Hotline). 2025-11-22 21:51:19 -08:00
Ed Braaten
ac8ecbdda4
Initial code to generate HOIP Subscriber list - replacement for
Hamshack Hotline.
2025-11-22 21:06:17 -08:00
Ed Braaten
334b5f22eb
Fixed missing space in subject line, added HOIP contact info. 2025-11-02 19:59:12 -08:00
Ed Braaten
cc56f81cc1
Cleaned up block text for header & footer of the report
and added hash-tags for Nostr Note.
2025-04-29 07:01:37 -07:00
5 changed files with 6343 additions and 45 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# ignore our binary Excel file
*.xlsx

View file

@ -22,7 +22,7 @@ pd.set_option("display.max_rows", 150)
# point to our various input files - adjust this to your environment # point to our various input files - adjust this to your environment
data_dir = '/home/ed/syncthing/ham-radio/ARES-RACES/Nets--PNW_ARES_DMR_Weekly_Net' data_dir = '/home/syncthing/ham-radio/ARES-RACES/Nets--PNW_ARES_DMR_Weekly_Net'
filename = 'Check_In_Data.xlsx' filename = 'Check_In_Data.xlsx'
data_file = os.path.join(data_dir, filename) data_file = os.path.join(data_dir, filename)
output_file = os.path.join(data_dir, "PNW_Digital_ARES_EMCOMM_Weekly_Net_Check_In_Form.txt") output_file = os.path.join(data_dir, "PNW_Digital_ARES_EMCOMM_Weekly_Net_Check_In_Form.txt")
@ -88,18 +88,18 @@ checkins_df["Date"] = pd.to_datetime(checkins_df["Date"]).dt.strftime('%Y-%m-%d'
callinfo_df = pd.read_excel(xls, "Call_Data", dtype=str) callinfo_df = pd.read_excel(xls, "Call_Data", dtype=str)
#print("callinfo_df:") #print("callinfo_df:")
#print(callinfo_df) #print(callinfo_df)
hh_dir_df = pd.read_excel(xls, "Hamshack_Hotline", dtype=str) hoip_dir_df = pd.read_excel(xls, "hoip_subscribers", dtype=str)
#print("hh_dir_df:") #print("hoip_dir_df:")
#print(hh_dir_df) #print(hoip_dir_df)
# build dictionary of HH phone numbers # build dictionary of HH phone numbers
hh_phone_dict = {"Callsign" : "HH Number"} hoip_phone_dict = {"Callsign" : "HH Number"}
for i,row in hh_dir_df.iterrows(): for i,row in hoip_dir_df.iterrows():
callsign = row.iloc[1] callsign = row.iloc[1]
hh_num = row.iloc[7] hoip_num = row.iloc[0]
# we only add the hh number first seen for a unique callsign # we only add the hoip number first seen for a unique callsign
if callsign not in hh_phone_dict.keys(): if callsign not in hoip_phone_dict.keys():
hh_phone_dict.update({callsign : hh_num}) hoip_phone_dict.update({callsign : hoip_num})
# fill in any missing data (NAN's) with default text # fill in any missing data (NAN's) with default text
callinfo_df.fillna({'Name' : ''}, inplace=True) callinfo_df.fillna({'Name' : ''}, inplace=True)
@ -127,27 +127,27 @@ for i,row in checkins_df.iterrows():
# build dictionary of call info indexed by callsign # build dictionary of call info indexed by callsign
call_data_dict = {} call_data_dict = {}
for i,row in callinfo_df.iterrows(): for i,row in callinfo_df.iterrows():
# row of data is call, name, state, district, county, affiliation, hh_num # row of data is call, name, state, district, county, affiliation, hoip_num
# dictionary becomes: { call: [name[0], state[1], district[2], county[3], affiliation[4]], hh_num[5]} # dictionary becomes: { call: [name[0], state[1], district[2], county[3], affiliation[4]], hoip_num[5]}
callsign = row.iloc[0] callsign = row.iloc[0]
if callsign in hh_phone_dict.keys(): if callsign in hoip_phone_dict.keys():
hh_num = hh_phone_dict[callsign] hoip_num = hoip_phone_dict[callsign]
else: else:
hh_num = "" hoip_num = ""
call_data_dict.update({callsign:[row.iloc[1],row.iloc[2],row.iloc[3],row.iloc[4],row.iloc[5],hh_num]}) call_data_dict.update({callsign:[row.iloc[1],row.iloc[2],row.iloc[3],row.iloc[4],row.iloc[5],hoip_num]})
# build checkin_form_dict dictionary # build checkin_form_dict dictionary
checkin_form_dict = blank_checkin_form_dict checkin_form_dict = blank_checkin_form_dict
for call in call_data_dict.keys(): for call in call_data_dict.keys():
#print("Looking at:", call) #print("Looking at:", call)
#if call in hh_phone_dict.keys(): #if call in hoip_phone_dict.keys():
#print("Call "+call+" has HH VOIP #"+hh_phone_dict[call]) #print("Call "+call+" has HOIP #"+hoip_phone_dict[call])
call_state = call_data_dict[call][1] call_state = call_data_dict[call][1]
call_dist = call_data_dict[call][2] call_dist = call_data_dict[call][2]
call_county = call_data_dict[call][3] call_county = call_data_dict[call][3]
call_affil = call_data_dict[call][4] call_affil = call_data_dict[call][4]
call_hh_num = call_data_dict[call][5] call_hoip_num = call_data_dict[call][5]
#print("State, Dist, Cnty, Affil, HH Num: ", call_state, call_dist, call_county, call_affil, call_hh_num) #print("State, Dist, Cnty, Affil, HH Num: ", call_state, call_dist, call_county, call_affil, call_hoip_num)
# sanity check the data - error if not found in dictionary keys... # sanity check the data - error if not found in dictionary keys...
# print("Verifying at: ", call, call_state, call_dist, call_county) # print("Verifying at: ", call, call_state, call_dist, call_county)
@ -218,8 +218,8 @@ with open(output_file,"w") as outfile:
checkin_str = "" checkin_str = ""
if checkin_count > 0: if checkin_count > 0:
outfile.write(" "+call+", "+call_name+checkin_str) outfile.write(" "+call+", "+call_name+checkin_str)
if call in hh_phone_dict.keys(): if call in hoip_phone_dict.keys():
outfile.write(", HH VOIP #"+hh_phone_dict[call]) outfile.write(", HOIP #"+hoip_phone_dict[call])
if call_affil != "": if call_affil != "":
outfile.write(", "+call_affil+"\n") outfile.write(", "+call_affil+"\n")
else: else:
@ -237,8 +237,8 @@ with open(output_file,"w") as outfile:
checkin_str = "" checkin_str = ""
if checkin_count > 0: if checkin_count > 0:
outfile.write(" "+call+", "+call_data_dict[call][0]+checkin_str) outfile.write(" "+call+", "+call_data_dict[call][0]+checkin_str)
if call in hh_phone_dict.keys(): if call in hoip_phone_dict.keys():
outfile.write(", HH VOIP #"+hh_phone_dict[call]) outfile.write(", HOIP #"+hoip_phone_dict[call])
affil = call_data_dict[call][4] affil = call_data_dict[call][4]
if affil != '': if affil != '':
outfile.write(", "+affil+"\n") outfile.write(", "+affil+"\n")
@ -263,8 +263,8 @@ with open(output_file,"w") as outfile:
checkin_count = 0 checkin_count = 0
checkin_str = "" checkin_str = ""
outfile.write(" "+call+", "+call_data_dict[call][0]+checkin_str) outfile.write(" "+call+", "+call_data_dict[call][0]+checkin_str)
if call in hh_phone_dict.keys(): if call in hoip_phone_dict.keys():
outfile.write(", HH VOIP #"+hh_phone_dict[call]) outfile.write(", HOIP #"+hoip_phone_dict[call])
affil = call_data_dict[call][4] affil = call_data_dict[call][4]
if affil != '': if affil != '':
outfile.write(", "+affil+"\n") outfile.write(", "+affil+"\n")
@ -337,14 +337,21 @@ for net_day in net_day_list:
with open(output_file,"w") as outfile: with open(output_file,"w") as outfile:
# Opening text # Opening text
outfile.write("\n ") headertext = '\n'.join([
outfile.write("PNW Digital ARES & EMCOMM Check-In Net - "+str(num_checkins)+" check-ins on "+net_day+"\n") 'PNW Digital ARES & EmComm Check-In Net - '+str(num_checkins)+
outfile.write("\n") ' check-ins on '+net_day+'\n',
outfile.write("We had a total of "+str(num_checkins)+" check-ins on "+net_day+" to the Pacific Northwest (PNW)\n") 'There were '+str(num_checkins)+' check-ins on '+net_day+' to the Pacific',
outfile.write("Digital ARES & EMCOMM Check-In Net. Below is the detailed check-in list grouped\n") 'Northwest (PNW) Digital ARES & EmComm Check-In Net.',
outfile.write("by ARRL ARES districts. If any of the info (i.e. name or agency affiliation) below is\n") ' ',
outfile.write("incomplete or incorrect, please IM me on Jabber/XMPP (ed@n7ekb.net), or\n") 'The following is the detailed check-in list grouped',
outfile.write("e-mail \"ed@n7ekb.net\" with the correction(s).\n\n\n") 'by our region\'s ARRL ARES districts. If any of the',
'info (i.e. name or agency affiliation) below is',
'incomplete or incorrect, please IM me on Jabber/',
'XMPP at n7ekb@n7ekb.net, e-mail me at ed@n7ekb.net, or',
'call me on HOIP extension 103675 with your updates.',
'\n\n'
])
outfile.write(headertext)
# Check-in details # Check-in details
for state in sorted(report_dict): for state in sorted(report_dict):
@ -360,8 +367,10 @@ for net_day in net_day_list:
cur_call_list.sort() cur_call_list.sort()
for call in cur_call_list: for call in cur_call_list:
affil = call_data_dict[call][4] affil = call_data_dict[call][4]
hh_num = call_data_dict[call][5] hoip_num = call_data_dict[call][5]
outfile.write(" "+call+", "+call_data_dict[call][0]) outfile.write(" "+call+", "+call_data_dict[call][0])
if hoip_num != '':
outfile.write(", HOIP #"+hoip_num)
if affil != '': if affil != '':
outfile.write(", "+affil+"\n") outfile.write(", "+affil+"\n")
else: else:
@ -373,8 +382,10 @@ for net_day in net_day_list:
cur_call_list.sort() cur_call_list.sort()
for call in cur_call_list: for call in cur_call_list:
affil = call_data_dict[call][4] affil = call_data_dict[call][4]
hh_num = call_data_dict[call][5] hoip_num = call_data_dict[call][5]
outfile.write(" "+call+", "+call_data_dict[call][0]) outfile.write(" "+call+", "+call_data_dict[call][0])
if hoip_num != '':
outfile.write(", HOIP #"+hoip_num)
if affil != '': if affil != '':
outfile.write(", "+affil+"\n") outfile.write(", "+affil+"\n")
else: else:
@ -390,8 +401,10 @@ for net_day in net_day_list:
cur_call_list.sort() cur_call_list.sort()
for call in cur_call_list: for call in cur_call_list:
affil = call_data_dict[call][4] affil = call_data_dict[call][4]
hh_num = call_data_dict[call][5] hoip_num = call_data_dict[call][5]
outfile.write(" "+call+", "+call_data_dict[call][0]) outfile.write(" "+call+", "+call_data_dict[call][0])
if hoip_num != '':
outfile.write(", HOIP #"+hoip_num)
if affil != '': if affil != '':
outfile.write(", "+affil+"\n") outfile.write(", "+affil+"\n")
else: else:
@ -400,14 +413,24 @@ for net_day in net_day_list:
outfile.write("\n") outfile.write("\n")
# output the footer text... # output the footer text...
outfile.write("\nAbout the net:\n\n") footer = '\n'.join([
outfile.write("The PNW Digital ARES & EMCOMM Check-In Net is held every Sunday evening at 7:30 PM local\n") 'About the net:\n',
outfile.write("time on PNW Regional, DMR talk group 31771 (available on both the PNW Digital http://pnwdigital.net\n") 'The PNW Digital ARES & EMCOMM Check-In Net is held',
outfile.write("and Brandmeister DMR networks). Anyone interested in Amateur Radio Emergency Communications is\n") 'every Sunday evening at 7:30 PM local time on PNW',
outfile.write("welcome to check-in. The net is an opportunity for DMR-capable ARES and EMCOMM hams to exercise\n") 'Regional, DMR talkgroup 31771. DMR Talk Group 31771 is',
outfile.write("their DMR equipment in a regional directed net. The net demonstrates the wide coverage area and\n") 'available on both the PNW Digital and the Brandmeister',
outfile.write("capability of DMR repeaters and hot spots in our Pacific Northwest region. It also highlights the\n") 'DMR networks. Anyone interested in Amateur Radio and',
outfile.write("wide range of EMCOMM-related organizations who have members with DMR capability.\n\n") 'Emergency Communications is welcome to check-in. The',
'net is an opportunity for DMR-capable ARES and EmComm',
'hams to exercise their DMR equipment in a regional,',
'directed net. The net demonstrates the wide coverage',
'area and capability of DMR repeaters and hot spots in',
'our Pacific Northwest region. It also highlights the',
'wide range of EmComm-related organizations who have',
'members with DMR capability.\n\n',
'#net-reports #hamradio #EmComm #ARES #DMR #HOIP #PNWDigital\n'
])
outfile.write(footer)
# close this week's report file # close this week's report file
outfile.close() outfile.close()

6203
hoip_subscribers.csv Normal file

File diff suppressed because it is too large Load diff

56
hoipdownload.py Normal file
View file

@ -0,0 +1,56 @@
from ldap3 import Server, Connection, ALL, SUBTREE
import pandas as pd
# LDAP connection details from the wiki
server_url = 'ldap://207.246.98.219:389' # Server and port (no TLS, as per "TLS Mode: LDAP or No" use 'ldaps://' if TLS needed)
search_base = 'ou=people,dc=hamsoverip,dc=com'
search_filter = '(telephoneNumber=*)' # Fetch all entries with an extension; adjust if needed, e.g., '(objectClass=person)'
attributes = ['cn', 'sn', 'telephoneNumber'] # Key attributes; add more if you discover others like 'callsign'
# Connect anonymously (no auth)
server = Server(server_url, get_info=ALL)
conn = Connection(server, auto_bind=True) # No user/password
# Perform the search
conn.search(
search_base=search_base,
search_filter=search_filter,
search_scope=SUBTREE,
attributes=attributes
)
# Extract data
data = []
for entry in conn.entries:
extension = entry.telephoneNumber.value if 'telephoneNumber' in entry else ''
cn_value = entry.cn.value if 'cn' in entry else ''
sn_value = entry.sn.value if 'sn' in entry else ''
# clean up data
hoipnumber = extension.partition(' ')[0]
callsign = cn_value.partition(' ')[0]
hamname = sn_value.partition(' ')[0]
if hoipnumber: # Only include entries with extensions
data.append({
'HOIP Extension': hoipnumber,
'Call': callsign,
'Name': hamname # Include raw for reference
})
# Save to CSV
if data:
df = pd.DataFrame(data)
# CSV output for debugging...
df.to_csv('hoip_subscribers.csv', index=False)
print(f'Saved {len(data)} entries to hoip_subscribers.csv')
# Excel output for production...
df.to_excel('hoip_subscribers.xlsx', index=False)
print(f'Saved {len(data)} entries to hoip_subscribers.xlsx')
else:
print('No entries found check filter or connection.')
# Unbind
conn.unbind()

14
shell.nix Normal file
View file

@ -0,0 +1,14 @@
# shell.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [
(pkgs.python3.withPackages (ps: with ps; [
numpy
openpyxl
pandas
python-dateutil
]))
];
}