class RadioImportChirp:
    def __init__(self):
        self.validator = Validator()
        self.validator.flush_names()

    def run_import(self, cols, import_path):
        radio_channel_cols = RadioChannel.generate_empty_dict()
        radio_channel_cols['name'] = cols['Name']
        radio_channel_cols['medium_name'] = cols['Name']
        radio_channel_cols['short_name'] = cols['Name']
        radio_channel_cols['zone_id'] = ''
        radio_channel_cols['rx_freq'] = cols['Frequency']
        radio_channel_cols['rx_ctcss'] = ''

        if cols['Tone'] == 'Tone' or cols['Tone'] == 'TSQL':
            radio_channel_cols['tx_ctcss'] = cols['rToneFreq']

        if cols['Tone'] == 'TSQL':
            radio_channel_cols['rx_ctcss'] = cols['cToneFreq']

        if cols['Tone'] == 'DTCS':
            radio_channel_cols['rx_dcs'] = cols['DtcsCode']
            radio_channel_cols['tx_dcs'] = cols['DtcsCode']
            if cols['DtcsPolarity'][0] == 'R':
                radio_channel_cols['rx_dcs_invert'] = 'True'
            if cols['DtcsPolarity'][1] == 'R':
                radio_channel_cols['tx_dcs_invert'] = 'True'
        radio_channel_cols['tx_power'] = 'High'

        if float(cols['Offset']) != 0:
            neg = 1
            if cols['Duplex'] == '-':
                neg = -1
            radio_channel_cols['tx_offset'] = float(cols['Offset']) * neg

        line_num = int(cols['Location']) + 1

        errors = self.validator.validate_radio_channel(radio_channel_cols,
                                                       line_num, import_path,
                                                       None, None)
        for err in errors:
            logging.error(
                f'\t\tline:{err.line_num} validation error: {err.message}')
            raise ValidationError("Import line failed to parse.", line_num,
                                  import_path)

        channel = RadioChannelChirp(radio_channel_cols, None, None)
        return channel
Exemple #2
0
class ValidatorTest(BaseTestSetup):
	def setUp(self):
		super().setUp()
		self.validator = Validator()
		self.validator.flush_names()
		logging.getLogger().setLevel(logging.CRITICAL)
		FileUtil.safe_delete_dir('in')
		FileUtil.safe_delete_dir('out')
		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'] = ''
		cols['latitude'] = ''
		cols['longitude'] = ''
		self.radio_cols = cols

	def test_validate_no_files_exist(self):
		errors = Validator.validate_files_exist()
		self.assertEqual(5, len(errors))

	def test_validate_files_exist(self):
		files = ['input.csv', 'digital_contacts.csv', 'dmr_id.csv', 'zones.csv', 'user.csv']
		for filename in files:
			f = PathManager.open_input_file(filename, 'w+')
			f.close()
		errors = Validator.validate_files_exist()
		self.assertEqual(0, len(errors))

	def test_only_some_files_exist(self):
		f = PathManager.open_input_file('input.csv', 'w+')
		f.close()
		errors = Validator.validate_files_exist()
		self.assertEqual(4, len(errors))

	def test_validate_radio_channel_name_dupe(self):
		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(self.radio_cols, 2, 'FILE_NO_EXIST_UNITTEST', {}, {})
		self.assertEqual(len(errors), 3)

		short_found = False
		medium_found = False
		long_found = False
		for err in errors:
			short_found = err.args[0].find('Collision in short_name') or short_found
			medium_found = err.args[0].find('Collision in medium_name') or medium_found
			long_found = err.args[0].find('Collision in name') or long_found

		self.assertTrue(short_found)
		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'
		radio_cols['latitude'] = ''
		radio_cols['longitude'] = ''

		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)

	def test_validate_zone_not_present(self):
		self.radio_cols['zone_id'] = '1'
		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('Zone ID not found:')
		self.assertEqual(found, 0)

	def test_validate_zone_present(self):
		self.radio_cols['zone_id'] = '1'
		zone = RadioZone({
			'number': 1,
			'name': 'Zone 1',
		})
		zones = {1: zone}
		errors = self.validator.validate_radio_channel(self.radio_cols, 1, 'FILE_NO_EXIST_UNITTEST', {}, zones)
		self.assertEqual(len(errors), 0)

	def test_validate_lat_long_present(self):
		self.radio_cols['latitude'] = '0.0'
		self.radio_cols['longitude'] = '0.0'
		errors = self.validator.validate_radio_channel(self.radio_cols, 1, 'LAT_LONG_UNITTEST', {}, {})
		self.assertEqual(len(errors), 0)

	def test_validate_lat_long_not_present(self):
		self.radio_cols['latitude'] = ''
		self.radio_cols['longitude'] = ''
		errors = self.validator.validate_radio_channel(self.radio_cols, 1, 'LAT_LONG_UNITTEST', {}, {})
		self.assertEqual(len(errors), 0)

	def test_validate_lat_only_present(self):
		self.radio_cols['latitude'] = '0.0'
		self.radio_cols['longitude'] = ''
		errors = self.validator.validate_radio_channel(self.radio_cols, 1, 'LAT_LONG_UNITTEST', {}, {})
		self.assertEqual(len(errors), 1)
		found = errors[0].args[0].find('Only one of latitude or longitude provided')
		self.assertEqual(found, 0)

	def test_validate_long_only_present(self):
		self.radio_cols['latitude'] = ''
		self.radio_cols['longitude'] = '0.0'
		errors = self.validator.validate_radio_channel(self.radio_cols, 1, 'LAT_LONG_UNITTEST', {}, {})
		self.assertEqual(len(errors), 1)
		found = errors[0].args[0].find('Only one of latitude or longitude provided')
		self.assertEqual(found, 0)

	def test_validate_bad_lat(self):
		self.radio_cols['latitude'] = '-91.0'
		self.radio_cols['longitude'] = '0.0'
		errors = self.validator.validate_radio_channel(self.radio_cols, 1, 'LAT_LONG_UNITTEST', {}, {})
		self.assertEqual(len(errors), 1)
		found = errors[0].args[0].find('Latitude must be between')
		self.assertEqual(found, 0)

	def test_validate_bad_long(self):
		self.radio_cols['latitude'] = '0.0'
		self.radio_cols['longitude'] = '-181.0'
		errors = self.validator.validate_radio_channel(self.radio_cols, 1, 'LAT_LONG_UNITTEST', {}, {})
		self.assertEqual(len(errors), 1)
		found = errors[0].args[0].find('Longitude must be between')
		self.assertEqual(found, 0)
Exemple #3
0
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 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 
		using a master set of csv files that have all the relevant information.

		How to use: Start by running the Wizard to generate the `in` directory and `out` directory.
		For more information about these files, please see:
			https://github.com/n2qzshce/ham-radio-sync/blob/master/INPUTS_OUTPUTS_SYNCING.md
			
		{dangerous_ops_info}

		Functions:
			(Dangerous Operation) Cleanup - Will delete the contents of your `in` and `out` directory.
			(Dangerous Operation) Wizard - Creates sample input files to help you get started.
			(Dangerous Operation) Migrate - If you have recently updated ham radio sync, this can add any new columns that
				may be needed. Migrations will rename your existing files by adding a `.bak` extension. You can run the
				'migrations cleanup' to remove these files.
			Create radio plugs - Will generate CSVs to import for the radios you have selected.
			Debug logging - This will enable chattier logging. Generally only needed if you have been instructed to do so.
		''')

    def generate_all_declared(self):
        file_errors = self._validator.validate_files_exist()
        self._validator.flush_names()
        if len(file_errors) > 0:
            return False

        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)

        for radio in radio_types.radio_choices():
            radio_folder = PathManager.get_output_path(radio)
            if not os.path.exists(radio_folder):
                continue
            logging.info(f'Deleting old output folder `{radio}`')
            FileUtil.safe_delete_dir(radio_folder)

        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()
        user, user_data_errors = self._generate_user_data()
        preload_errors = digi_contact_errors + dmr_id_errors + zone_errors + user_data_errors

        feed = PathManager.open_input_file('input.csv', 'r')
        csv_reader = csv.DictReader(feed)

        radio_channel_errors = []
        radio_channels = []
        line_num = 1
        for line in csv_reader:
            line_errors = self._validator.validate_radio_channel(
                line, line_num, feed.name, digital_contacts, zones)
            radio_channel_errors += line_errors
            line_num += 1

            if len(line_errors) > 0:
                continue

            radio_channel = RadioChannel(line, digital_contacts, dmr_ids)
            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)
        feed.close()

        all_errors = preload_errors + radio_channel_errors
        if len(all_errors) > 0:
            logging.error('--- VALIDATION ERRORS, CANNOT CONTINUE ---')
            for err in all_errors:
                logging.error(
                    f'\t\tfile: `{err.file_name}` line:{err.line_num} validation error: {err.message}'
                )
            return False
        else:
            logging.info(
                'File validation complete, no obvious formatting errors found')

        radio_files = dict()
        headers_gen = RadioChannel.create_empty()
        FileUtil.safe_create_dir('out')

        channel_numbers = dict()
        for radio in self.radio_list:
            radio_casted = RadioChannelBuilder.casted(headers_gen, radio)
            FileUtil.safe_create_dir(f'out/{radio}')
            logging.info(f'Generating for radio type `{radio}`')

            if radio_casted.skip_radio_csv():
                logging.info(
                    f'`{radio}` uses special output style. Skipping channels csv'
                )
                continue
            output = RadioWriter.output_writer(f'{radio}/{radio}_channels.csv',
                                               '\r\n')
            file_headers = radio_casted.headers()
            output.writerow(file_headers)
            radio_files[radio] = output
            channel_numbers[radio] = 1

        logging.info('Processing radio channels')
        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}')
            line += 1
            for radio in self.radio_list:
                if radio not in radio_files.keys():
                    continue

                if not radio_types.supports_dmr(
                        radio) and radio_channel.is_digital():
                    continue

                casted_channel = RadioChannelBuilder.casted(
                    radio_channel, radio)

                input_data = casted_channel.output(channel_numbers[radio])
                radio_files[radio].writerow(input_data)
                channel_numbers[radio] += 1

        additional_data = RadioAdditional(radio_channels, dmr_ids,
                                          digital_contacts, zones, user)
        for radio in self.radio_list:
            if radio in radio_files.keys():
                radio_files[radio].close()
            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)'''
                     )
        return True

    def _generate_digital_contact_data(self):
        logging.info('Processing digital contacts')
        feed = PathManager.open_input_file('digital_contacts.csv', 'r')
        csv_feed = csv.DictReader(feed)
        digital_contacts = dict()
        errors = []

        line_num = 1
        for line in csv_feed:
            logging.debug(f'Processing line {line_num}: `{line}`')
            line_errors = self._validator.validate_digital_contact(
                line, 1, feed.name)
            errors += line_errors
            line_num += 1
            if len(line_errors) != 0:
                continue
            contact = DmrContact(line)
            digital_contacts[contact.digital_id.fmt_val()] = contact
        feed.close()
        return digital_contacts, errors

    def _generate_dmr_id_data(self):
        logging.info('Processing dmr ids')
        feed = PathManager.open_input_file('dmr_id.csv', 'r')
        csv_feed = csv.DictReader(feed)
        dmr_ids = dict()
        errors = []
        line_num = 0
        for line in csv_feed:
            logging.debug(f'Processing line {line_num}: `{line}`')
            line_num += 1
            line_errors = self._validator.validate_dmr_id(
                line, line_num, feed.name)
            errors += line_errors
            if len(line_errors) != 0:
                continue
            dmr_id = DmrId(line)
            dmr_ids[line_num] = dmr_id
        feed.close()
        return dmr_ids, errors

    def _generate_zone_data(self):
        logging.info('Processing zones')
        feed = PathManager.open_input_file('zones.csv', 'r')
        csv_feed = csv.DictReader(feed)
        zones = dict()
        errors = []
        line_num = 1
        for line in csv_feed:
            logging.debug(f'Processing line {line_num}: `{line}`')
            line_errors = self._validator.validate_radio_zone(
                line, line_num, feed.name)
            errors += line_errors
            line_num += 1
            if len(line_errors) != 0:
                continue
            zone = RadioZone(line)
            zones[zone.number.fmt_val()] = zone
        feed.close()
        return zones, errors

    def _generate_user_data(self):
        logging.info('Processing dmr IDs. This step can take a while.')
        feed = PathManager.open_input_file('user.csv', 'r')
        csv_feed = csv.DictReader(feed)
        users = dict()
        errors = []
        rows_processed = 0
        for line in csv_feed:
            line_errors = self._validator.validate_dmr_user(
                line, rows_processed + 1, feed.name)
            errors += line_errors
            rows_processed += 1
            if len(line_errors) != 0:
                continue
            zone = DmrUser(line)
            users[zone.radio_id.fmt_val()] = zone
            logging.debug(f'Writing user row {rows_processed}')
            if rows_processed % file_util.USER_LINE_LOG_INTERVAL == 0:
                logging.info(f'Processed {rows_processed} DMR users')
        feed.close()
        return users, errors