From b57f70183ee3c8f4b1d6058af2dfa8c53fed56cb Mon Sep 17 00:00:00 2001 From: n2qzshce <67349049+n2qzshce@users.noreply.github.com> Date: Fri, 2 Apr 2021 20:13:23 -0600 Subject: [PATCH] Validation version alerts rownums (#25) * Digital contact validation now working. * increment semver. * Migrations removing column 'number' * We are not actually ignoring the 'number' column. * Moved all sizes to use dp for screen scaling. * adding workflow dispatch run option. * Syntax fix for workflow dispatch * Syntax fix for workflow dispatch * Not sure why it's complaining about line 18 * Still complaining about line 18 * Removing unused conditional * d710 now passing spec. * Verified chirp and d710 passing spec. * Fixed an issue with channel numbers in dmr_ids. Still more work to do. * Removed more dictionary references. * Added d878 test. * trim whitespace. * Successfully removed 'number' column. * Successfully removed 'number' column. * Fixed failing unit test for d878 * Syntax env issue. * Moving echo command to different section. * Had the wrong string in the version file, that's why it wasn't updating. * Let's try updating this again. * Try putting it in run tests. * only concerned with pc at the moment. * Version update check? * woops it's an environment variable. * Bad character. * Version echo in its own place. * File pathing tweak. * Are we even rewriting the file? * Added version to title bar. * Optimize imports, add version output. * Updating steps so we can get a passing build. * Try piping output. * Set version back to development. * Forgot out-file encoding. * Let's see if we can get mac and pc working. * Updated syntax to fix error. * local yml complains about syntax. * Still not happy about syntax. * not happy with double quotes. * Fixed op performing check. --- .github/workflows/pyinstaller-build.yml | 7 +- radio_sync.py | 30 ++- src/ham/migration/migration_manager.py | 92 +++++++ src/ham/radio/chirp/radio_channel_chirp.py | 3 +- src/ham/radio/cs800/dmr_contact_cs800.py | 13 +- src/ham/radio/cs800/dmr_user_cs800.py | 11 +- src/ham/radio/cs800/radio_additional_cs800.py | 16 +- src/ham/radio/cs800/radio_channel_cs800.py | 7 +- src/ham/radio/d710/radio_additional_d710.py | 7 +- src/ham/radio/d710/radio_channel_d710.py | 5 +- src/ham/radio/d878/dmr_contact_d878.py | 14 +- src/ham/radio/d878/dmr_id_d878.py | 7 +- src/ham/radio/d878/dmr_user_d878.py | 4 +- src/ham/radio/d878/radio_additional_d878.py | 13 +- src/ham/radio/d878/radio_channel_d878.py | 7 +- .../default_radio/dmr_contact_default.py | 8 +- src/ham/radio/default_radio/dmr_id_default.py | 4 +- .../radio/default_radio/dmr_user_default.py | 8 +- .../default_radio/radio_additional_default.py | 11 +- .../default_radio/radio_channel_default.py | 6 +- src/ham/radio/dmr_contact.py | 6 +- src/ham/radio/dmr_id.py | 4 +- src/ham/radio/dmr_user.py | 5 +- src/ham/radio/ftm400/radio_channel_ftm400.py | 3 +- src/ham/radio/radio_casted_builder.py | 8 +- src/ham/radio/radio_channel.py | 4 +- src/ham/radio_generator.py | 40 ++- src/ham/util/validator.py | 20 +- src/ham/wizard.py | 13 +- src/radio_sync_version.py | 1 + src/ui/app_window.py | 36 ++- src/ui/async_wrapper.py | 6 + test/radio/base_radio_test_setup.py | 21 ++ test/radio/test_chirp.py | 175 ++++++++++++ test/radio/test_d710.py | 14 +- test/radio/test_d878.py | 251 ++++++++++++++++++ test/test_migration_manager.py | 14 + test/test_validator.py | 98 +++++-- test/test_version.py | 13 + 39 files changed, 838 insertions(+), 167 deletions(-) create mode 100644 src/radio_sync_version.py create mode 100644 test/radio/base_radio_test_setup.py create mode 100644 test/radio/test_chirp.py create mode 100644 test/radio/test_d878.py create mode 100644 test/test_version.py diff --git a/.github/workflows/pyinstaller-build.yml b/.github/workflows/pyinstaller-build.yml index a965da6..ef265ac 100644 --- a/.github/workflows/pyinstaller-build.yml +++ b/.github/workflows/pyinstaller-build.yml @@ -4,7 +4,7 @@ name: Build executeable env: - semver: 1.6.0.${{ github.run_number }} + semver: 1.6.1.${{ github.run_number }} python-version: 3.8 KIVY_GL_BACKEND: 'angle_sdl2' @@ -27,10 +27,12 @@ jobs: exe-extension: .exe short-name: win move-command: move + version-command: ${{ '|' }} out-file -encoding utf-8 ./src/radio_sync_version.py - os: macos-10.15 exe-extension: .app short-name: osx move-command: mv + version-command: ${{ '>' }} ./src/radio_sync_version.py steps: - uses: actions/checkout@v2 - name: Set up Python ${{ env.python-version }} @@ -41,6 +43,9 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -r requirements.txt + - name: Write version file + run: | + echo "version = '${{ env.semver }}'" ${{ matrix.version-command }} - name: Run Tests run: | python -m unittest discover -v diff --git a/radio_sync.py b/radio_sync.py index 6616002..393cc53 100644 --- a/radio_sync.py +++ b/radio_sync.py @@ -4,6 +4,7 @@ import sys import src.ham.util.radio_types +from src import radio_sync_version from src.ham.migration.migration_manager import MigrationManager from src.ham.radio_generator import RadioGenerator from src.ham.wizard import Wizard @@ -32,6 +33,14 @@ def main(): help='Destroys `in` and `out` directories along with all their contents.', ) + parser.add_argument( + '--migrate-check', + action='store_true', + default=False, + required=False, + help='Checks for outdated columns.', + ) + parser.add_argument( '--migrate', '-m', action='store_true', @@ -41,7 +50,7 @@ def main(): ) parser.add_argument( - '--migrate_cleanup', + '--migrate-cleanup', action='store_true', default=False, required=False, @@ -71,6 +80,14 @@ def main(): help=f"""Target radios to create.""" ) + parser.add_argument( + '--version', + action='store_true', + default=False, + required=False, + help='Display app version.', + ) + parser.add_argument( '--debug', action='store_true', @@ -85,6 +102,7 @@ def main(): if arg_values.debug: logger.setLevel(logging.DEBUG) + logging.debug("Logging level set to debug.") if arg_values.force: logging.warning("FORCE HAS BEEN SET. ALL PROMPTS WILL DEFAULT YES. Files may be destroyed.") @@ -111,6 +129,12 @@ def main(): wizard.bootstrap(arg_values.force) op_performed = True + if arg_values.migrate_check: + logging.info("Running migration check") + migrations = MigrationManager() + migrations.log_check_migrations() + op_performed = True + if arg_values.migrate: logging.info("Running migration") migrations = MigrationManager() @@ -123,6 +147,10 @@ def main(): migrations.remove_backups() op_performed = True + if arg_values.version: + logging.info(f"App version {src.radio_sync_version.version}") + op_performed = True + if len(arg_values.radios) > 0: logging.info("Running radio generator.") radio_generator = RadioGenerator(arg_values.radios) diff --git a/src/ham/migration/migration_manager.py b/src/ham/migration/migration_manager.py index e0fd021..5eb7261 100644 --- a/src/ham/migration/migration_manager.py +++ b/src/ham/migration/migration_manager.py @@ -4,6 +4,9 @@ import re import shutil +from src.ham.radio.default_radio.dmr_contact_default import DmrContactDefault +from src.ham.radio.default_radio.radio_channel_default import RadioChannelDefault +from src.ham.radio.default_radio.radio_zone_default import RadioZoneDefault from src.ham.util.file_util import FileUtil @@ -48,6 +51,32 @@ def _add_col(self, file_name, col_name, default_val): os.rename(f'{file_name}.tmp', f'{file_name}') return + def _delete_col(self, file_name, col_name): + logging.info(f'Deleting column `{col_name}` in `{file_name}`') + reader = FileUtil.open_file(f'{file_name}', 'r') + cols = reader.readline().replace('\n', '').split(',') + if cols == ['']: + cols = [] + if col_name in cols: + cols.remove(col_name) + reader.seek(0) + + writer = FileUtil.open_file(f'{file_name}.tmp', 'w+') + dict_writer = csv.DictWriter(writer, fieldnames=cols, dialect='unix', quoting=0) + dict_reader = csv.DictReader(reader, fieldnames=cols) + + dict_writer.writeheader() + for row in dict_reader: + if dict_reader.line_num == 1: + continue + dict_writer.writerow(row) + + reader.close() + writer.close() + os.remove(file_name) + os.rename(f'{file_name}.tmp', f'{file_name}') + return + def remove_backups(self): if not os.path.exists('in/'): return @@ -58,6 +87,61 @@ def remove_backups(self): os.remove(f'in/{file_name}') return + def check_migrations_needed(self): + not_needed_cols = dict() + + channels_file = 'in/input.csv' + channel_cols = dict(RadioChannelDefault.create_empty().__dict__) + extra_cols = self._migration_check(channels_file, channel_cols) + + if len(extra_cols) > 0: + not_needed_cols['input.csv'] = extra_cols + + contacts_file = 'in/digital_contacts.csv' + contact_cols = dict(DmrContactDefault.create_empty().__dict__) + extra_cols = self._migration_check(contacts_file, contact_cols) + + if len(extra_cols) > 0: + not_needed_cols['digital_contacts.csv'] = extra_cols + + zones_file = 'in/zones.csv' + zone_cols = dict(RadioZoneDefault.create_empty().__dict__) + extra_cols = self._migration_check(zones_file, zone_cols) + + if len(extra_cols) > 0: + not_needed_cols['zones.csv'] = extra_cols + + return not_needed_cols + + def log_check_migrations(self): + try: + results = self.check_migrations_needed() + except Exception: + logging.info("Migrations check could not be run. Have you run the setup wizard?") + return + + output_str = "" + for k in results.keys(): + for v in results[k]: + output_str += f"{k:20s} | {v}\n" + if len(output_str) != 0: + logging.info(f"The following extra columns were found: \n{'file name':20s} | extra column\n{output_str}") + logging.info(f"New columns may still be needed. Create radio plugs to validate and create.") + + def _migration_check(self, input_file, needed_cols): + f = FileUtil.open_file(input_file, 'r') + dict_reader = csv.DictReader(f) + provided_fields = dict_reader.fieldnames + f.close() + + needed_fields = needed_cols.keys() + + not_needed = [] + for provided in provided_fields: + if provided not in needed_fields: + not_needed.append(provided) + return not_needed + def migrate(self): existing_backups = False files_list = [] @@ -76,6 +160,7 @@ def migrate(self): self._migrate_one() self._migrate_two() self._migrate_three() + self._migrate_four() logging.info("Migrations are complete. Your original files have been renamed to have a `.bak` extension.") def _migrate_one(self): @@ -126,3 +211,10 @@ def _migrate_three(self): zone_columns = ['number', 'name'] self._add_cols_to_file('in/zones.csv', zone_columns) return + + def _migrate_four(self): + logging.info("Running migration step 4: removing 'number' columns") + self._delete_col('in/input.csv', 'number') + self._delete_col('in/dmr_id.csv', 'number') + self._delete_col('in/digital_contacts.csv', 'number') + return diff --git a/src/ham/radio/chirp/radio_channel_chirp.py b/src/ham/radio/chirp/radio_channel_chirp.py index bd9a470..6aa97e2 100644 --- a/src/ham/radio/chirp/radio_channel_chirp.py +++ b/src/ham/radio/chirp/radio_channel_chirp.py @@ -5,7 +5,6 @@ class RadioChannelChirp(RadioChannel): def __init__(self, cols, digital_contacts, dmr_ids): super().__init__(cols, digital_contacts, dmr_ids) - self.number.set_alias(radio_types.CHIRP, 'Location') self.short_name.set_alias(radio_types.CHIRP, 'Name') self.rx_freq.set_alias(radio_types.CHIRP, 'Frequency') self.rx_ctcss.set_alias(radio_types.CHIRP, 'cToneFreq') @@ -18,7 +17,7 @@ def skip_radio_csv(self): def headers(self): output = list() - output.append(f"{self.number.get_alias(radio_types.CHIRP)}") + output.append(f"Location") output.append(f"{self.short_name.get_alias(radio_types.CHIRP)}") output.append(f"{self.rx_freq.get_alias(radio_types.CHIRP)}") output.append(f"Duplex") diff --git a/src/ham/radio/cs800/dmr_contact_cs800.py b/src/ham/radio/cs800/dmr_contact_cs800.py index 6f68ccd..8a821db 100644 --- a/src/ham/radio/cs800/dmr_contact_cs800.py +++ b/src/ham/radio/cs800/dmr_contact_cs800.py @@ -5,26 +5,25 @@ class DmrContactCs800(DmrContact): def __init__(self, cols): super().__init__(cols) - self.number.set_alias(radio_types.CS800, 'No') - self.radio_id.set_alias(radio_types.CS800, 'Call ID') + self.digital_id.set_alias(radio_types.CS800, 'Call ID') self.name.set_alias(radio_types.CS800, 'Call Alias') self.call_type.set_alias(radio_types.CS800, 'Call Type') return def headers(self): output = list() - output.append(f'{self.number.get_alias(radio_types.CS800)}') + output.append(f'No') output.append(f'{self.name.get_alias(radio_types.CS800)}') output.append(f'{self.call_type.get_alias(radio_types.CS800)}') - output.append(f'{self.radio_id.get_alias(radio_types.CS800)}') + output.append(f'{self.digital_id.get_alias(radio_types.CS800)}') output.append(f'Receive Tone') return output - def output(self): + def output(self, number): output = list() - output.append(f'{self.number.fmt_val()}') + output.append(f'{number}') output.append(f'{self.name.fmt_val()}') output.append(f'Group Call') - output.append(f'{self.radio_id.fmt_val()}') + output.append(f'{self.digital_id.fmt_val()}') output.append(f'No') return output diff --git a/src/ham/radio/cs800/dmr_user_cs800.py b/src/ham/radio/cs800/dmr_user_cs800.py index 13c1e8e..7f63efd 100644 --- a/src/ham/radio/cs800/dmr_user_cs800.py +++ b/src/ham/radio/cs800/dmr_user_cs800.py @@ -3,24 +3,23 @@ class DmrUserCs800(DmrUser): - def __init__(self, cols, number=None): - super().__init__(cols, number=number) - self.number.set_alias(radio_types.CS800, 'No') + def __init__(self, cols): + super().__init__(cols) self.callsign.set_alias(radio_types.CS800, 'Call Alias') self.radio_id.set_alias(radio_types.CS800, 'Call ID') def headers(self): output = list() - output.append(f'{self.number.get_alias(radio_types.CS800)}') + output.append(f'No') output.append(f'{self.callsign.get_alias(radio_types.CS800)}') output.append(f'Call Type') output.append(f'{self.radio_id.get_alias(radio_types.CS800)}') output.append(f'Receive Tone') return output - def output(self): + def output(self, number): output = list() - output.append(f'{self.number.fmt_val()}') + output.append(f'{number}') output.append(f'{self.callsign.fmt_val()} {self.first_name.fmt_val()} {self.last_name.fmt_val("")[:1]}') output.append(f'Private Call') output.append(f'{self.radio_id.fmt_val()}') diff --git a/src/ham/radio/cs800/radio_additional_cs800.py b/src/ham/radio/cs800/radio_additional_cs800.py index 2e4b729..feeec31 100644 --- a/src/ham/radio/cs800/radio_additional_cs800.py +++ b/src/ham/radio/cs800/radio_additional_cs800.py @@ -34,14 +34,14 @@ def _output_channels(self): analog_num = 1 digital_num = 1 - for radio_channel in self._channels.values(): + for radio_channel in self._channels: casted_channel = RadioChannelBuilder.casted(radio_channel, radio_types.CS800) if casted_channel.is_digital(): - digital_sheet.append(casted_channel.output(digital_num)) + digital_sheet.append(casted_channel.output(None)) digital_num += 1 else: - analog_sheet.append(casted_channel.output(analog_num)) + analog_sheet.append(casted_channel.output(None)) analog_num += 1 channels_workbook.save(f'out/{self._style}/{self._style}_channels.xlsx') channels_workbook.close() @@ -61,21 +61,19 @@ def _output_user(self): casted_contact = DmrContactBuilder.casted(dmr_contact, self._style) if casted_contact.name.fmt_val() == 'Analog': continue - casted_contact.number._fmt_val = number - dmr_contacts_sheet.append(casted_contact.output()) + dmr_contacts_sheet.append(casted_contact.output(None)) number += 1 logging.info(f"Writing DMR users for {self._style}") for dmr_user in self._users.values(): - dmr_user.number._fmt_val = number - casted_user = DmrUserBuilder.casted(dmr_user.cols, number, self._style) - dmr_contacts_sheet.append(casted_user.output()) + casted_user = DmrUserBuilder.casted(dmr_user.cols, self._style) + dmr_contacts_sheet.append(casted_user.output(number)) number += 1 logging.debug(f"Writing user row {number}") if number % file_util.USER_LINE_LOG_INTERVAL == 0: logging.info(f"Writing user row {number}") - logging.info("Saving workbook...") + logging.info(f"Saving {self._style} workbook...") user_workbook.save(f'out/{self._style}/{self._style}_user.xlsx') logging.info("Save done.") user_workbook.close() diff --git a/src/ham/radio/cs800/radio_channel_cs800.py b/src/ham/radio/cs800/radio_channel_cs800.py index 18275e7..63a355e 100644 --- a/src/ham/radio/cs800/radio_channel_cs800.py +++ b/src/ham/radio/cs800/radio_channel_cs800.py @@ -5,7 +5,6 @@ class RadioChannelCS800(RadioChannel): def __init__(self, cols, digital_contacts, dmr_ids): super().__init__(cols, digital_contacts, dmr_ids) - self.number.set_alias(radio_types.CS800, 'No') self.name.set_alias(radio_types.CS800, 'Channel Alias') self.rx_freq.set_alias(radio_types.CS800, 'Receive Frequency') self.digital_timeslot.set_alias(radio_types.CS800, 'Time Slot') @@ -23,7 +22,7 @@ def headers(self): def _headers_cs800_digital(self): output = list() - output.append(self.number.get_alias(radio_types.CS800)) # No + output.append('No') # No output.append(self.name.get_alias(radio_types.CS800)) # Channel Alias output.append('Digital Id') # Digital Id output.append(self.digital_color.get_alias(radio_types.CS800)) # Color Code @@ -57,7 +56,7 @@ def _headers_cs800_digital(self): def _headers_cs800_analog(self): output = list() - output.append(f'{self.number.get_alias(radio_types.CS800)}') # No + output.append(f'No') # No output.append(f'{self.name.get_alias(radio_types.CS800)}') # Channel Alias output.append(f'Squelch Level') # Squelch Level output.append(f'Channel Band[KHz]') # Channel Band[KHz] @@ -120,7 +119,7 @@ def _output_cs800_digital(self, channel_number): output.append(f'Off') # Emergency Call Indication output.append(f'{transmit_frequency:.4f}') # Transmit Frequency output.append(f'Middle') # TX Ref Frequency - output.append(f'{self.digital_contacts[self.digital_contact.fmt_val()].name.fmt_val()}') # TX Contact + output.append(f'{self.digital_contacts[self.digital_contact_id.fmt_val()].name.fmt_val()}') # TX Contact output.append(f'None') # Emergency System output.append(f'{self.tx_power.fmt_val()}') # Power Level output.append(f'Color Code') # TX Admit diff --git a/src/ham/radio/d710/radio_additional_d710.py b/src/ham/radio/d710/radio_additional_d710.py index 90c689d..822f299 100644 --- a/src/ham/radio/d710/radio_additional_d710.py +++ b/src/ham/radio/d710/radio_additional_d710.py @@ -7,7 +7,7 @@ class RadioAdditionalD710(RadioAdditional): def __init__(self, channels, dmr_ids, digital_contacts, zones, users): super().__init__(channels, dmr_ids, digital_contacts, zones, users) - for k in self._channels.keys(): + for k in range(0, len(self._channels)): uncasted_channel = self._channels[k] casted_channel = RadioChannelBuilder.casted(uncasted_channel, radio_types.D710) self._channels[k] = casted_channel @@ -29,9 +29,10 @@ def _headers(self): def _output(self): f = FileUtil.open_file(f'out/{radio_types.D710}/{radio_types.D710}.hmk', 'a') channel_number = 1 - for channel in self._channels.values(): + for channel in self._channels: if channel.is_digital(): continue - f.writelines(channel.output(channel_number)+"\n") + channel_data = channel.output(channel_number) + f.writelines(channel_data + "\n") channel_number += 1 f.close() diff --git a/src/ham/radio/d710/radio_channel_d710.py b/src/ham/radio/d710/radio_channel_d710.py index 350ac86..378cb04 100644 --- a/src/ham/radio/d710/radio_channel_d710.py +++ b/src/ham/radio/d710/radio_channel_d710.py @@ -6,7 +6,6 @@ class RadioChannelD710(RadioChannel): def __init__(self, cols, digital_contacts, dmr_ids): super().__init__(cols, digital_contacts, dmr_ids) - self.number.set_alias(radio_types.D710, 'Ch') self.rx_freq.set_alias(radio_types.D710, 'Rx Freq.') self.tx_offset.set_alias(radio_types.D710, 'Offset') self.tx_ctcss.set_alias(radio_types.D710, 'TO Freq.') @@ -29,7 +28,7 @@ def headers(self): // Memory Channels !!""" - result += f"{self.number.get_alias(radio_types.D710)}," + result += f"Ch," result += f"{self.rx_freq.get_alias(radio_types.D710)}," result += f"Rx Step," result += f"{self.tx_offset.get_alias(radio_types.D710)}," @@ -71,7 +70,7 @@ def output(self, channel_number): shift_split = '+' medium_name = self.medium_name.fmt_val().upper() output = "" - output += f"{self.number.fmt_val()-1:04d}," + output += f"{channel_number-1:04d}," output += f"{self.rx_freq.fmt_val():012.06f}," output += f"{rx_step:06.02f}," output += f"{abs(self.tx_offset.fmt_val(0.0)):09.06f}," diff --git a/src/ham/radio/d878/dmr_contact_d878.py b/src/ham/radio/d878/dmr_contact_d878.py index 474f222..2b33b9a 100644 --- a/src/ham/radio/d878/dmr_contact_d878.py +++ b/src/ham/radio/d878/dmr_contact_d878.py @@ -5,30 +5,28 @@ class DmrContactD878(DmrContact): def __init__(self, cols): super().__init__(cols) - - self.number.set_alias(radio_types.D878, 'No.') - self.radio_id.set_alias(radio_types.D878, 'Radio ID') + self.digital_id.set_alias(radio_types.D878, 'Radio ID') self.name.set_alias(radio_types.D878, 'Name') self.call_type.set_alias(radio_types.D878, 'Call Type') return def headers(self): output = list() - output.append(f"{self.number.get_alias(radio_types.D878)}") - output.append(f"{self.radio_id.get_alias(radio_types.D878)}") + output.append(f"No.") + output.append(f"{self.digital_id.get_alias(radio_types.D878)}") output.append(f"{self.name.get_alias(radio_types.D878)}") output.append(f"{self.call_type.get_alias(radio_types.D878)}") output.append(f"Call Alert") return output - def output(self): + def output(self, number): call_type = 'All Call' if self.call_type.fmt_val() == 'group': call_type = 'Group Call' output = list() - output.append(f"{self.number.fmt_val()}") - output.append(f"{self.radio_id.fmt_val()}") + output.append(f"{number}") + output.append(f"{self.digital_id.fmt_val()}") output.append(f"{self.name.fmt_val()}") output.append(f"{call_type}") output.append(f"None") diff --git a/src/ham/radio/d878/dmr_id_d878.py b/src/ham/radio/d878/dmr_id_d878.py index 65fc31c..912f685 100644 --- a/src/ham/radio/d878/dmr_id_d878.py +++ b/src/ham/radio/d878/dmr_id_d878.py @@ -5,20 +5,19 @@ class DmrIdD878(DmrId): def __init__(self, cols): super().__init__(cols) - self.number.set_alias(radio_types.D878, 'No.') self.radio_id.set_alias(radio_types.D878, 'Radio ID') self.name.set_alias(radio_types.D878, 'Name') def headers(self): output = list() - output.append(f"{self.number.get_alias(radio_types.D878)}") + output.append(f"No.") output.append(f"{self.radio_id.get_alias(radio_types.D878)}") output.append(f"{self.name.get_alias(radio_types.D878)}") return output - def output(self): + def output(self, number): output = list() - output.append(f"{self.number.fmt_val()}") + output.append(f"{number}") output.append(f"{self.radio_id.fmt_val()}") output.append(f"{self.name.fmt_val()}") return output diff --git a/src/ham/radio/d878/dmr_user_d878.py b/src/ham/radio/d878/dmr_user_d878.py index 50ff050..e6364f2 100644 --- a/src/ham/radio/d878/dmr_user_d878.py +++ b/src/ham/radio/d878/dmr_user_d878.py @@ -2,6 +2,6 @@ class DmrUserD878(DmrUserDefault): - def __init__(self, cols, number=None): - super().__init__(cols, number=number) + def __init__(self, cols): + super().__init__(cols) diff --git a/src/ham/radio/d878/radio_additional_d878.py b/src/ham/radio/d878/radio_additional_d878.py index 72b437c..b9918bc 100644 --- a/src/ham/radio/d878/radio_additional_d878.py +++ b/src/ham/radio/d878/radio_additional_d878.py @@ -34,9 +34,11 @@ def _output_radioids(self): headers = DmrIdD878.create_empty() radio_id_file.writerow(headers.headers()) + number = 1 for dmr_id in self._dmr_ids.values(): casted_id = DmrIdBuilder.casted(dmr_id, radio_types.D878) - radio_id_file.writerow(casted_id.output()) + radio_id_file.writerow(casted_id.output(number)) + number += 1 radio_id_file.close() return @@ -51,9 +53,12 @@ def _output_contacts(self): headers = DmrContactD878.create_empty() dmr_contact_file.writerow(headers.headers()) + number = 1 for dmr_contact in self._digital_contacts.values(): casted_contact = DmrContactBuilder.casted(dmr_contact, self._style) - dmr_contact_file.writerow(casted_contact.output()) + row_data = casted_contact.output(number) + dmr_contact_file.writerow(row_data) + number += 1 dmr_contact_file.close() @@ -88,8 +93,8 @@ def _output_user(self): rows_processed = 1 for user in self._users.values(): - casted_user = DmrUserBuilder.casted(user.cols, user.number, self._style) - users_file.writerow(casted_user.output()) + casted_user = DmrUserBuilder.casted(user.cols, self._style) + users_file.writerow(casted_user.output(None)) rows_processed += 1 logging.debug(f"Writing user row {rows_processed}") if rows_processed % file_util.USER_LINE_LOG_INTERVAL == 0: diff --git a/src/ham/radio/d878/radio_channel_d878.py b/src/ham/radio/d878/radio_channel_d878.py index d9a6ac6..70022c2 100644 --- a/src/ham/radio/d878/radio_channel_d878.py +++ b/src/ham/radio/d878/radio_channel_d878.py @@ -5,7 +5,6 @@ class RadioChannelD878(RadioChannel): def __init__(self, cols, digital_contacts, dmr_ids): super().__init__(cols, digital_contacts, dmr_ids) - self.number.set_alias(radio_types.D878, 'No.') self.name.set_alias(radio_types.D878, 'Channel Name') self.rx_freq.set_alias(radio_types.D878, 'Receive Frequency') self.digital_timeslot.set_alias(radio_types.D878, 'Slot') @@ -17,7 +16,7 @@ def skip_radio_csv(self): def headers(self): output = list() - output.append(f"{self.number.get_alias(radio_types.D878)}") # "No.," + output.append(f"No.") # "No.," output.append(f"{self.name.get_alias(radio_types.D878)}") # "Channel Name," output.append(f"{self.rx_freq.get_alias(radio_types.D878)}") # "Receive Frequency," output.append(f"Transmit Frequency") # "Transmit Frequency," @@ -75,7 +74,7 @@ def output(self, channel_number): busy_lock = 'Off' dmr_mode = 0 contact_call_type = 'All Call' - contact = self.digital_contacts[self.digital_contact.fmt_val(0)] + contact = self.digital_contacts[self.digital_contact_id.fmt_val(0)] dmr_name = self.dmr_ids[1].name.fmt_val() contact_id = self.dmr_ids[1].radio_id.fmt_val() call_confirmation = 'Off' @@ -84,7 +83,7 @@ def output(self, channel_number): busy_lock = 'Always' dmr_mode = 1 contact_call_type = 'Group Call' - contact_id = contact.radio_id.fmt_val() + contact_id = contact.digital_id.fmt_val() call_confirmation = 'On' ctcs_dcs_decode = 'Off' diff --git a/src/ham/radio/default_radio/dmr_contact_default.py b/src/ham/radio/default_radio/dmr_contact_default.py index 6d15e46..03484eb 100644 --- a/src/ham/radio/default_radio/dmr_contact_default.py +++ b/src/ham/radio/default_radio/dmr_contact_default.py @@ -10,16 +10,14 @@ def __init__(self, cols): def headers(self): output = list() - output.append(f"{self.number.get_alias(radio_types.DEFAULT)}") - output.append(f"{self.radio_id.get_alias(radio_types.DEFAULT)}") + output.append(f"{self.digital_id.get_alias(radio_types.DEFAULT)}") output.append(f"{self.name.get_alias(radio_types.DEFAULT)}") output.append(f"{self.call_type.get_alias(radio_types.DEFAULT)}") return output - def output(self): + def output(self, number): output = list() - output.append(f"{self.number.fmt_val()}") - output.append(f"{self.radio_id.fmt_val()}") + output.append(f"{self.digital_id.fmt_val()}") output.append(f"{self.name.fmt_val()}") output.append(f"{self.call_type.fmt_val()}") return output diff --git a/src/ham/radio/default_radio/dmr_id_default.py b/src/ham/radio/default_radio/dmr_id_default.py index 3df10f2..14d05a0 100644 --- a/src/ham/radio/default_radio/dmr_id_default.py +++ b/src/ham/radio/default_radio/dmr_id_default.py @@ -9,14 +9,12 @@ def __init__(self, cols): def headers(self): output = list() - output.append(f"{self.number.get_alias(radio_types.DEFAULT)}") output.append(f"{self.radio_id.get_alias(radio_types.DEFAULT)}") output.append(f"{self.name.get_alias(radio_types.DEFAULT)}") return output - def output(self): + def output(self, number): output = list() - output.append(f"{self.number.fmt_val()}") output.append(f"{self.radio_id.fmt_val()}") output.append(f"{self.name.fmt_val()}") return output diff --git a/src/ham/radio/default_radio/dmr_user_default.py b/src/ham/radio/default_radio/dmr_user_default.py index fd4d070..bb23d59 100644 --- a/src/ham/radio/default_radio/dmr_user_default.py +++ b/src/ham/radio/default_radio/dmr_user_default.py @@ -4,8 +4,8 @@ class DmrUserDefault(DmrUser): - def __init__(self, cols, number=None): - super().__init__(cols, number=number) + def __init__(self, cols): + super().__init__(cols) self.radio_id = DataColumn(fmt_name='RADIO_ID', fmt_val=cols['RADIO_ID'], shape=str) self.callsign = DataColumn(fmt_name='CALLSIGN', fmt_val=cols['CALLSIGN'], shape=str) self.first_name = DataColumn(fmt_name='FIRST_NAME', fmt_val=cols['FIRST_NAME'], shape=str) @@ -14,8 +14,6 @@ def __init__(self, cols, number=None): self.state = DataColumn(fmt_name='STATE', fmt_val=cols['STATE'], shape=str) self.country = DataColumn(fmt_name='COUNTRY', fmt_val=cols['COUNTRY'], shape=str) self.remarks = DataColumn(fmt_name='REMARKS', fmt_val=cols['REMARKS'], shape=str) - self.number = DataColumn(fmt_name='NUMBER', fmt_val=number, shape=int) - self.number.set_alias(radio_types.CS800, 'No') def headers(self): output = list() @@ -29,7 +27,7 @@ def headers(self): output.append(f"{self.remarks.get_alias(radio_types.DEFAULT)}") return output - def output(self): + def output(self, number): output = list() output.append(f"{self.radio_id.fmt_val('')}") output.append(f"{self.callsign.fmt_val('')}") diff --git a/src/ham/radio/default_radio/radio_additional_default.py b/src/ham/radio/default_radio/radio_additional_default.py index b272ed1..aad0fb0 100644 --- a/src/ham/radio/default_radio/radio_additional_default.py +++ b/src/ham/radio/default_radio/radio_additional_default.py @@ -34,9 +34,11 @@ def _output_radioids(self): headers = DmrIdDefault.create_empty() radio_id_file.writerow(headers.headers()) + number = 1 for dmr_id in self._dmr_ids.values(): casted_id = DmrIdBuilder.casted(dmr_id, self._style) - radio_id_file.writerow(casted_id.output()) + radio_id_file.writerow(casted_id.output(None)) + number += 1 writer.close() return @@ -69,7 +71,8 @@ def _output_contacts(self): dmr_contact_file.writerow(headers.headers()) for dmr_contact in self._digital_contacts.values(): casted_contact = DmrContactBuilder.casted(dmr_contact, self._style) - dmr_contact_file.writerow(casted_contact.output()) + row_data = casted_contact.output(None) + dmr_contact_file.writerow(row_data) writer.close() @@ -84,7 +87,7 @@ def _output_user(self): headers = DmrUserDefault.create_empty() users_file.writerow(headers.headers()) for user in self._users.values(): - casted_user = DmrUserBuilder.casted(user.cols, user.number, self._style) - users_file.writerow(casted_user.output()) + casted_user = DmrUserBuilder.casted(user.cols, self._style) + users_file.writerow(casted_user.output(None)) writer.close() diff --git a/src/ham/radio/default_radio/radio_channel_default.py b/src/ham/radio/default_radio/radio_channel_default.py index 667b051..3c86b06 100644 --- a/src/ham/radio/default_radio/radio_channel_default.py +++ b/src/ham/radio/default_radio/radio_channel_default.py @@ -11,7 +11,6 @@ def skip_radio_csv(self): def headers(self): output = list() - output.append(f"{self.number.get_alias(radio_types.DEFAULT)}") output.append(f"{self.name.get_alias(radio_types.DEFAULT)}") output.append(f"{self.medium_name.get_alias(radio_types.DEFAULT)}") output.append(f"{self.short_name.get_alias(radio_types.DEFAULT)}") @@ -27,12 +26,11 @@ def headers(self): output.append(f"{self.tx_dcs_invert.get_alias(radio_types.DEFAULT)}") output.append(f"{self.digital_timeslot.get_alias(radio_types.DEFAULT)}") output.append(f"{self.digital_color.get_alias(radio_types.DEFAULT)}") - output.append(f"{self.digital_contact.get_alias(radio_types.DEFAULT)}") + output.append(f"{self.digital_contact_id.get_alias(radio_types.DEFAULT)}") return output def output(self, channel_number): output = list() - output.append(f"{channel_number}") output.append(f"{self.name.fmt_val('')}") output.append(f"{self.medium_name.fmt_val('')}") output.append(f"{self.short_name.fmt_val('')}") @@ -48,5 +46,5 @@ def output(self, channel_number): output.append(f"{self.tx_dcs_invert.fmt_val('')}") output.append(f"{self.digital_timeslot.fmt_val('')}") output.append(f"{self.digital_color.fmt_val('')}") - output.append(f"{self.digital_contact.fmt_val('')}") + output.append(f"{self.digital_contact_id.fmt_val('')}") return output diff --git a/src/ham/radio/dmr_contact.py b/src/ham/radio/dmr_contact.py index eeed66e..00ea7ef 100644 --- a/src/ham/radio/dmr_contact.py +++ b/src/ham/radio/dmr_contact.py @@ -5,15 +5,13 @@ class DmrContact: @classmethod def create_empty(cls): cols = dict() - cols['number'] = '' cols['digital_id'] = '' cols['name'] = '' cols['call_type'] = '' return cls(cols) def __init__(self, cols): - self.number = DataColumn(fmt_name='number', fmt_val=cols['number'], shape=int) - self.radio_id = DataColumn(fmt_name='digital_id', fmt_val=cols['digital_id'], shape=int) + self.digital_id = DataColumn(fmt_name='digital_id', fmt_val=cols['digital_id'], shape=int) self.name = DataColumn(fmt_name='name', fmt_val=cols['name'], shape=str) self.call_type = DataColumn(fmt_name='call_type', fmt_val=cols['call_type'], shape=str) @@ -23,6 +21,6 @@ def __init__(self, cols): def headers(self): raise Exception("Base method cannot be called!") - def output(self): + def output(self, number): raise Exception("Base method cannot be called!") diff --git a/src/ham/radio/dmr_id.py b/src/ham/radio/dmr_id.py index a0d3a2d..aa883af 100644 --- a/src/ham/radio/dmr_id.py +++ b/src/ham/radio/dmr_id.py @@ -5,13 +5,11 @@ class DmrId: @classmethod def create_empty(cls): cols = dict() - cols['number'] = '' cols['radio_id'] = '' cols['name'] = '' return cls(cols) def __init__(self, cols): - self.number = DataColumn(fmt_name='number', fmt_val=cols['number'], shape=int) self.radio_id = DataColumn(fmt_name='radio_id', fmt_val=cols['radio_id'], shape=int) self.name = DataColumn(fmt_name='name', fmt_val=cols['name'], shape=str) self.cols = cols @@ -19,5 +17,5 @@ def __init__(self, cols): def headers(self): raise Exception("Base method cannot be called!") - def output(self): + def output(self, number): raise Exception("Base method cannot be called!") diff --git a/src/ham/radio/dmr_user.py b/src/ham/radio/dmr_user.py index 2303ccd..c6108bd 100644 --- a/src/ham/radio/dmr_user.py +++ b/src/ham/radio/dmr_user.py @@ -15,7 +15,7 @@ def create_empty(cls): cols['REMARKS'] = '' return cls(cols) - def __init__(self, cols, number = None): + def __init__(self, cols): self.radio_id = DataColumn(fmt_name='RADIO_ID', fmt_val=cols['RADIO_ID'], shape=str) self.callsign = DataColumn(fmt_name='CALLSIGN', fmt_val=cols['CALLSIGN'], shape=str) self.first_name = DataColumn(fmt_name='FIRST_NAME', fmt_val=cols['FIRST_NAME'], shape=str) @@ -24,12 +24,11 @@ def __init__(self, cols, number = None): self.state = DataColumn(fmt_name='STATE', fmt_val=cols['STATE'], shape=str) self.country = DataColumn(fmt_name='COUNTRY', fmt_val=cols['COUNTRY'], shape=str) self.remarks = DataColumn(fmt_name='REMARKS', fmt_val=cols['REMARKS'], shape=str) - self.number = DataColumn(fmt_name='NUMBER', fmt_val=number, shape=int) self.cols = cols def headers(self): raise Exception("Base method cannot be called!") - def output(self): + def output(self, number): raise Exception("Base method cannot be called!") diff --git a/src/ham/radio/ftm400/radio_channel_ftm400.py b/src/ham/radio/ftm400/radio_channel_ftm400.py index f30c6e6..026deb1 100644 --- a/src/ham/radio/ftm400/radio_channel_ftm400.py +++ b/src/ham/radio/ftm400/radio_channel_ftm400.py @@ -5,7 +5,6 @@ class RadioChannelFtm400(RadioChannel): def __init__(self, cols, digital_contacts, dmr_ids): super().__init__(cols, digital_contacts, dmr_ids) - self.number.set_alias(radio_types.FTM400_RT, 'Channel Number') self.medium_name.set_alias(radio_types.FTM400_RT, 'Name') self.rx_freq.set_alias(radio_types.FTM400_RT, 'Receive Frequency') self.rx_ctcss.set_alias(radio_types.FTM400_RT, 'CTCSS') @@ -19,7 +18,7 @@ def skip_radio_csv(self): def headers(self): output = list() - output.append(f"{self.number.get_alias(radio_types.FTM400_RT)}") + output.append(f"Channel Number") output.append(f"{self.rx_freq.get_alias(radio_types.FTM400_RT)}") output.append(f"Transmit Frequency") output.append(f"{self.tx_offset.get_alias(radio_types.FTM400_RT)}") diff --git a/src/ham/radio/radio_casted_builder.py b/src/ham/radio/radio_casted_builder.py index 1690a8a..f02e5d2 100644 --- a/src/ham/radio/radio_casted_builder.py +++ b/src/ham/radio/radio_casted_builder.py @@ -57,11 +57,11 @@ def casted(cls, radio_channel, style): class DmrUserBuilder: @classmethod - def casted(cls, cols, number, style): + def casted(cls, cols, style): switch = { - radio_types.DEFAULT: DmrUserDefault(cols, number), - radio_types.D878: DmrUserD878(cols, number), - radio_types.CS800: DmrUserCs800(cols, number), + radio_types.DEFAULT: DmrUserDefault(cols), + radio_types.D878: DmrUserD878(cols), + radio_types.CS800: DmrUserCs800(cols), } return switch[style] diff --git a/src/ham/radio/radio_channel.py b/src/ham/radio/radio_channel.py index ed0e36b..5efb638 100644 --- a/src/ham/radio/radio_channel.py +++ b/src/ham/radio/radio_channel.py @@ -5,7 +5,6 @@ class RadioChannel: @classmethod def create_empty(cls): col_vals = dict() - col_vals['number'] = '' col_vals['name'] = '' col_vals['medium_name'] = '' col_vals['short_name'] = '' @@ -25,7 +24,6 @@ def create_empty(cls): return cls(col_vals, digital_contacts=None, dmr_ids=None) def __init__(self, cols, digital_contacts, dmr_ids): - self.number = DataColumn(fmt_name='number', fmt_val=cols['number'], shape=int) self.name = DataColumn(fmt_name='name', fmt_val=cols['name'], shape=str) self.medium_name = DataColumn(fmt_name='medium_name', fmt_val=cols['medium_name'], shape=str) self.short_name = DataColumn(fmt_name='short_name', fmt_val=cols['short_name'], shape=str) @@ -40,7 +38,7 @@ def __init__(self, cols, digital_contacts, dmr_ids): self.tx_dcs_invert = DataColumn(fmt_name='tx_dcs_invert', fmt_val=cols['tx_dcs_invert'], shape=bool) self.digital_timeslot = DataColumn(fmt_name='digital_timeslot', fmt_val=cols['digital_timeslot'], shape=int) self.digital_color = DataColumn(fmt_name='digital_color', fmt_val=cols['digital_color'], shape=int) - self.digital_contact = DataColumn(fmt_name='digital_contact_id', fmt_val=cols['digital_contact_id'], shape=int) + self.digital_contact_id = DataColumn(fmt_name='digital_contact_id', fmt_val=cols['digital_contact_id'], shape=int) self.tx_power = DataColumn(fmt_name='tx_power', fmt_val=cols['tx_power'], shape=str) self.cols = cols diff --git a/src/ham/radio_generator.py b/src/ham/radio_generator.py index 8490d83..032cc8b 100644 --- a/src/ham/radio_generator.py +++ b/src/ham/radio_generator.py @@ -1,7 +1,10 @@ import csv import logging import os +from time import sleep +from src import radio_sync_version +from src.ham.migration.migration_manager import MigrationManager from src.ham.radio.dmr_contact import DmrContact from src.ham.radio.dmr_id import DmrId from src.ham.radio.dmr_user import DmrUser @@ -19,11 +22,12 @@ class RadioGenerator: def __init__(self, radio_list): self.radio_list = radio_list self._validator = Validator() + self._migrations = MigrationManager() @classmethod def info(cls, dangerous_ops_info): logging.info(f""" - HAM RADIO SYNC GENERATOR + HAM RADIO SYNC GENERATOR v{radio_sync_version.version} Homepage: https://github.com/n2qzshce/ham-radio-sync Purpose: The intent of this program is to generate codeplug files to import into various radio applications by @@ -51,6 +55,12 @@ def generate_all_declared(self): if len(file_errors) > 0: return + results = self._migrations.check_migrations_needed() + if len(results.keys()) > 0: + logging.warning("You may be using an old version of the input files. Have you run migrations?") + logging.warning("Migrations check is under the 'File' menu.") + sleep(1) + digital_contacts, digi_contact_errors = self._generate_digital_contact_data() dmr_ids, dmr_id_errors = self._generate_dmr_id_data() zones, zone_errors = self._generate_zone_data() @@ -61,10 +71,10 @@ def generate_all_declared(self): csv_reader = csv.DictReader(feed) radio_channel_errors = [] - radio_channels = dict() + radio_channels = [] line_num = 1 for line in csv_reader: - line_errors = self._validator.validate_radio_channel(line, line_num, feed.name) + line_errors = self._validator.validate_radio_channel(line, line_num, feed.name, digital_contacts) radio_channel_errors += line_errors line_num += 1 @@ -72,7 +82,7 @@ def generate_all_declared(self): continue radio_channel = RadioChannel(line, digital_contacts, dmr_ids) - radio_channels[radio_channel.number.fmt_val()] = radio_channel + radio_channels.append(radio_channel) if radio_channel.zone_id.fmt_val(None) is not None: zones[radio_channel.zone_id.fmt_val()].add_channel(radio_channel) @@ -106,10 +116,11 @@ def generate_all_declared(self): channel_numbers[radio] = 1 logging.info("Processing radio channels") - for radio_channel in radio_channels.values(): - logging.debug(f"Processing radio line {radio_channel.number}") - if radio_channel.number.fmt_val(None) % file_util.RADIO_LINE_LOG_INTERVAL == 0: - logging.info(f"Processing radio line {radio_channel.number.fmt_val(None)}") + line = 1 + for radio_channel in radio_channels: + logging.debug(f"Processing radio line {line}") + if line % file_util.RADIO_LINE_LOG_INTERVAL == 0: + logging.info(f"Processing radio line {line}") for radio in self.radio_list: if radio not in radio_files.keys(): @@ -131,8 +142,9 @@ def generate_all_declared(self): casted_additional_data = RadioAdditionalBuilder.casted(additional_data, radio) casted_additional_data.output() - logging.info(f"""Radio generator complete. Your output files are in `{os.path.abspath('out')}` - The next step is to import these files into your radio programming application. (e.g. CHiRP)""") + logging.info(f"""Radio generator complete. Your output files are in + `{os.path.abspath('out')}` + The next step is to import these files into your radio programming application. (e.g. CHiRP)""") return def _generate_digital_contact_data(self): @@ -150,7 +162,7 @@ def _generate_digital_contact_data(self): if len(line_errors) != 0: continue contact = DmrContact(line) - digital_contacts[contact.radio_id.fmt_val()] = contact + digital_contacts[contact.digital_id.fmt_val()] = contact return digital_contacts, errors @@ -160,15 +172,15 @@ def _generate_dmr_id_data(self): csv_feed = csv.DictReader(feed) dmr_ids = dict() errors = [] - line_num = 1 + line_num = 0 for line in csv_feed: + line_num += 1 line_errors = self._validator.validate_dmr_id(line, line_num, feed.name) errors += line_errors - line_num += 1 if len(line_errors) != 0: continue dmr_id = DmrId(line) - dmr_ids[dmr_id.number.fmt_val()] = dmr_id + dmr_ids[line_num] = dmr_id return dmr_ids, errors diff --git a/src/ham/util/validator.py b/src/ham/util/validator.py index f892409..9c46f9a 100644 --- a/src/ham/util/validator.py +++ b/src/ham/util/validator.py @@ -45,7 +45,7 @@ def validate_files_exist(cls): logging.info(f"Checked `{os.path.abspath('./in')}`") for err in errors: logging.error(f"\t\t{err.message}") - logging.info("Have you run `Wizard` under `Dangerous Operations`?") + logging.info("Have you run `Wizard (new)` or `Migrations (update)` under `Dangerous Operations`?") else: logging.info("All necessary files found") @@ -67,7 +67,7 @@ def validate_digital_contact(self, cols, line_num, file_name): needed_cols_dict_gen = dict(self._digital_contact_template.__dict__) return self._validate_generic(cols, line_num, file_name, needed_cols_dict_gen) - def validate_radio_channel(self, cols, line_num, file_name): + def validate_radio_channel(self, cols, line_num, file_name, digital_contacts): needed_cols_dict_gen = dict(self._radio_channel_template.__dict__) errors = self._validate_generic(cols, line_num, file_name, needed_cols_dict_gen) if len(errors) > 0: @@ -118,6 +118,22 @@ def validate_radio_channel(self, cols, line_num, file_name): ) errors.append(err) + if channel.is_digital() and channel.digital_contact_id.fmt_val() not in digital_contacts.keys(): + err = ValidationError( + f"Cannot find digital contact `{channel.digital_contact_id.fmt_val()}` specified in " + f"digital contacts.", line_num, file_name + ) + errors.append(err) + + acceptable_tx_powers = ["Low", "Medium", "High"] + if channel.tx_power.fmt_val() is None or channel.tx_power.fmt_val() not in acceptable_tx_powers: + err = ValidationError( + f"Transmit power (`tx_power`) invalid: `{channel.digital_contact_id.fmt_val()}`. Valid values " + f"are {acceptable_tx_powers}" + , line_num, file_name + ) + errors.append(err) + return errors def _validate_generic(self, cols, line_num, file_name, needed_cols_dict_gen): diff --git a/src/ham/wizard.py b/src/ham/wizard.py index 90fa8ec..e2fed84 100644 --- a/src/ham/wizard.py +++ b/src/ham/wizard.py @@ -47,7 +47,6 @@ def _create_input(self, is_forced): def _create_channel_file(self): channel_file = RadioWriter('in/input.csv', '\n') first_channel = RadioChannelDefault({ - 'number': '1', 'name': 'National 2m', 'medium_name': 'Natl 2m', 'short_name': 'NATL 2M', @@ -66,7 +65,6 @@ def _create_channel_file(self): 'digital_contact_id': '', }, digital_contacts=None, dmr_ids=None) second_channel = RadioChannelDefault({ - 'number': '2', 'name': 'Basic Repeater', 'medium_name': 'BasicRpt', 'short_name': 'BASRPTR', @@ -92,31 +90,28 @@ def _create_channel_file(self): def _create_dmr_data(self): dmr_id_file = RadioWriter('in/dmr_id.csv', '\n') dmr_id = DmrIdDefault({ - 'number': 1, 'radio_id': '00000', 'name': 'DMR', }) dmr_id_file.writerow(dmr_id.headers()) - dmr_id_file.writerow(dmr_id.output()) + dmr_id_file.writerow(dmr_id.output(1)) dmr_id_file.close() digital_contacts_file = RadioWriter('in/digital_contacts.csv', '\n') analog_contact = DmrContactDefault({ - 'number': 1, 'digital_id': dmr_id.radio_id.fmt_val(), 'name': 'Analog', 'call_type': 'all', }) group_contact = DmrContactDefault({ - 'number': 2, 'digital_id': 99999, 'name': 'Some Repeater', 'call_type': 'group', }) digital_contacts_file.writerow(analog_contact.headers()) - digital_contacts_file.writerow(analog_contact.output()) - digital_contacts_file.writerow(group_contact.output()) + digital_contacts_file.writerow(analog_contact.output(1)) + digital_contacts_file.writerow(group_contact.output(2)) digital_contacts_file.close() def _create_zone_data(self): @@ -142,7 +137,7 @@ def _create_dmr_user_data(self): 'REMARKS': 'Sample Entry', }) user_file.writerow(dmr_user.headers()) - user_file.writerow(dmr_user.output()) + user_file.writerow(dmr_user.output(None)) user_file.close() return diff --git a/src/radio_sync_version.py b/src/radio_sync_version.py new file mode 100644 index 0000000..04aa7e1 --- /dev/null +++ b/src/radio_sync_version.py @@ -0,0 +1 @@ +version = 'DEVELOPMENT' diff --git a/src/ui/app_window.py b/src/ui/app_window.py index 69a1554..b2eda8d 100644 --- a/src/ui/app_window.py +++ b/src/ui/app_window.py @@ -9,6 +9,7 @@ from kivy.config import Config from kivy.core.window import Window from kivy.lang import Builder +from kivy.metrics import dp from kivy.resources import resource_add_path from kivy.resources import resource_paths from kivy.uix.boxlayout import BoxLayout @@ -16,6 +17,7 @@ from kivy.uix.label import Label from kivy.uix.textinput import TextInput +from src import radio_sync_version from src.ham.util import radio_types from src.ui.async_wrapper import AsyncWrapper @@ -38,6 +40,7 @@ class LayoutIds: action_previous = 'action_previous' create_radio_plugs = 'create_radio_plugs' enable_dangerous = 'enable_dangerous' + check_migrations = 'check_migrations' clear_log = 'clear_log' exit_button = 'exit_button' dangerous_operations = 'dangerous_operations' @@ -81,6 +84,10 @@ class LayoutIds: ActionGroup: text: "File" mode: "spinner" + dropdown_width: dp(225) + ActionButton: + id: {LayoutIds.check_migrations} + text: "Check for needed migrations" ActionButton: id: {LayoutIds.clear_log} text: "Clear log" @@ -91,7 +98,7 @@ class LayoutIds: text: "Dangerous Operations" mode: "spinner" id: {LayoutIds.dangerous_operations} - dropdown_width: 225 + dropdown_width: dp(225) ActionButton: id: {LayoutIds.dangerous_operation__delete_migrate} text: "Remove migration backups" @@ -107,7 +114,7 @@ class LayoutIds: ActionGroup: text: "Help / Getting Started" mode: "spinner" - dropdown_width: 250 + dropdown_width: dp(250) ActionButton: id: {LayoutIds.getting_started} text: "About/Getting started..." @@ -127,21 +134,21 @@ class LayoutIds: orientation: "horizontal" StackLayout: id: {LayoutIds.button_pool} - spacing: 10 + spacing: dp(10) size_hint: (0.2, 1) - padding: [20,20,20,20] - size_hint_min_x: 225 - size_hint_max_x: 275 + padding: [dp(20), dp(20), dp(20), dp(20)] + size_hint_min_x: dp(225) + size_hint_max_x: dp(275) Label: id: {LayoutIds.radio_header} text: "Radios to Generate" size_hint: (1.0, 0.1) - font_size: 15 + font_size: dp(15) bold: True BoxLayout: id: {LayoutIds.radio_labels} orientation: "vertical" - spacing: 10 + spacing: dp(10) size_hint: (1, 0.4) BoxLayout: id: {LayoutIds.buffer} @@ -155,7 +162,7 @@ class LayoutIds: text: '' size_hint: (1, 1) readonly: True - font_size: 11 + font_size: dp(11) use_bubble: True """ @@ -185,11 +192,11 @@ def build(self): self._async_wrapper = AsyncWrapper() layout = Builder.load_string(kv) - Window.size = (1200, 500) + Window.size = (dp(1200), dp(500)) Window.clearcolor = (0.15, 0.15, 0.15, 1) Window.bind(on_keyboard=self.key_handler) - self.title = 'Ham Radio Sync' + self.title = f'Ham Radio Sync v{radio_sync_version.version}' action_previous = layout.ids[LayoutIds.action_previous] action_previous.app_icon = action_icon_path @@ -211,8 +218,6 @@ def build(self): def key_handler(self, window, keycode1, keycode2, text, modifiers): if keycode1 == 27 or keycode1 == 1001: - # Do whatever you want here - or nothing at all - # Returning True will eat the keypress return True return False @@ -225,7 +230,7 @@ def _bind_radio_menu(self, layout): for radio in radios: radio_layout = BoxLayout(orientation='horizontal', size_hint=(1, 0.1)) - radio_label = Label(text=radio_types.pretty_name(radio), size_hint=(0.9, 1), font_size=11, halign='left') + radio_label = Label(text=radio_types.pretty_name(radio), size_hint=(0.9, 1), font_size=dp(11), halign='left') radio_checkbox = CheckBox(size_hint=(0.1, 1)) radio_checkbox.active = radio == radio_types.DEFAULT radio_label.bind(size=radio_label.setter('text_size')) @@ -263,6 +268,9 @@ def _bind_console_log(self, layout): logger.addHandler(handler) def _bind_file_menu(self, layout): + check_migrations_button = layout.ids[LayoutIds.check_migrations] + check_migrations_button.bind(on_press=self._async_wrapper.check_migrations) + clear_console_button = layout.ids[LayoutIds.clear_log] clear_console_button.bind(on_press=self._clear_console) diff --git a/src/ui/async_wrapper.py b/src/ui/async_wrapper.py index d3b9365..14846a8 100644 --- a/src/ui/async_wrapper.py +++ b/src/ui/async_wrapper.py @@ -99,6 +99,12 @@ def wizard_bootstrap(self, button): def _wizard_bootstrap_async(self): self._wizard.bootstrap(True) + def check_migrations(self, event): + self._submit_blocking_task(self._check_migrations_async) + + def _check_migrations_async(self): + self._migrations.log_check_migrations() + def migrations(self, event): self._submit_blocking_task(self._migrations_async) diff --git a/test/radio/base_radio_test_setup.py b/test/radio/base_radio_test_setup.py new file mode 100644 index 0000000..947d7e6 --- /dev/null +++ b/test/radio/base_radio_test_setup.py @@ -0,0 +1,21 @@ +from test.base_test_setup import BaseTestSetup + + +class BaseRadioTestSetup(BaseTestSetup): + def test_simplex(self): + pass + + def test_uhf_simplex(self): + pass + + def test_vhf_repeater(self): + pass + + def test_positive_offset(self): + pass + + def test_dcs_repeater(self): + pass + + def test_dcs_invert(self): + pass diff --git a/test/radio/test_chirp.py b/test/radio/test_chirp.py new file mode 100644 index 0000000..708dd33 --- /dev/null +++ b/test/radio/test_chirp.py @@ -0,0 +1,175 @@ +from src.ham.radio.chirp.radio_channel_chirp import RadioChannelChirp +from test.base_test_setup import BaseTestSetup + + +class ChirpTest(BaseTestSetup): + def setUp(self): + self.radio_channel = RadioChannelChirp.create_empty() + + def test_headers(self): + result = self.radio_channel.headers() + self.assertEqual( + [ + "Location", "Name", "Frequency", "Duplex", "Offset", "Tone", "rToneFreq", "cToneFreq", + "DtcsCode", "DtcsPolarity", "Mode", "TStep", "Skip", "Comment", "URCALL", "RPT1CALL", + "RPT2CALL", "DVCODE", + ], result + ) + + def test_simplex(self): + cols = dict() + cols['name'] = 'National 2m' + cols['medium_name'] = 'Natl 2m' + cols['short_name'] = 'NATL 2M' + cols['zone_id'] = '' + cols['rx_freq'] = '146.52' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = '' + cols['tx_offset'] = '' + cols['tx_ctcss'] = '' + cols['tx_dcs'] = '' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + channel = RadioChannelChirp(cols, None, None) + result = channel.output(1) + self.assertEqual( + [ + '0', 'NATL 2M', '146.520000', '', '0.000000', '', '67.0', '67.0', + '023', 'NN', 'FM', '5.00', '', '', '', '', '', '', + ], result + ) + + def test_uhf_simplex(self): + cols = dict() + cols['name'] = 'National 70cm' + cols['medium_name'] = 'Natl 70c' + cols['short_name'] = 'NATL 70' + cols['zone_id'] = '' + cols['rx_freq'] = '446.0' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = '' + cols['tx_offset'] = '' + cols['tx_ctcss'] = '' + cols['tx_dcs'] = '' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + channel = RadioChannelChirp(cols, None, None) + result = channel.output(2) + self.assertEqual( + [ + '1', 'NATL 70', '446.000000', '', '0.000000', '', '67.0', '67.0', + '023', 'NN', 'FM', '5.00', '', '', '', '', '', '', + ], result + ) + + def test_vhf_repeater(self): + cols = dict() + cols['name'] = 'Some Repeater' + cols['medium_name'] = 'Some Rpt' + cols['short_name'] = 'SOMERPT' + cols['zone_id'] = '' + cols['rx_freq'] = '145.310' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = '' + cols['tx_offset'] = '-0.6' + cols['tx_ctcss'] = '100.0' + cols['tx_dcs'] = '' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + result = RadioChannelChirp(cols, None, None).output(3) + self.assertEqual( + [ + '2', 'SOMERPT', '145.310000', '-', '0.600000', 'Tone', '100.0', '67.0', + '023', 'NN', 'FM', '5.00', '', '', '', '', '', '', + ], result + ) + + def test_positive_offset(self): + cols = dict() + cols['name'] = 'Some Repeater' + cols['medium_name'] = 'Some Rpt' + cols['short_name'] = 'SOMERPT' + cols['zone_id'] = '' + cols['rx_freq'] = '442.125' + cols['rx_ctcss'] = '127.3' + cols['rx_dcs'] = '' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = '' + cols['tx_offset'] = '5.0' + cols['tx_ctcss'] = '100.0' + cols['tx_dcs'] = '' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + result = RadioChannelChirp(cols, None, None).output(4) + self.assertEqual( + [ + '3', 'SOMERPT', '442.125000', '+', '5.000000', 'TSQL', '100.0', '127.3', + '023', 'NN', 'FM', '5.00', '', '', '', '', '', '', + ], result + ) + + def test_dcs_repeater(self): + cols = dict() + cols['name'] = 'Dcs Repeater' + cols['medium_name'] = 'Dcs Rpt' + cols['short_name'] = 'DCS RPT' + cols['zone_id'] = '' + cols['rx_freq'] = '447.075' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '165' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = '' + cols['tx_offset'] = '-5' + cols['tx_ctcss'] = '' + cols['tx_dcs'] = '165' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + result = RadioChannelChirp(cols, None, None).output(6) + self.assertEqual( + [ + '5', 'DCS RPT', '447.075000', '-', '5.000000', 'DTCS', '67.0', '67.0', + '165', 'NN', 'FM', '5.00', '', '', '', '', '', '', + ], result + ) + + def test_dcs_invert(self): + cols = dict() + cols['name'] = 'Dcs Repeater' + cols['medium_name'] = 'Dcs Rpt' + cols['short_name'] = 'DCS RPT' + cols['zone_id'] = '' + cols['rx_freq'] = '447.075' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '23' + cols['rx_dcs_invert'] = 'true' + cols['tx_power'] = '' + cols['tx_offset'] = '-5' + cols['tx_ctcss'] = '' + cols['tx_dcs'] = '23' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + result = RadioChannelChirp(cols, None, None).output(6) + self.assertEqual( + [ + '5', 'DCS RPT', '447.075000', '-', '5.000000', 'DTCS', '67.0', '67.0', + '023', 'RN', 'FM', '5.00', '', '', '', '', '', '', + ], result + ) \ No newline at end of file diff --git a/test/radio/test_d710.py b/test/radio/test_d710.py index 7e8dfd6..f36a0c2 100644 --- a/test/radio/test_d710.py +++ b/test/radio/test_d710.py @@ -23,7 +23,6 @@ def test_headers(self): def test_simplex(self): cols = dict() - cols['number'] = '1' cols['name'] = 'National 2m' cols['medium_name'] = 'Natl 2m' cols['short_name'] = 'NATL 2M' @@ -46,9 +45,8 @@ def test_simplex(self): '0000,00146.520000,005.00,00.000000,Off,88.5,88.5,023, ,Off,Off,FM,146.520000,005.00,NATL 2M', result ) - def test_vhf_simplex(self): + def test_uhf_simplex(self): cols = dict() - cols['number'] = '2' cols['name'] = 'National 70cm' cols['medium_name'] = 'Natl 70c' cols['short_name'] = 'NATL 70' @@ -73,7 +71,6 @@ def test_vhf_simplex(self): def test_vhf_repeater(self): cols = dict() - cols['number'] = '3' cols['name'] = 'Some Repeater' cols['medium_name'] = 'Some Rpt' cols['short_name'] = 'SOMERPT' @@ -95,9 +92,8 @@ def test_vhf_repeater(self): '0002,00145.310000,005.00,00.600000,T,100.0,88.5,023,-,Off,Off,FM,145.310000,005.00,SOME RPT', result ) - def test_pos_offset(self): + def test_positive_offset(self): cols = dict() - cols['number'] = '4' cols['name'] = 'Some Repeater' cols['medium_name'] = 'Some Rpt' cols['short_name'] = 'SOMERPT' @@ -122,7 +118,6 @@ def test_pos_offset(self): def test_dcs_repeater(self): cols = dict() - cols['number'] = '6' cols['name'] = 'Dcs Repeater' cols['medium_name'] = 'Dcs Rpt' cols['short_name'] = 'DCS RPT' @@ -139,14 +134,13 @@ def test_dcs_repeater(self): cols['digital_timeslot'] = '' cols['digital_color'] = '' cols['digital_contact_id'] = '' - result = RadioChannelD710(cols, None, None).output(3) + result = RadioChannelD710(cols, None, None).output(6) self.assertEqual( '0005,00447.075000,025.00,05.000000,DCS,88.5,88.5,165,-,Off,Off,FM,447.075000,025.00,DCS RPT', result ) def test_dcs_invert(self): cols = dict() - cols['number'] = '6' cols['name'] = 'Dcs Repeater' cols['medium_name'] = 'Dcs Rpt' cols['short_name'] = 'DCS RPT' @@ -163,7 +157,7 @@ def test_dcs_invert(self): cols['digital_timeslot'] = '' cols['digital_color'] = '' cols['digital_contact_id'] = '' - result = RadioChannelD710(cols, None, None).output(3) + result = RadioChannelD710(cols, None, None).output(6) self.assertEqual( '0005,00447.075000,025.00,05.000000,DCS,88.5,88.5,047,-,Off,Off,FM,447.075000,025.00,DCS RPT', result ) diff --git a/test/radio/test_d878.py b/test/radio/test_d878.py new file mode 100644 index 0000000..5782509 --- /dev/null +++ b/test/radio/test_d878.py @@ -0,0 +1,251 @@ +from src.ham.radio.d878.radio_channel_d878 import RadioChannelD878 +from src.ham.radio.dmr_contact import DmrContact +from src.ham.radio.dmr_id import DmrId +from test.radio.base_radio_test_setup import BaseRadioTestSetup + + +class D878Test(BaseRadioTestSetup): + def setUp(self): + self.radio_channel = RadioChannelD878.create_empty() + self.digital_contacts = dict() + cols = dict() + cols['digital_id'] = '00000' + cols['name'] = 'Analog' + cols['call_type'] = 'all' + digital_contact = DmrContact(cols) + self.digital_contacts[0] = digital_contact + + cols['digital_id'] = '54321' + cols['name'] = 'Contact1' + cols['call_type'] = 'group' + digital_contact = DmrContact(cols) + self.digital_contacts[54321] = digital_contact + + cols = dict() + cols['radio_id'] = '12345' + cols['name'] = 'N0CALL DMR' + self.digital_ids = {1: DmrId(cols)} + + def test_headers(self): + expected = [ + "No.", "Channel Name", "Receive Frequency", "Transmit Frequency", "Channel Type", "Transmit Power", + "Band Width", "CTCSS/DCS Decode", "CTCSS/DCS Encode", "Contact", "Contact Call Type", "Contact TG/DMR ID", + "Radio ID", "Busy Lock/TX Permit", "Squelch Mode", "Optional Signal", "DTMF ID", "2Tone ID", + "5Tone ID", "PTT ID", "Color Code", "Slot", "Scan List", "Receive Group List", "PTT Prohibit", + "Reverse", "Simplex TDMA", "Slot Suit", "AES Digital Encryption", "Digital Encryption", "Call Confirmation", + "Talk Around(Simplex)", "Work Alone", "Custom CTCSS", "2TONE Decode", "Ranging", "Through Mode", + "Digi APRS RX", "Analog APRS PTT Mode", "Digital APRS PTT Mode", "APRS Report Type", + "Digital APRS Report Channel", "Correct Frequency[Hz]", "SMS Confirmation", "Exclude channel from roaming", + "DMR MODE", "DataACK Disable", "R5toneBot", "R5ToneEot" + ] + generated = self.radio_channel.headers() + self.assertEqual(expected, generated) + + def test_simplex(self): + cols = dict() + cols['name'] = 'National 2m' + cols['medium_name'] = 'Natl 2m' + cols['short_name'] = 'NATL 2M' + cols['zone_id'] = '' + cols['rx_freq'] = '146.52' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = 'High' + cols['tx_offset'] = '' + cols['tx_ctcss'] = '' + cols['tx_dcs'] = '' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + channel = RadioChannelD878(cols, self.digital_contacts, self.digital_ids) + + result = channel.output(1) + expected = [ + "1", "National 2m", "146.52000", "146.52000", "A-Analog", "High", "12.5K", "Off", "Off", "Analog", + "All Call", "12345", "N0CALL DMR", "Off", "Carrier", "Off", "1", "1", "1", "Off", "1", "1", "None", + "None", "Off", "Off", "Off", "Off", "Normal Encryption", "Off", "Off", "Off", "Off", "251.1", "0", + "Off", "Off", "Off", "Off", "Off", "Off", "1", "0", "Off", "0", "0", "0", "0", "0" + ] + self.assertEqual( + expected, result + ) + + def test_uhf_simplex(self): + cols = dict() + cols['name'] = 'National 70cm' + cols['medium_name'] = 'Natl 70c' + cols['short_name'] = 'NATL 70' + cols['zone_id'] = '' + cols['rx_freq'] = '446.0' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = 'High' + cols['tx_offset'] = '' + cols['tx_ctcss'] = '' + cols['tx_dcs'] = '' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + channel = RadioChannelD878(cols, self.digital_contacts, self.digital_ids) + + result = channel.output(1) + expected = [ + "1", "National 70cm", "446.00000", "446.00000", "A-Analog", "High", "12.5K", "Off", "Off", "Analog", + "All Call", "12345", "N0CALL DMR", "Off", "Carrier", "Off", "1", "1", "1", "Off", "1", "1", "None", + "None", "Off", "Off", "Off", "Off", "Normal Encryption", "Off", "Off", "Off", "Off", "251.1", "0", + "Off", "Off", "Off", "Off", "Off", "Off", "1", "0", "Off", "0", "0", "0", "0", "0" + ] + self.assertEqual( + expected, result + ) + + def test_vhf_repeater(self): + cols = dict() + cols['name'] = 'Some Repeater' + cols['medium_name'] = 'Some Rpt' + cols['short_name'] = 'SOMERPT' + cols['zone_id'] = '' + cols['rx_freq'] = '145.310' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = 'High' + cols['tx_offset'] = '-0.6' + cols['tx_ctcss'] = '100.0' + cols['tx_dcs'] = '' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + channel = RadioChannelD878(cols, self.digital_contacts, self.digital_ids) + result = channel.output(3) + self.assertEqual( + [ + '3', 'Some Repeater', '145.31000', '144.71000', 'A-Analog', 'High', '12.5K', 'Off', '100.0', 'Analog', + 'All Call', '12345', 'N0CALL DMR', 'Off', 'Carrier', 'Off', '1', '1', '1', 'Off', '1', '1', 'None', + 'None', 'Off', 'Off', 'Off', 'Off', 'Normal Encryption', 'Off', 'Off', 'Off', 'Off', '251.1', '0', 'Off', + 'Off', 'Off', 'Off', 'Off', 'Off', '1', '0', 'Off', '0', '0', '0', '0', '0', + ] + , result + ) + + def test_positive_offset(self): + cols = dict() + cols['name'] = 'Some Repeater' + cols['medium_name'] = 'Some Rpt' + cols['short_name'] = 'SOMERPT' + cols['zone_id'] = '' + cols['rx_freq'] = '442.125' + cols['rx_ctcss'] = '127.3' + cols['rx_dcs'] = '' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = 'High' + cols['tx_offset'] = '5.0' + cols['tx_ctcss'] = '100.0' + cols['tx_dcs'] = '' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + channel = RadioChannelD878(cols, self.digital_contacts, self.digital_ids) + result = channel.output(4) + + self.assertEqual( + [ + '4', 'Some Repeater', '442.12500', '447.12500', 'A-Analog', 'High', '12.5K', '127.3', '100.0', 'Analog', + 'All Call', '12345', 'N0CALL DMR', 'Off', 'Carrier', 'Off', '1', '1', '1', 'Off', '1', '1', 'None', + 'None', 'Off', 'Off', 'Off', 'Off', 'Normal Encryption', 'Off', 'Off', 'Off', 'Off', '251.1', '0', 'Off', + 'Off', 'Off', 'Off', 'Off', 'Off', '1', '0', 'Off', '0', '0', '0', '0', '0', + ], result + ) + + def test_dcs_repeater(self): + cols = dict() + cols['name'] = 'Dcs Repeater' + cols['medium_name'] = 'Dcs Rpt' + cols['short_name'] = 'DCS RPT' + cols['zone_id'] = '' + cols['rx_freq'] = '447.075' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '165' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = 'High' + cols['tx_offset'] = '-5' + cols['tx_ctcss'] = '' + cols['tx_dcs'] = '165' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + channel = RadioChannelD878(cols, self.digital_contacts, self.digital_ids) + result = channel.output(5) + self.assertEqual( + [ + '5', 'Dcs Repeater', '447.07500', '442.07500', 'A-Analog', 'High', '12.5K', 'D165N', 'D165N', 'Analog', + 'All Call', '12345', 'N0CALL DMR', 'Off', 'Carrier', 'Off', '1', '1', '1', 'Off', '1', '1', 'None', + 'None', 'Off', 'Off', 'Off', 'Off', 'Normal Encryption', 'Off', 'Off', 'Off', 'Off', '251.1', '0', 'Off', + 'Off', 'Off', 'Off', 'Off', 'Off', '1', '0', 'Off', '0', '0', '0', '0', '0', + ], result + ) + + def test_dcs_invert(self): + cols = dict() + cols['name'] = 'Dcs Repeater' + cols['medium_name'] = 'Dcs Rpt' + cols['short_name'] = 'DCS RPT' + cols['zone_id'] = '' + cols['rx_freq'] = '447.075' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '23' + cols['rx_dcs_invert'] = 'true' + cols['tx_power'] = 'High' + cols['tx_offset'] = '-5' + cols['tx_ctcss'] = '' + cols['tx_dcs'] = '23' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + channel = RadioChannelD878(cols, self.digital_contacts, self.digital_ids) + result = channel.output(6) + self.assertEqual( + [ + '6', 'Dcs Repeater', '447.07500', '442.07500', 'A-Analog', 'High', '12.5K', 'D023I', 'D023N', 'Analog', + 'All Call', '12345', 'N0CALL DMR', 'Off', 'Carrier', 'Off', '1', '1', '1', 'Off', '1', '1', 'None', + 'None', 'Off', 'Off', 'Off', 'Off', 'Normal Encryption', 'Off', 'Off', 'Off', 'Off', '251.1', '0', 'Off', + 'Off', 'Off', 'Off', 'Off', 'Off', '1', '0', 'Off', '0', '0', '0', '0', '0', + ], result + ) + + def test_dmr(self): + cols = dict() + cols['name'] = 'Dmr Repeater' + cols['medium_name'] = 'Dmr Rpt' + cols['short_name'] = 'DMR RPT' + cols['zone_id'] = '' + cols['rx_freq'] = '447.075' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = 'High' + cols['tx_offset'] = '-5' + cols['tx_ctcss'] = '' + cols['tx_dcs'] = '' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '2' + cols['digital_color'] = '3' + cols['digital_contact_id'] = '54321' + channel = RadioChannelD878(cols, self.digital_contacts, self.digital_ids) + result = channel.output(6) + self.assertEqual( + [ + '6', 'Dmr Repeater', '447.07500', '442.07500', 'D-Digital', 'High', '12.5K', 'Off', 'Off', 'Contact1', + 'Group Call', '54321', 'N0CALL DMR', 'Always', 'Carrier', 'Off', '1', '1', '1', 'Off', '3', '2', 'None', + 'None', 'Off', 'Off', 'Off', 'Off', 'Normal Encryption', 'Off', 'On', 'Off', 'Off', '251.1', '0', 'Off', + 'Off', 'Off', 'Off', 'Off', 'Off', '1', '0', 'Off', '0', '1', '0', '0', '0', + ], result + ) diff --git a/test/test_migration_manager.py b/test/test_migration_manager.py index a6adaa2..c8030a5 100644 --- a/test/test_migration_manager.py +++ b/test/test_migration_manager.py @@ -107,3 +107,17 @@ def test_migrate_with_no_files(self): self.manager.migrate() self.assertTrue(True) + + def test_migrate_four(self): + self.manager._migrate_one() + self.manager._migrate_two() + self.manager._migrate_three() + self.manager._migrate_four() + + f = FileUtil.open_file('in/input.csv', 'r') + dict_reader = DictReader(f) + self.assertFalse('number' in dict_reader.fieldnames) + f.close() + + def text_expected_col_definitions(self): + self.fail("test not implemented") diff --git a/test/test_validator.py b/test/test_validator.py index ef959e3..74e13be 100644 --- a/test/test_validator.py +++ b/test/test_validator.py @@ -1,5 +1,6 @@ import logging +from src.ham.radio.dmr_contact import DmrContact from src.ham.util.file_util import FileUtil from src.ham.util.validator import Validator from test.base_test_setup import BaseTestSetup @@ -16,6 +17,25 @@ def setUp(self): FileUtil.safe_create_dir('in') FileUtil.safe_create_dir('out') + cols = dict() + cols['name'] = 'National 2m' + cols['medium_name'] = 'Natl 2m' + cols['short_name'] = 'NATL 2M' + cols['zone_id'] = '' + cols['rx_freq'] = '146.52' + cols['rx_ctcss'] = '' + cols['rx_dcs'] = '' + cols['rx_dcs_invert'] = '' + cols['tx_power'] = 'High' + cols['tx_offset'] = '' + cols['tx_ctcss'] = '' + cols['tx_dcs'] = '' + cols['tx_dcs_invert'] = '' + cols['digital_timeslot'] = '' + cols['digital_color'] = '' + cols['digital_contact_id'] = '' + self.radio_cols = cols + def test_validate_no_files_exist(self): errors = Validator.validate_files_exist() self.assertEqual(5, len(errors)) @@ -35,27 +55,9 @@ def test_only_some_files_exist(self): self.assertEqual(4, len(errors)) def test_validate_radio_channel_name_dupe(self): - cols = dict() - cols['number'] = '1' - cols['name'] = 'National 2m' - cols['medium_name'] = 'Natl 2m' - cols['short_name'] = 'NATL 2M' - cols['zone_id'] = '' - cols['rx_freq'] = '146.52' - cols['rx_ctcss'] = '' - cols['rx_dcs'] = '' - cols['rx_dcs_invert'] = '' - cols['tx_power'] = '' - cols['tx_offset'] = '' - cols['tx_ctcss'] = '' - cols['tx_dcs'] = '' - cols['tx_dcs_invert'] = '' - cols['digital_timeslot'] = '' - cols['digital_color'] = '' - cols['digital_contact_id'] = '' - errors = self.validator.validate_radio_channel(cols, 1, "FILE_NO_EXIST_UNITTEST") + errors = self.validator.validate_radio_channel(self.radio_cols, 1, "FILE_NO_EXIST_UNITTEST", {}) self.assertEqual(len(errors), 0) - errors = self.validator.validate_radio_channel(cols, 2, "FILE_NO_EXIST_UNITTEST") + errors = self.validator.validate_radio_channel(self.radio_cols, 2, "FILE_NO_EXIST_UNITTEST", {}) self.assertEqual(len(errors), 3) short_found = False @@ -70,4 +72,60 @@ def test_validate_radio_channel_name_dupe(self): self.assertTrue(medium_found) self.assertTrue(long_found) + def test_validate_missing_contact(self): + radio_cols = dict() + radio_cols['number'] = '1' + radio_cols['name'] = 'Test channel' + radio_cols['medium_name'] = 'TestChan' + radio_cols['short_name'] = 'TestChn' + radio_cols['zone_id'] = '' + radio_cols['rx_freq'] = '146.52' + radio_cols['rx_ctcss'] = '' + radio_cols['rx_dcs'] = '' + radio_cols['rx_dcs_invert'] = '' + radio_cols['tx_power'] = 'High' + radio_cols['tx_offset'] = '0.6' + radio_cols['tx_ctcss'] = '' + radio_cols['tx_dcs'] = '' + radio_cols['tx_dcs_invert'] = '' + radio_cols['digital_timeslot'] = '1' + radio_cols['digital_color'] = '2' + radio_cols['digital_contact_id'] = '314' + + digital_contact_cols = dict() + digital_contact_cols['number'] = '1' + digital_contact_cols['digital_id'] = '314' + digital_contact_cols['name'] = 'Digi Contact' + digital_contact_cols['call_type'] = 'Group' + digital_contacts = { + 314: DmrContact(digital_contact_cols), + } + errors = self.validator.validate_radio_channel(radio_cols, 1, 'FILE_NO_EXIST_UNITTEST', digital_contacts) + self.assertEqual(len(errors), 0) + self.validator.flush_names() + + errors = self.validator.validate_radio_channel(radio_cols, 1, 'FILE_NO_EXIST_UNITTEST', {}) + self.assertEqual(len(errors), 1) + found = errors[0].args[0].find('Cannot find digital contact') + self.assertEqual(found, 0) + + def test_ignore_extra_column(self): + self.radio_cols['foo'] = '1' + errors = self.validator.validate_radio_channel(self.radio_cols, 1, "FILE_NO_EXIST_UNITTEST", {}) + self.assertEqual(len(errors), 0) + + def test_validate_tx_power(self): + self.radio_cols['tx_power'] = 'mega' + errors = self.validator.validate_radio_channel(self.radio_cols, 1, "FILE_NO_EXIST_UNITTEST", {}) + self.assertEqual(len(errors), 1) + found = errors[0].args[0].find('Transmit power (`tx_power`) invalid') + self.assertEqual(found, 0) + + def test_validate_tx_power_not_present(self): + self.radio_cols['tx_power'] = '' + errors = self.validator.validate_radio_channel(self.radio_cols, 1, "FILE_NO_EXIST_UNITTEST", {}) + self.assertEqual(len(errors), 1) + found = errors[0].args[0].find('Transmit power (`tx_power`) invalid') + self.assertEqual(found, 0) + diff --git a/test/test_version.py b/test/test_version.py new file mode 100644 index 0000000..1267722 --- /dev/null +++ b/test/test_version.py @@ -0,0 +1,13 @@ +import logging +import unittest + +from src import radio_sync_version + + +class VersionTest(unittest.TestCase): + def test_version_present(self): + self.assertIsNotNone(radio_sync_version.version) + + def test_version_ci(self): + self.assertNotEqual(radio_sync_version.version, 'DEVELOPMENT') + logging.critical(f"Version found: {radio_sync_version.version}")