pyjs8call.settings

JS8Call settings.

Functions for reading and writing settings, including configuration file convenience functions.

   1# MIT License
   2# 
   3# Copyright (c) 2022-2024 Simply Equipped
   4# 
   5# Permission is hereby granted, free of charge, to any person obtaining a copy
   6# of this software and associated documentation files (the "Software"), to deal
   7# in the Software without restriction, including without limitation the rights
   8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   9# copies of the Software, and to permit persons to whom the Software is
  10# furnished to do so, subject to the following conditions:
  11# 
  12# The above copyright notice and this permission notice shall be included in all
  13# copies or substantial portions of the Software.
  14# 
  15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21# SOFTWARE.
  22
  23'''JS8Call settings.
  24
  25Functions for reading and writing settings, including configuration file convenience functions.
  26'''
  27
  28__docformat__ = 'google'
  29
  30
  31import os
  32import time
  33import configparser
  34
  35import pyjs8call
  36from pyjs8call import Message
  37
  38
  39class Settings:
  40    '''Settings function container.
  41    
  42    This class is initilized by pyjs8call.client.Client.
  43    '''
  44    
  45    def __init__(self, client):
  46        '''Initialize settings object.
  47
  48        Returns:
  49            pyjs8call.client.Settings: Constructed setting object
  50        '''
  51        self._client = client
  52        self._daily_restart_schedule = None
  53        self.loaded_settings = None
  54        '''Loaded settings container (see python3 configparser for more information)'''
  55
  56        self._settings_map = {
  57            'station' : {
  58                'callsign': lambda value: self.set_station_callsign(value),
  59                'grid': lambda value: self.set_station_grid(value),
  60                'speed': lambda value: self.set_speed(value),
  61                'freq': lambda value: self.set_freq(value),
  62                'frequency': lambda value: self.set_freq(value),
  63                'offset': lambda value: self.set_offset(value),
  64                'info': lambda value: self.set_station_info(value),
  65                'append_pyjs8call_info': lambda value: self.append_pyjs8call_to_station_info(),
  66                'daily_restart': lambda value: self.enable_daily_restart(value) if value else self.disable_daily_restart()
  67            },
  68            'general': {
  69                'groups': lambda value: self.set_groups(value),
  70                'multi_speed_decode': lambda value: self.enable_multi_decode() if value else self.disable_multi_decode(),
  71                'autoreply_on_at_startup': lambda value: self.enable_autoreply_startup() if value else self.disable_autoreply_startup(),
  72                'autoreply_confirmation': lambda value: self.enable_autoreply_confirmation() if value else self.disable_autoreply_confirmation(),
  73                'allcall': lambda value: self.enable_allcall() if value else self.disable_allcall(),
  74                'reporting': lambda value: self.enable_reporting() if value else self.disable_reporting(),
  75                'transmit': lambda value: self.enable_transmit() if value else self.disable_transmit(),
  76                'idle_timeout': lambda value: self.set_idle_timeout(value),
  77                'distance_units': lambda value: self.set_distance_units(value)
  78            },
  79            'heartbeat': {
  80                'enable': lambda value: self._client.heartbeat.enable() if value else self._client.heartbeat.disable(),
  81                'interval': lambda value: self.set_heartbeat_interval(value),
  82                'acknowledgements': lambda value: self.enable_heartbeat_acknowledgements() if value else self.disable_heartbeat_acknowledgements(),
  83                'pause_during_qso': lambda value: self.pause_heartbeat_during_qso() if value else self.allow_heartbeat_during_qso()
  84            },
  85            'profile': {
  86                'profile': lambda value: self.set_profile(value),
  87                'set_profile_on_exit': lambda value: self._client.set_profile_on_exit(value)
  88            },
  89            'highlight': {
  90                'primary_words': lambda value: self.set_primary_highlight_words(value),
  91                'secondary_words': lambda value: self.set_secondary_highlight_words(value)
  92            },
  93            'spots': {
  94                'watch_stations': lambda value: self._client.spots.set_watched_stations(value),
  95                'watch_groups': lambda value: self._client.spots.set_watched_groups(value)
  96            },
  97            'notifications': {
  98                'enable': lambda value: self._client.notifications.enable() if value else self._client.notifications.disable(),
  99                'smtp_server': lambda value: self._client.notifications.set_smtp_server(value),
 100                'smtp_port': lambda value: self._client.notifications.set_smtp_server_port(value),
 101                'smtp_email_address': lambda value: self._client.notifications.set_smtp_email_address(value),
 102                'smtp_password': lambda value: self._client.notifications.set_smtp_password(value),
 103                'notification_email_address': lambda value: self._client.notifications.set_email_destination(value),
 104                'notification_email_subject': lambda value: self._client.notifications.set_email_subject(value),
 105                'incoming': lambda value: self._client.notifications.enable_incoming() if value else self._client.notifications.disable_incoming(),
 106                'spots': lambda value: self._client.notifications.enable_spots() if value else self._client.notifications.disable_spots(),
 107                'station_spots': lambda value: self._client.notifications.enable_station_spots() if value else self._client.notifications.disable_station_spots(),
 108                'group_spots': lambda value: self._client.notifications.enable_group_spots() if value else self._client.notifications.disable_group_spots()
 109            }
 110        }
 111
 112        # settings set via js8call config file
 113        self._pre_start_settings = {
 114            'station' : [
 115                'callsign',
 116                'speed'
 117            ],
 118            'general': [
 119                'groups', 
 120                'multi_speed_decode',
 121                'autoreplay_on_at_startup',
 122                'autoreply_confirmation',
 123                'allcall',
 124                'reporting',
 125                'transmit',
 126                'idle_timeout',
 127                'distance_units'
 128            ],
 129            'heartbeat': [
 130                'interval',
 131                'acknowledgements',
 132                'pause_during_qso'
 133            ],
 134            'profile': [
 135                'profile',
 136                'set_profile_on_exit'
 137            ],
 138            'highlight': [
 139                'primary_words',
 140                'secondary_words'
 141            ],
 142            'spots': [
 143            ],
 144            'notifications': [
 145            ]
 146        }
 147
 148    def load(self, settings_path):
 149        '''Load pyjs8call settings from file.
 150
 151        The settings file referenced here is specific to pyjs8call, and is not the same as the JS8Call configuration file. The pyjs8call settings file is not required.
 152
 153        This function must be called before calling *client.start()*. Settings that must be set before or after starting the JS8Call application are handled automatically. Settings that affect the JS8Call config file are set immediately. All other settings are set after *client.start()* is called.
 154
 155        Example settings file:
 156
 157        ```
 158        [station]
 159
 160        callsign=CALL0SIGN
 161        grid=EM19
 162        speed=normal
 163        freq=7078000
 164        offset=1750
 165        info=QDX 5W, DIPOLE 30FT
 166        append_pyjs8call_info=true
 167        daily_restart=02:00
 168
 169        [general]
 170
 171        groups=@TTP, @AMRRON
 172        multi_speed_decode=true
 173        autoreply_on_at_startup=true
 174        autoreply_confirmation=false
 175        allcall=true
 176        reporting=true
 177        transmit=true
 178        idle_timeout=0
 179        distance_units=miles
 180
 181        [heartbeat]
 182
 183        enable=true
 184        interval=15
 185        acknowledgements=true
 186        pause_during_qso=true
 187
 188        [profile]
 189
 190        profile=Default
 191        set_profile_on_exit=Default
 192
 193        [highlight]
 194
 195        primary_words=KT7RUN, OH8STN
 196        secondary_words=simplyequipped
 197
 198        [spots]
 199
 200        watch_stations=KT7RUN, OH8STN
 201        watch_groups=@TTP, @AMRRON
 202
 203        [notifications]
 204
 205        enable=true
 206        smtp_server=smtp.gmail.com
 207        smtp_port=465
 208        smtp_email_address=email@address.com
 209        smtp_password=APP_PASSWORD
 210        notification_email_address=0123456789@vtext.com
 211        notification_email_subject=
 212        incoming=true
 213        spots=false
 214        station_spots=true
 215        group_spots=true
 216        ```
 217
 218        Args:
 219            settings_path (str): Relative or absolute path to settings file
 220
 221        Raises:
 222            OSError: Specified settings file not found
 223        '''
 224        settings_path = os.path.expanduser(settings_path)
 225        settings_path = os.path.abspath(settings_path)
 226
 227        if not os.path.exists(settings_path):
 228            raise FileNotFoundError('Specified settings file not found: {}'.format(settings_path))
 229
 230        self.loaded_settings = configparser.ConfigParser(interpolation = None)
 231        self.loaded_settings.read(settings_path)
 232
 233        self.apply_loaded_settings()
 234
 235    def apply_loaded_settings(self, post_start=False):
 236        '''Apply loaded pyjs8call settings.
 237
 238        This function is called internally by *load_settings()* and *client.start()*.
 239
 240        Args:
 241            post_start (bool): Post start processing if True, pre start processing if False, defaults to False
 242        '''
 243        for section in self.loaded_settings.sections():
 244            #skip unsupported section
 245            if section not in self._settings_map:
 246                continue
 247
 248            for key, value in self.loaded_settings[section].items():
 249                # skip unsupported key
 250                if key not in self._settings_map[section]:
 251                    continue
 252
 253                # skip post start settings during pre start processing
 254                if not post_start and key in self._pre_start_settings[section]:
 255                    value = self._parse_loaded_value(value)
 256                    self._settings_map[section][key](value)
 257
 258                # skip pre start settings during post start processing
 259                if post_start and not key in self._pre_start_settings[section]:
 260                    value = self._parse_loaded_value(value)
 261                    self._settings_map[section][key](value)
 262
 263    def _parse_loaded_value(self, value):
 264        '''Parse setting value from string to Python type.
 265        
 266        Args:
 267            value (str): Setting value to parse
 268        '''
 269        if value is None:
 270            return None
 271        elif value.lower() in ['true', 'yes']:
 272            return True
 273        elif value.lower() in ['false', 'no']:
 274            return False
 275        elif value.lower() in ('none', '', 'nil', 'nill', 'null'):
 276            return None
 277        elif value.isnumeric():
 278            return int(value)
 279        else:
 280            return value
 281    
 282    def enable_heartbeat_networking(self):
 283        '''Enable heartbeat networking via config file.
 284        
 285        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 286        
 287        Note that this function disables JS8Call application heartbeat networking via the config file. To enable the pyjs8call heartbeat network messaging module see *client.heartbeat.enable()*.
 288        '''
 289        self._client.config.set('Common', 'SubModeHB', 'true')
 290
 291    def disable_heartbeat_networking(self):
 292        '''Disable heartbeat networking via config file.
 293        
 294        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 295        
 296        Note that this function disables JS8Call application heartbeat networking via the config file. To disable the pyjs8call heartbeat network messaging module see *client.heartbeat.disable()*.
 297        '''
 298        self._client.config.set('Common', 'SubModeHB', 'false')
 299
 300    def heartbeat_networking_enabled(self):
 301        '''Whether heartbeat networking enabled in config file.
 302        
 303        Returns:
 304            bool: True if heartbeat networking enabled, False otherwise
 305        '''
 306        return self._client.config.get('Common', 'SubModeHB', bool)
 307
 308    def get_heartbeat_interval(self):
 309        '''Get heartbeat networking interval.
 310        
 311        Returns:
 312            int: Heartbeat networking time interval in minutes
 313        '''
 314        return self._client.config.get('Common', 'HBInterval', int)
 315        
 316    def set_heartbeat_interval(self, interval):
 317        '''Set the heartbeat networking interval.
 318        
 319        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 320
 321        Args:
 322            interval (int): New heartbeat networking time interval in minutes
 323        
 324        Returns:
 325            int: Current heartbeat networking time interval in minutes
 326        '''
 327        return self._client.config.set('Common', 'HBInterval', interval)
 328        
 329    def enable_heartbeat_acknowledgements(self):
 330        '''Enable heartbeat acknowledgements via config file.
 331        
 332        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 333
 334        Also enables JS8Call heartbeat networking, since heartbeat acknowledgements will not be enabled without heartbeat networking enabled first. This only enables the feature within JS8Call, and does not casue heartbeats to be sent.
 335        '''
 336        self.enable_heartbeat_networking()
 337        self._client.config.set('Common', 'SubModeHBAck', 'true')
 338
 339    def disable_heartbeat_acknowledgements(self):
 340        '''Disable heartbeat acknowledgements via config file.
 341        
 342        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 343        '''
 344        self._client.config.set('Common', 'SubModeHBAck', 'false')
 345
 346    def heartbeat_acknowledgements_enabled(self):
 347        '''Whether heartbeat acknowledgements enabled in config file.
 348        
 349        Returns:
 350            bool: True if heartbeat acknowledgements enabled, False otherwise
 351        '''
 352        return self._client.config.get('Common', 'SubModeHBAck', bool)
 353        
 354    def pause_heartbeat_during_qso(self):
 355        '''Pause heartbeat messages during QSO via config file.
 356        
 357        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 358        '''
 359        self._client.config.set('Configuration', 'HeartbeatQSOPause', 'true')
 360
 361    def allow_heartbeat_during_qso(self):
 362        '''Allow heartbeat messages during QSO via config file.
 363        
 364        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 365        '''
 366        self._client.config.set('Configuration', 'HeartbeatQSOPause', 'false')
 367
 368    def heartbeat_during_qso_paused(self):
 369        '''Whether heartbeat messages paused during QSO in config file.
 370        
 371        Returns:
 372            bool: True if heartbeat messages paused during QSO, False otherwise
 373        '''
 374        return self._client.config.get('Configuration', 'HeartbeatQSOPause', bool)
 375
 376    def enable_multi_decode(self):
 377        '''Enable multi-speed decoding via config file.
 378        
 379        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 380        '''
 381        self._client.config.set('Common', 'SubModeHBMultiDecode', 'true')
 382
 383    def disable_multi_decode(self):
 384        '''Disable multi-speed decoding via config file.
 385        
 386        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 387        '''
 388        self._client.config.set('Common', 'SubModeMultiDecode', 'false')
 389
 390    def multi_decode_enabled(self):
 391        '''Whether multi-decode enabled in config file.
 392        
 393        Returns:
 394            bool: True if multi-decode enabled, False otherwise
 395        '''
 396        return self._client.config.get('Common', 'SubModeMultiDecode', bool)
 397
 398    def enable_autoreply_startup(self):
 399        '''Enable autoreply on start-up via config file.
 400        
 401        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 402        '''
 403        self._client.config.set('Configuration', 'AutoreplyOnAtStartup', 'true')
 404
 405    def disable_autoreply_startup(self):
 406        '''Disable autoreply on start-up via config file.
 407        
 408        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 409        '''
 410        self._client.config.set('Configuration', 'AutoreplyOnAtStartup', 'false')
 411
 412    def autoreply_startup_enabled(self):
 413        '''Whether autoreply enabled at start-up in config file.
 414        
 415        Returns:
 416            bool: True if autoreply is enabled at start-up, False otherwise
 417        '''
 418        return self._client.config.get('Configuration', 'AutoreplyOnAtStartup', bool)
 419
 420    def enable_autoreply_confirmation(self):
 421        '''Enable autoreply confirmation via config file.
 422        
 423        When running headless the autoreply confirmation dialog box will be inaccessible.
 424        
 425        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 426        '''
 427        self._client.config.set('Configuration', 'AutoreplyConfirmation', 'true')
 428
 429    def disable_autoreply_confirmation(self):
 430        '''Disable autoreply confirmation via config file.
 431        
 432        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 433        '''
 434        self._client.config.set('Configuration', 'AutoreplyConfirmation', 'false')
 435
 436    def autoreply_confirmation_enabled(self):
 437        '''Whether autoreply confirmation enabled in config file.
 438        
 439        Returns:
 440            bool: True if autoreply confirmation enabled, False otherwise
 441        '''
 442        return self._client.config.get('Configuration', 'AutoreplyConfirmation', bool)
 443
 444    def enable_allcall(self):
 445        '''Enable @ALLCALL participation via config file.
 446        
 447        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 448        '''
 449        self._client.config.set('Configuration', 'AvoidAllcall', 'false')
 450
 451    def disable_allcall(self):
 452        '''Disable @ALLCALL participation via config file.
 453        
 454        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 455        '''
 456        self._client.config.set('Configuration', 'AvoidAllcall', 'true')
 457
 458    def allcall_enabled(self):
 459        '''Whether @ALLCALL participation enabled in config file.
 460        
 461        Returns:
 462            bool: True if @ALLCALL participation enabled, False otherwise
 463        '''
 464        return not self._client.config.get('Configuration', 'AvoidAllcall', bool)
 465
 466    def enable_reporting(self):
 467        '''Enable PSKReporter reporting via config file.
 468        
 469        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 470        '''
 471        self._client.config.set('Configuration', 'PSKReporter', 'true')
 472
 473    def disable_reporting(self):
 474        '''Disable PSKReporter reporting via config file.
 475        
 476        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 477        '''
 478        self._client.config.set('Configuration', 'PSKReporter', 'false')
 479
 480    def reporting_enabled(self):
 481        '''Whether PSKReporter reporting enabled in config file.
 482        
 483        Returns:
 484            bool: True if reporting enabled, False otherwise
 485        '''
 486        return self._client.config.get('Configuration', 'PSKReporter', bool)
 487
 488    def enable_transmit(self):
 489        '''Enable JS8Call transmitting via config file.
 490        
 491        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 492        '''
 493        self._client.config.set('Configuration', 'TransmitOFF', 'false')
 494
 495    def disable_transmit(self):
 496        '''Disable JS8Call transmitting via config file.
 497        
 498        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 499        '''
 500        self._client.config.set('Configuration', 'TransmitOFF', 'true')
 501
 502    def transmit_enabled(self):
 503        '''Whether JS8Call transmitting enabled in config file.
 504        
 505        Returns:
 506            bool: True if transmitting enabled, False otherwise
 507        '''
 508        return not self._client.config.get('Configuration', 'TransmitOFF', bool)
 509
 510    def get_profile(self):
 511        '''Get active JS8call configuration profile via config file.
 512
 513        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 514
 515        Returns:
 516            str: Name of the active configuration profile
 517        '''
 518        return self._client.config.get_active_profile()
 519
 520    def get_profile_list(self):
 521        '''Get list of JS8Call configuration profiles via config file.
 522
 523        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 524
 525        Returns:
 526            list: List of configuration profile names
 527        '''
 528        return self._client.config.get_profile_list()
 529
 530    def set_profile(self, profile, restore_on_exit=False, create=False):
 531        '''Set active JS8Call configuration profile via config file.
 532        
 533        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 534        
 535        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 536
 537        Args:
 538            profile (str): Profile name
 539            restore_on_exit (bool): Restore previous profile on exit, defaults to False
 540            create (bool): Create a new profile (copying from Default) if the specified profile does not exist, defaults to False
 541
 542        Raises:
 543            ValueError: Specified profile name does not exist
 544        '''
 545        if profile not in self.get_profile_list():
 546            if create:
 547                # copy from Default profile
 548                self.create_new_profile(profile)
 549            else:
 550                raise ValueError('Config profile \'' + profile + '\' does not exist')
 551
 552        if restore_on_exit:
 553            self._client._previous_profile = self.get_profile()
 554            
 555        # set profile as active
 556        self._client.config.change_profile(profile)
 557
 558    def create_new_profile(self, new_profile, copy_profile='Default'):
 559        '''Create new JS8Call configuration profile.
 560            
 561        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 562            
 563        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 564    
 565        Args:
 566            new_profile (str): Name of new profile to create
 567            copy_profile (str): Name of an existing profile to copy when creating the new profile, defaults to 'Default'
 568        '''
 569        self._client.config.create_new_profile(new_profile, copy_profile)
 570
 571    def get_groups_list(self):
 572        '''Get list of configured JS8Call groups via config file.
 573
 574        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 575
 576        Returns:
 577            list: List of configured group names
 578        '''
 579        return self._client.config.get_groups()
 580
 581    def add_group(self, group):
 582        '''Add configured JS8Call group via config file.
 583        
 584        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 585        
 586        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 587
 588        Args:
 589            group (str): Group name
 590        '''
 591        self._client.config.add_group(group)
 592
 593    def remove_group(self, group):
 594        '''Remove configured JS8Call group via config file.
 595        
 596        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 597        
 598        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 599
 600        Args:
 601            group (str): Group name
 602        '''
 603        self._client.config.remove_group(group)
 604
 605    def set_groups(self, groups):
 606        '''Set configured JS8Call groups via config file.
 607        
 608        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 609        
 610        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 611
 612        Args:
 613            groups (list): List of group names
 614        '''
 615        if isinstance(groups, str):
 616            groups = groups.split(',')
 617
 618        groups = ['@' + group.strip(' @') for group in groups]
 619        groups = ', '.join(groups)
 620        self._client.config.set('Configuration', 'MyGroups', groups)
 621
 622    def get_primary_highlight_words(self):
 623        '''Get primary highlight words via config file.
 624
 625        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 626        
 627        Returns:
 628            list: Words that should be highlighted on the JC8Call UI
 629        '''
 630        words = self._client.config.get('Configuration', 'PrimaryHighlightWords')
 631
 632        if words == '@Invalid()':
 633            words = []
 634        elif words is not None:
 635            words = words.split(', ')
 636
 637        return words
 638
 639    def set_primary_highlight_words(self, words):
 640        '''Set primary highlight words via config file.
 641
 642        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 643        
 644        Args:
 645            words (list): Words that should be highlighted on the JC8Call UI
 646        '''
 647        if isinstance(words, str):
 648            words = [word.strip() for word in words.split(',')]
 649
 650        if len(words) == 0:
 651            words = '@Invalid()'
 652        else:
 653            words = ', '.join(words)
 654
 655        self._client.config.set('Configuration', 'PrimaryHighlightWords', words)
 656
 657    def get_secondary_highlight_words(self):
 658        '''Get secondary highlight words via config file.
 659
 660        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 661        
 662        Returns:
 663            list: Words that should be highlighted on the JC8Call UI
 664        '''
 665        words = self._client.config.get('Configuration', 'SecondaryHighlightWords')
 666
 667        if words == '@Invalid()':
 668            words = []
 669        elif words is not None:
 670            words = words.split(', ')
 671
 672        return words
 673
 674    def set_secondary_highlight_words(self, words):
 675        '''Set secondary highlight words via config file.
 676
 677        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 678        
 679        Args:
 680            words (list): Words that should be highlighted on the JC8Call UI
 681        '''
 682        if isinstance(words, str):
 683            words = [word.strip() for word in words.split(',')]
 684
 685        if len(words) == 0:
 686            words = '@Invalid()'
 687        else:
 688            words = ', '.join(words)
 689
 690        self._client.config.set('Configuration', 'SecondaryHighlightWords', words)
 691
 692    def submode_to_speed(self, submode):
 693        '''Map submode *int* to speed *str*.
 694
 695        | Submode | Speed |
 696        | -------- | -------- |
 697        | 0 | normal |
 698        | 1 | fast |
 699        | 2 | turbo |
 700        | 4 | slow |
 701        | 8 | ultra |
 702
 703        Args:
 704            submode (int): Submode to map to text
 705
 706        Returns:
 707            str: Speed as text
 708        '''
 709        # map integer to text
 710        speeds = {4:'slow', 0:'normal', 1:'fast', 2:'turbo', 8:'ultra'}
 711
 712        if submode is not None and int(submode) in speeds:
 713            return speeds[int(submode)]
 714        else:
 715            raise ValueError('Invalid submode \'' + str(submode) + '\'')
 716
 717    def get_speed(self, update=False):
 718        '''Get JS8Call modem speed.
 719
 720        Possible modem speeds:
 721        - slow
 722        - normal
 723        - fast
 724        - turbo
 725        - ultra
 726
 727        Args:
 728            update (bool): Update speed if True or use local state if False, defaults to False
 729
 730        Returns:
 731            str: JS8call modem speed setting
 732        '''
 733        speed = self._client.js8call.get_state('speed')
 734
 735        if update or speed is None:
 736            msg = Message()
 737            msg.set('type', Message.MODE_GET_SPEED)
 738            self._client.js8call.send(msg)
 739            speed = self._client.js8call.watch('speed')
 740
 741        return self.submode_to_speed(speed)
 742
 743    def set_speed(self, speed):
 744        '''Set JS8Call modem speed via config file.
 745
 746        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 747
 748        Possible modem speeds:
 749        - slow
 750        - normal
 751        - fast
 752        - turbo
 753        - ultra
 754
 755        Args:
 756            speed (str): Speed to set
 757
 758        Returns:
 759            str: JS8Call modem speed setting
 760
 761        '''
 762        if isinstance(speed, str):
 763            speeds = {'slow':4, 'normal':0, 'fast':1, 'turbo':2, 'ultra':8}
 764            if speed in speeds:
 765                speed = speeds[speed]
 766            else:
 767                raise ValueError('Invalid speed: ' + str(speed))
 768
 769        return self._client.config.set('Common', 'SubMode', speed)
 770
 771#        TODO this code sets speed via API, which doesn't work as of JS8Call v2.2
 772#        msg = Message()
 773#        msg.set('type', Message.MODE_SET_SPEED)
 774#        msg.set('params', {'SPEED': speed})
 775#        self._client.js8call.send(msg)
 776#        time.sleep(self._client._set_get_delay)
 777#        return self.get_speed()
 778
 779    def get_freq(self, update=False):
 780        '''Get JS8Call dial frequency.
 781
 782        Args:
 783            update (bool): Update if True or use local state if False, defaults to False
 784
 785        Returns:
 786            int: Dial frequency in Hz
 787        '''
 788        freq = self._client.js8call.get_state('dial')
 789
 790        if update or freq is None:
 791            msg = Message()
 792            msg.type = Message.RIG_GET_FREQ
 793            self._client.js8call.send(msg)
 794            freq = self._client.js8call.watch('dial')
 795
 796        return freq
 797
 798    def set_freq(self, freq):
 799        '''Set JS8Call dial frequency.
 800
 801        Args:
 802            freq (int): Dial frequency in Hz
 803
 804        Returns:
 805            int: Dial frequency in Hz
 806        '''
 807        msg = Message()
 808        msg.set('type', Message.RIG_SET_FREQ)
 809        msg.set('params', {'DIAL': freq, 'OFFSET': self._client.js8call.get_state('offset')})
 810        self._client.js8call.send(msg)
 811        time.sleep(self._client._set_get_delay)
 812        return self.get_freq(update = True)
 813
 814    def get_band(self):
 815        '''Get frequency band designation.
 816
 817        Returns:
 818            str: Band designator like \'40m\' or Client.OOB (out-of-band)
 819        '''
 820        return Client.freq_to_band(self.get_freq())
 821
 822    def get_offset(self, update=False):
 823        '''Get JS8Call offset frequency.
 824
 825        Args:
 826            update (bool): Update if True or use local state if False, defaults to False
 827
 828        Returns:
 829            int: Offset frequency in Hz
 830        '''
 831        offset = self._client.js8call.get_state('offset')
 832        
 833        if update or offset is None:
 834            msg = Message()
 835            msg.type = Message.RIG_GET_FREQ
 836            self._client.js8call.send(msg)
 837            offset = self._client.js8call.watch('offset')
 838
 839        return offset
 840
 841    def set_offset(self, offset):
 842        '''Set JS8Call offset frequency.
 843
 844        Args:
 845            offset (int): Offset frequency in Hz
 846
 847        Returns:
 848            int: Offset frequency in Hz
 849        '''
 850        msg = Message()
 851        msg.set('type', Message.RIG_SET_FREQ)
 852        msg.set('params', {'DIAL': self._client.js8call.get_state('dial'), 'OFFSET': offset})
 853        self._client.js8call.send(msg)
 854        time.sleep(self._client._set_get_delay)
 855        return self.get_offset(update = True)
 856
 857    def get_station_callsign(self, update=False):
 858        '''Get JS8Call callsign.
 859
 860        Args:
 861            update (bool): Update if True or use local state if False, defaults to False
 862
 863        Returns:
 864            str: JS8Call configured callsign
 865        '''
 866        callsign = self._client.js8call.get_state('callsign')
 867
 868        if update or callsign is None:
 869            msg = Message()
 870            msg.type = Message.STATION_GET_CALLSIGN
 871            self._client.js8call.send(msg)
 872            callsign = self._client.js8call.watch('callsign')
 873
 874        return callsign
 875
 876    def set_station_callsign(self, callsign):
 877        '''Set JS8Call callsign.
 878
 879        Callsign must be a maximum of 9 characters and contain at least one number.
 880
 881        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 882
 883        Args:
 884            callsign (str): Callsign to set
 885
 886        Returns:
 887            str: JS8Call configured callsign
 888        '''
 889        callsign = callsign.upper()
 890
 891        if len(callsign) <= 9 and any(char.isdigit() for char in callsign):
 892            return self._client.config.set('Configuration', 'MyCall', callsign)
 893        else:
 894            raise ValueError('callsign must be <= 9 characters in length and contain at least 1 number')
 895
 896    def get_idle_timeout(self):
 897        '''Get JS8Call idle timeout.
 898
 899        Returns:
 900            int: Idle timeout in minutes
 901        '''
 902        return self._client.config.get('Configuration', 'TxIdleWatchdog', value_type=int)
 903
 904    def set_idle_timeout(self, timeout):
 905        '''Set JS8Call idle timeout.
 906
 907        If the JS8Call idle timeout is between 1 and 5 minutes, JS8Call will force the idle timeout to 5 minutes on the next application start or exit.
 908
 909        The maximum idle timeout is 1440 minutes (24 hours).
 910
 911        Disable the idle timeout by setting it to 0 (zero).
 912
 913        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 914
 915        Args:
 916            timeout (int): Idle timeout in minutes
 917
 918        Returns:
 919            int: Current idle timeout in minutes
 920
 921        Raises:
 922            ValueError: Idle timeout must be between 0 and 1440 minutes
 923        '''
 924        if timeout < 0 or timeout > 1440:
 925            raise ValueError('Idle timeout must be between 0 and 1440 minutes')
 926
 927        self._client.config.set('Configuration', 'TxIdleWatchdog', timeout)
 928        return self.get_idle_timeout()
 929
 930    def get_distance_units_miles(self):
 931        '''Get JS8Call distance unit setting.
 932        
 933        Returns:
 934            bool: True if distance units are set to miles, False if km
 935        '''
 936        return self._client.config.get('Configuration', 'Miles', bool)
 937        
 938    def set_distance_units_miles(self, units_miles):
 939        '''Set JS8Call distance unit setting.
 940        
 941        Args:
 942            units_miles (bool): Set units to miles if True, set to km if False
 943            
 944        Returns:
 945            bool: True if distance units are set to miles, False if km
 946        '''
 947        self._client.config.set('Configuration', 'Miles', str(units_miles).lower())
 948        return self.get_distance_units_miles()
 949        
 950    def get_distance_units(self):
 951        '''Get JS8Call distance units.
 952        
 953        Returns:
 954            str: Configured distance units: 'mi' or 'km'
 955        '''
 956        if self.get_distance_units_miles():
 957            return 'mi'
 958        else:
 959            return 'km'
 960        
 961    def set_distance_units(self, units):
 962        ''' Set JS8Call distance units.
 963        
 964        Args:
 965            units (str): Distance units: 'mi', 'miles', 'km', or 'kilometers'
 966            
 967        Returns:
 968            str: Configured distance units: 'miles' or 'km'
 969        '''
 970        if units.lower() in ['mi', 'miles']:
 971            self.set_distance_units_miles(True)
 972            return self.get_distance_units()
 973        elif units.lower() in ['km', 'kilometers']:
 974            self.set_distance_units_miles(False)
 975            return self.get_distance_units()
 976        else:
 977            raise ValueError('Distance units must be: mi, miles, km, or kilometers')
 978    
 979    def get_station_grid(self, update=False):
 980        '''Get JS8Call grid square.
 981
 982        Args:
 983            update (bool): Update if True or use local state if False, defaults to False
 984
 985        Returns:
 986            str: JS8Call configured grid square
 987        '''
 988        grid = self._client.js8call.get_state('grid')
 989
 990        if update or grid is None:
 991            msg = Message()
 992            msg.type = Message.STATION_GET_GRID
 993            self._client.js8call.send(msg)
 994            grid = self._client.js8call.watch('grid')
 995
 996        return grid
 997
 998    def set_station_grid(self, grid):
 999        '''Set JS8Call grid square.
1000
1001        Args:
1002            grid (str): Grid square
1003
1004        Returns:
1005            str: JS8Call configured grid square
1006        '''
1007        grid = grid.upper()
1008        msg = Message()
1009        msg.type = Message.STATION_SET_GRID
1010        msg.value = grid
1011        self._client.js8call.send(msg)
1012        time.sleep(self._client._set_get_delay)
1013        return self.get_station_grid(update = True)
1014
1015    def get_station_info(self, update=False):
1016        '''Get JS8Call station information.
1017
1018        Args:
1019            update (bool): Update if True or use local state if False, defaults to False
1020
1021        Returns:
1022            str: JS8Call configured station information
1023        '''
1024        info = self._client.js8call.get_state('info')
1025
1026        if update or info is None:
1027            msg = Message()
1028            msg.type = Message.STATION_GET_INFO
1029            self._client.js8call.send(msg)
1030            info = self._client.js8call.watch('info')
1031
1032        return info
1033
1034    def set_station_info(self, info):
1035        '''Set JS8Call station information.
1036
1037        *info* updated via API (if connected) and set in JS8Call configuration file.
1038
1039        Args:
1040            info (str): Station information
1041
1042        Returns:
1043            str: JS8Call configured station information
1044        '''
1045        if self._client.online:
1046            msg = Message()
1047            msg.type = Message.STATION_SET_INFO
1048            msg.value = info
1049            self._client.js8call.send(msg)
1050            time.sleep(self._client._set_get_delay)
1051            info = self.get_station_info(update = True)
1052
1053        # save to config file to preserve over restart
1054        self._client.config.set('Configuration', 'MyInfo', info)
1055        return info
1056
1057    def append_pyjs8call_to_station_info(self):
1058        '''Append pyjs8call info to station info
1059
1060        A string like ', PYJS8CALL V0.0.0' is appended to the current station info.
1061        Example: 'QRPLABS QDX, 40M DIPOLE 33FT, PYJS8CALL V0.2.2'
1062
1063        If a string like ', PYJS8CALL' or ',PYJS8CALL' is found in the current station info, that substring (and everything after it) is dropped before appending the new pyjs8call info.
1064
1065        Returns:
1066            str: JS8Call configured station information
1067        '''
1068        info = self.get_station_info().upper()
1069        
1070        if ', PYJS8CALL' in info:
1071            info = info.split(', PYJS8CALL')[0]
1072        elif ',PYJS8CALL' in info:
1073            info = info.split(',PYJS8CALL')[0]
1074            
1075        info = '{}, PYJS8CALL {}'.format(info, pyjs8call.__version__)
1076        return self.set_station_info(info)
1077
1078    def get_bandwidth(self, speed=None):
1079        '''Get JS8Call signal bandwidth based on modem speed.
1080
1081        Uses JS8Call configured speed if no speed is given.
1082
1083        | Speed | Bandwidth |
1084        | -------- | -------- |
1085        | slow | 25 Hz |
1086        | normal | 50 Hz |
1087        | fast | 80 Hz |
1088        | turbo | 160 Hz |
1089        | ultra | 250 Hz |
1090
1091        Args:
1092            speed (str): Speed setting, defaults to None
1093
1094        Returns:
1095            int: Bandwidth of JS8Call signal
1096        '''
1097        if speed is None:
1098            speed = self.get_speed()
1099        elif isinstance(speed, int):
1100            speed = self.submode_to_speed(speed)
1101
1102        bandwidths = {'slow':25, 'normal':50, 'fast':80, 'turbo':160, 'ultra':250}
1103
1104        if speed in bandwidths:
1105            return bandwidths[speed]
1106        else:
1107            raise ValueError('Invalid speed \'' + speed + '\'')
1108
1109    def get_window_duration(self, speed=None):
1110        '''Get JS8Call rx/tx window duration based on modem speed.
1111
1112        Uses JS8Call configured speed if no speed is given.
1113
1114        | Speed | Duration |
1115        | -------- | -------- |
1116        | slow | 30 seconds |
1117        | normal | 15 seconds |
1118        | fast | 10 seconds |
1119        | turbo | 6 seconds |
1120        | ultra | 4 seconds |
1121
1122        Args:
1123            speed (str): Speed setting, defaults to None
1124
1125        Returns:
1126            int: Duration of JS8Call rx/tx window in seconds
1127        '''
1128        if speed is None:
1129            speed = self.get_speed()
1130        elif isinstance(speed, int):
1131            speed = self.submode_to_speed(speed)
1132
1133        duration = {'slow': 30, 'normal': 15, 'fast': 10, 'turbo': 6, 'ultra':4}
1134        return duration[speed]
1135
1136    def enable_daily_restart(self, restart_time='02:00'):
1137        '''Enable daily JS8Call restart at specified time.
1138
1139        The intended use of this function is to allow the removal of the *timer.out* file, which grows in size until it consumes all available disk space. This file cannot be removed while the application is running, but is automatically removed during the pyjs8call restart process.
1140
1141        This function adds a schedule entry. See *pyjs8call.schedulemonitor* for more information.
1142
1143        Args:
1144            restart_time (str): Local restart time in 24-hour format (ex. '23:30'), defaults to '02:00'
1145        '''
1146        # add schedule entry to restart application daily with no settings changes
1147        self._daily_restart_schedule = self._client.schedule.add(restart_time, restart=True)
1148
1149    def disable_daily_restart(self):
1150        '''Disable daily JS8Call restart.
1151        
1152        This function removes the schedule entry created by *enable_daily_restart*. See *pyjs8call.schedulemonitor* for more information.
1153        '''
1154        if self._daily_restart_schedule is None:
1155            return
1156            
1157        self._client.schedule.remove(self._daily_restart_schedule.dict()['time'], schedule=self._daily_restart_schedule)
1158        self._daily_restart_schedule = None
1159
1160    def daily_restart_enabled(self):
1161        '''Whether daily JS8Call restart is enabled.
1162
1163        Returns:
1164            bool: True if associated schedule entry is set, False otherwise
1165        '''
1166        if self._daily_restart_schedule is not None:
1167            return True
1168        else:
1169            return False
1170
1171    def get_daily_restart_time(self):
1172        '''Get daily JS8Call restart time.
1173
1174        Returns:
1175            str or None: Local restart time in 24-hour format (ex. '23:30'), or None if not enabled
1176        '''
1177        if self._daily_restart_schedule is None:
1178            return
1179
1180        return self._daily_restart_schedule.start.strftime('%H:%M')
1181    
class Settings:
  40class Settings:
  41    '''Settings function container.
  42    
  43    This class is initilized by pyjs8call.client.Client.
  44    '''
  45    
  46    def __init__(self, client):
  47        '''Initialize settings object.
  48
  49        Returns:
  50            pyjs8call.client.Settings: Constructed setting object
  51        '''
  52        self._client = client
  53        self._daily_restart_schedule = None
  54        self.loaded_settings = None
  55        '''Loaded settings container (see python3 configparser for more information)'''
  56
  57        self._settings_map = {
  58            'station' : {
  59                'callsign': lambda value: self.set_station_callsign(value),
  60                'grid': lambda value: self.set_station_grid(value),
  61                'speed': lambda value: self.set_speed(value),
  62                'freq': lambda value: self.set_freq(value),
  63                'frequency': lambda value: self.set_freq(value),
  64                'offset': lambda value: self.set_offset(value),
  65                'info': lambda value: self.set_station_info(value),
  66                'append_pyjs8call_info': lambda value: self.append_pyjs8call_to_station_info(),
  67                'daily_restart': lambda value: self.enable_daily_restart(value) if value else self.disable_daily_restart()
  68            },
  69            'general': {
  70                'groups': lambda value: self.set_groups(value),
  71                'multi_speed_decode': lambda value: self.enable_multi_decode() if value else self.disable_multi_decode(),
  72                'autoreply_on_at_startup': lambda value: self.enable_autoreply_startup() if value else self.disable_autoreply_startup(),
  73                'autoreply_confirmation': lambda value: self.enable_autoreply_confirmation() if value else self.disable_autoreply_confirmation(),
  74                'allcall': lambda value: self.enable_allcall() if value else self.disable_allcall(),
  75                'reporting': lambda value: self.enable_reporting() if value else self.disable_reporting(),
  76                'transmit': lambda value: self.enable_transmit() if value else self.disable_transmit(),
  77                'idle_timeout': lambda value: self.set_idle_timeout(value),
  78                'distance_units': lambda value: self.set_distance_units(value)
  79            },
  80            'heartbeat': {
  81                'enable': lambda value: self._client.heartbeat.enable() if value else self._client.heartbeat.disable(),
  82                'interval': lambda value: self.set_heartbeat_interval(value),
  83                'acknowledgements': lambda value: self.enable_heartbeat_acknowledgements() if value else self.disable_heartbeat_acknowledgements(),
  84                'pause_during_qso': lambda value: self.pause_heartbeat_during_qso() if value else self.allow_heartbeat_during_qso()
  85            },
  86            'profile': {
  87                'profile': lambda value: self.set_profile(value),
  88                'set_profile_on_exit': lambda value: self._client.set_profile_on_exit(value)
  89            },
  90            'highlight': {
  91                'primary_words': lambda value: self.set_primary_highlight_words(value),
  92                'secondary_words': lambda value: self.set_secondary_highlight_words(value)
  93            },
  94            'spots': {
  95                'watch_stations': lambda value: self._client.spots.set_watched_stations(value),
  96                'watch_groups': lambda value: self._client.spots.set_watched_groups(value)
  97            },
  98            'notifications': {
  99                'enable': lambda value: self._client.notifications.enable() if value else self._client.notifications.disable(),
 100                'smtp_server': lambda value: self._client.notifications.set_smtp_server(value),
 101                'smtp_port': lambda value: self._client.notifications.set_smtp_server_port(value),
 102                'smtp_email_address': lambda value: self._client.notifications.set_smtp_email_address(value),
 103                'smtp_password': lambda value: self._client.notifications.set_smtp_password(value),
 104                'notification_email_address': lambda value: self._client.notifications.set_email_destination(value),
 105                'notification_email_subject': lambda value: self._client.notifications.set_email_subject(value),
 106                'incoming': lambda value: self._client.notifications.enable_incoming() if value else self._client.notifications.disable_incoming(),
 107                'spots': lambda value: self._client.notifications.enable_spots() if value else self._client.notifications.disable_spots(),
 108                'station_spots': lambda value: self._client.notifications.enable_station_spots() if value else self._client.notifications.disable_station_spots(),
 109                'group_spots': lambda value: self._client.notifications.enable_group_spots() if value else self._client.notifications.disable_group_spots()
 110            }
 111        }
 112
 113        # settings set via js8call config file
 114        self._pre_start_settings = {
 115            'station' : [
 116                'callsign',
 117                'speed'
 118            ],
 119            'general': [
 120                'groups', 
 121                'multi_speed_decode',
 122                'autoreplay_on_at_startup',
 123                'autoreply_confirmation',
 124                'allcall',
 125                'reporting',
 126                'transmit',
 127                'idle_timeout',
 128                'distance_units'
 129            ],
 130            'heartbeat': [
 131                'interval',
 132                'acknowledgements',
 133                'pause_during_qso'
 134            ],
 135            'profile': [
 136                'profile',
 137                'set_profile_on_exit'
 138            ],
 139            'highlight': [
 140                'primary_words',
 141                'secondary_words'
 142            ],
 143            'spots': [
 144            ],
 145            'notifications': [
 146            ]
 147        }
 148
 149    def load(self, settings_path):
 150        '''Load pyjs8call settings from file.
 151
 152        The settings file referenced here is specific to pyjs8call, and is not the same as the JS8Call configuration file. The pyjs8call settings file is not required.
 153
 154        This function must be called before calling *client.start()*. Settings that must be set before or after starting the JS8Call application are handled automatically. Settings that affect the JS8Call config file are set immediately. All other settings are set after *client.start()* is called.
 155
 156        Example settings file:
 157
 158        ```
 159        [station]
 160
 161        callsign=CALL0SIGN
 162        grid=EM19
 163        speed=normal
 164        freq=7078000
 165        offset=1750
 166        info=QDX 5W, DIPOLE 30FT
 167        append_pyjs8call_info=true
 168        daily_restart=02:00
 169
 170        [general]
 171
 172        groups=@TTP, @AMRRON
 173        multi_speed_decode=true
 174        autoreply_on_at_startup=true
 175        autoreply_confirmation=false
 176        allcall=true
 177        reporting=true
 178        transmit=true
 179        idle_timeout=0
 180        distance_units=miles
 181
 182        [heartbeat]
 183
 184        enable=true
 185        interval=15
 186        acknowledgements=true
 187        pause_during_qso=true
 188
 189        [profile]
 190
 191        profile=Default
 192        set_profile_on_exit=Default
 193
 194        [highlight]
 195
 196        primary_words=KT7RUN, OH8STN
 197        secondary_words=simplyequipped
 198
 199        [spots]
 200
 201        watch_stations=KT7RUN, OH8STN
 202        watch_groups=@TTP, @AMRRON
 203
 204        [notifications]
 205
 206        enable=true
 207        smtp_server=smtp.gmail.com
 208        smtp_port=465
 209        smtp_email_address=email@address.com
 210        smtp_password=APP_PASSWORD
 211        notification_email_address=0123456789@vtext.com
 212        notification_email_subject=
 213        incoming=true
 214        spots=false
 215        station_spots=true
 216        group_spots=true
 217        ```
 218
 219        Args:
 220            settings_path (str): Relative or absolute path to settings file
 221
 222        Raises:
 223            OSError: Specified settings file not found
 224        '''
 225        settings_path = os.path.expanduser(settings_path)
 226        settings_path = os.path.abspath(settings_path)
 227
 228        if not os.path.exists(settings_path):
 229            raise FileNotFoundError('Specified settings file not found: {}'.format(settings_path))
 230
 231        self.loaded_settings = configparser.ConfigParser(interpolation = None)
 232        self.loaded_settings.read(settings_path)
 233
 234        self.apply_loaded_settings()
 235
 236    def apply_loaded_settings(self, post_start=False):
 237        '''Apply loaded pyjs8call settings.
 238
 239        This function is called internally by *load_settings()* and *client.start()*.
 240
 241        Args:
 242            post_start (bool): Post start processing if True, pre start processing if False, defaults to False
 243        '''
 244        for section in self.loaded_settings.sections():
 245            #skip unsupported section
 246            if section not in self._settings_map:
 247                continue
 248
 249            for key, value in self.loaded_settings[section].items():
 250                # skip unsupported key
 251                if key not in self._settings_map[section]:
 252                    continue
 253
 254                # skip post start settings during pre start processing
 255                if not post_start and key in self._pre_start_settings[section]:
 256                    value = self._parse_loaded_value(value)
 257                    self._settings_map[section][key](value)
 258
 259                # skip pre start settings during post start processing
 260                if post_start and not key in self._pre_start_settings[section]:
 261                    value = self._parse_loaded_value(value)
 262                    self._settings_map[section][key](value)
 263
 264    def _parse_loaded_value(self, value):
 265        '''Parse setting value from string to Python type.
 266        
 267        Args:
 268            value (str): Setting value to parse
 269        '''
 270        if value is None:
 271            return None
 272        elif value.lower() in ['true', 'yes']:
 273            return True
 274        elif value.lower() in ['false', 'no']:
 275            return False
 276        elif value.lower() in ('none', '', 'nil', 'nill', 'null'):
 277            return None
 278        elif value.isnumeric():
 279            return int(value)
 280        else:
 281            return value
 282    
 283    def enable_heartbeat_networking(self):
 284        '''Enable heartbeat networking via config file.
 285        
 286        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 287        
 288        Note that this function disables JS8Call application heartbeat networking via the config file. To enable the pyjs8call heartbeat network messaging module see *client.heartbeat.enable()*.
 289        '''
 290        self._client.config.set('Common', 'SubModeHB', 'true')
 291
 292    def disable_heartbeat_networking(self):
 293        '''Disable heartbeat networking via config file.
 294        
 295        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 296        
 297        Note that this function disables JS8Call application heartbeat networking via the config file. To disable the pyjs8call heartbeat network messaging module see *client.heartbeat.disable()*.
 298        '''
 299        self._client.config.set('Common', 'SubModeHB', 'false')
 300
 301    def heartbeat_networking_enabled(self):
 302        '''Whether heartbeat networking enabled in config file.
 303        
 304        Returns:
 305            bool: True if heartbeat networking enabled, False otherwise
 306        '''
 307        return self._client.config.get('Common', 'SubModeHB', bool)
 308
 309    def get_heartbeat_interval(self):
 310        '''Get heartbeat networking interval.
 311        
 312        Returns:
 313            int: Heartbeat networking time interval in minutes
 314        '''
 315        return self._client.config.get('Common', 'HBInterval', int)
 316        
 317    def set_heartbeat_interval(self, interval):
 318        '''Set the heartbeat networking interval.
 319        
 320        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 321
 322        Args:
 323            interval (int): New heartbeat networking time interval in minutes
 324        
 325        Returns:
 326            int: Current heartbeat networking time interval in minutes
 327        '''
 328        return self._client.config.set('Common', 'HBInterval', interval)
 329        
 330    def enable_heartbeat_acknowledgements(self):
 331        '''Enable heartbeat acknowledgements via config file.
 332        
 333        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 334
 335        Also enables JS8Call heartbeat networking, since heartbeat acknowledgements will not be enabled without heartbeat networking enabled first. This only enables the feature within JS8Call, and does not casue heartbeats to be sent.
 336        '''
 337        self.enable_heartbeat_networking()
 338        self._client.config.set('Common', 'SubModeHBAck', 'true')
 339
 340    def disable_heartbeat_acknowledgements(self):
 341        '''Disable heartbeat acknowledgements via config file.
 342        
 343        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 344        '''
 345        self._client.config.set('Common', 'SubModeHBAck', 'false')
 346
 347    def heartbeat_acknowledgements_enabled(self):
 348        '''Whether heartbeat acknowledgements enabled in config file.
 349        
 350        Returns:
 351            bool: True if heartbeat acknowledgements enabled, False otherwise
 352        '''
 353        return self._client.config.get('Common', 'SubModeHBAck', bool)
 354        
 355    def pause_heartbeat_during_qso(self):
 356        '''Pause heartbeat messages during QSO via config file.
 357        
 358        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 359        '''
 360        self._client.config.set('Configuration', 'HeartbeatQSOPause', 'true')
 361
 362    def allow_heartbeat_during_qso(self):
 363        '''Allow heartbeat messages during QSO via config file.
 364        
 365        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 366        '''
 367        self._client.config.set('Configuration', 'HeartbeatQSOPause', 'false')
 368
 369    def heartbeat_during_qso_paused(self):
 370        '''Whether heartbeat messages paused during QSO in config file.
 371        
 372        Returns:
 373            bool: True if heartbeat messages paused during QSO, False otherwise
 374        '''
 375        return self._client.config.get('Configuration', 'HeartbeatQSOPause', bool)
 376
 377    def enable_multi_decode(self):
 378        '''Enable multi-speed decoding via config file.
 379        
 380        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 381        '''
 382        self._client.config.set('Common', 'SubModeHBMultiDecode', 'true')
 383
 384    def disable_multi_decode(self):
 385        '''Disable multi-speed decoding via config file.
 386        
 387        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 388        '''
 389        self._client.config.set('Common', 'SubModeMultiDecode', 'false')
 390
 391    def multi_decode_enabled(self):
 392        '''Whether multi-decode enabled in config file.
 393        
 394        Returns:
 395            bool: True if multi-decode enabled, False otherwise
 396        '''
 397        return self._client.config.get('Common', 'SubModeMultiDecode', bool)
 398
 399    def enable_autoreply_startup(self):
 400        '''Enable autoreply on start-up via config file.
 401        
 402        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 403        '''
 404        self._client.config.set('Configuration', 'AutoreplyOnAtStartup', 'true')
 405
 406    def disable_autoreply_startup(self):
 407        '''Disable autoreply on start-up via config file.
 408        
 409        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 410        '''
 411        self._client.config.set('Configuration', 'AutoreplyOnAtStartup', 'false')
 412
 413    def autoreply_startup_enabled(self):
 414        '''Whether autoreply enabled at start-up in config file.
 415        
 416        Returns:
 417            bool: True if autoreply is enabled at start-up, False otherwise
 418        '''
 419        return self._client.config.get('Configuration', 'AutoreplyOnAtStartup', bool)
 420
 421    def enable_autoreply_confirmation(self):
 422        '''Enable autoreply confirmation via config file.
 423        
 424        When running headless the autoreply confirmation dialog box will be inaccessible.
 425        
 426        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 427        '''
 428        self._client.config.set('Configuration', 'AutoreplyConfirmation', 'true')
 429
 430    def disable_autoreply_confirmation(self):
 431        '''Disable autoreply confirmation via config file.
 432        
 433        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 434        '''
 435        self._client.config.set('Configuration', 'AutoreplyConfirmation', 'false')
 436
 437    def autoreply_confirmation_enabled(self):
 438        '''Whether autoreply confirmation enabled in config file.
 439        
 440        Returns:
 441            bool: True if autoreply confirmation enabled, False otherwise
 442        '''
 443        return self._client.config.get('Configuration', 'AutoreplyConfirmation', bool)
 444
 445    def enable_allcall(self):
 446        '''Enable @ALLCALL participation via config file.
 447        
 448        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 449        '''
 450        self._client.config.set('Configuration', 'AvoidAllcall', 'false')
 451
 452    def disable_allcall(self):
 453        '''Disable @ALLCALL participation via config file.
 454        
 455        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 456        '''
 457        self._client.config.set('Configuration', 'AvoidAllcall', 'true')
 458
 459    def allcall_enabled(self):
 460        '''Whether @ALLCALL participation enabled in config file.
 461        
 462        Returns:
 463            bool: True if @ALLCALL participation enabled, False otherwise
 464        '''
 465        return not self._client.config.get('Configuration', 'AvoidAllcall', bool)
 466
 467    def enable_reporting(self):
 468        '''Enable PSKReporter reporting via config file.
 469        
 470        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 471        '''
 472        self._client.config.set('Configuration', 'PSKReporter', 'true')
 473
 474    def disable_reporting(self):
 475        '''Disable PSKReporter reporting via config file.
 476        
 477        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 478        '''
 479        self._client.config.set('Configuration', 'PSKReporter', 'false')
 480
 481    def reporting_enabled(self):
 482        '''Whether PSKReporter reporting enabled in config file.
 483        
 484        Returns:
 485            bool: True if reporting enabled, False otherwise
 486        '''
 487        return self._client.config.get('Configuration', 'PSKReporter', bool)
 488
 489    def enable_transmit(self):
 490        '''Enable JS8Call transmitting via config file.
 491        
 492        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 493        '''
 494        self._client.config.set('Configuration', 'TransmitOFF', 'false')
 495
 496    def disable_transmit(self):
 497        '''Disable JS8Call transmitting via config file.
 498        
 499        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 500        '''
 501        self._client.config.set('Configuration', 'TransmitOFF', 'true')
 502
 503    def transmit_enabled(self):
 504        '''Whether JS8Call transmitting enabled in config file.
 505        
 506        Returns:
 507            bool: True if transmitting enabled, False otherwise
 508        '''
 509        return not self._client.config.get('Configuration', 'TransmitOFF', bool)
 510
 511    def get_profile(self):
 512        '''Get active JS8call configuration profile via config file.
 513
 514        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 515
 516        Returns:
 517            str: Name of the active configuration profile
 518        '''
 519        return self._client.config.get_active_profile()
 520
 521    def get_profile_list(self):
 522        '''Get list of JS8Call configuration profiles via config file.
 523
 524        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 525
 526        Returns:
 527            list: List of configuration profile names
 528        '''
 529        return self._client.config.get_profile_list()
 530
 531    def set_profile(self, profile, restore_on_exit=False, create=False):
 532        '''Set active JS8Call configuration profile via config file.
 533        
 534        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 535        
 536        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 537
 538        Args:
 539            profile (str): Profile name
 540            restore_on_exit (bool): Restore previous profile on exit, defaults to False
 541            create (bool): Create a new profile (copying from Default) if the specified profile does not exist, defaults to False
 542
 543        Raises:
 544            ValueError: Specified profile name does not exist
 545        '''
 546        if profile not in self.get_profile_list():
 547            if create:
 548                # copy from Default profile
 549                self.create_new_profile(profile)
 550            else:
 551                raise ValueError('Config profile \'' + profile + '\' does not exist')
 552
 553        if restore_on_exit:
 554            self._client._previous_profile = self.get_profile()
 555            
 556        # set profile as active
 557        self._client.config.change_profile(profile)
 558
 559    def create_new_profile(self, new_profile, copy_profile='Default'):
 560        '''Create new JS8Call configuration profile.
 561            
 562        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 563            
 564        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 565    
 566        Args:
 567            new_profile (str): Name of new profile to create
 568            copy_profile (str): Name of an existing profile to copy when creating the new profile, defaults to 'Default'
 569        '''
 570        self._client.config.create_new_profile(new_profile, copy_profile)
 571
 572    def get_groups_list(self):
 573        '''Get list of configured JS8Call groups via config file.
 574
 575        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 576
 577        Returns:
 578            list: List of configured group names
 579        '''
 580        return self._client.config.get_groups()
 581
 582    def add_group(self, group):
 583        '''Add configured JS8Call group via config file.
 584        
 585        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 586        
 587        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 588
 589        Args:
 590            group (str): Group name
 591        '''
 592        self._client.config.add_group(group)
 593
 594    def remove_group(self, group):
 595        '''Remove configured JS8Call group via config file.
 596        
 597        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 598        
 599        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 600
 601        Args:
 602            group (str): Group name
 603        '''
 604        self._client.config.remove_group(group)
 605
 606    def set_groups(self, groups):
 607        '''Set configured JS8Call groups via config file.
 608        
 609        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
 610        
 611        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 612
 613        Args:
 614            groups (list): List of group names
 615        '''
 616        if isinstance(groups, str):
 617            groups = groups.split(',')
 618
 619        groups = ['@' + group.strip(' @') for group in groups]
 620        groups = ', '.join(groups)
 621        self._client.config.set('Configuration', 'MyGroups', groups)
 622
 623    def get_primary_highlight_words(self):
 624        '''Get primary highlight words via config file.
 625
 626        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 627        
 628        Returns:
 629            list: Words that should be highlighted on the JC8Call UI
 630        '''
 631        words = self._client.config.get('Configuration', 'PrimaryHighlightWords')
 632
 633        if words == '@Invalid()':
 634            words = []
 635        elif words is not None:
 636            words = words.split(', ')
 637
 638        return words
 639
 640    def set_primary_highlight_words(self, words):
 641        '''Set primary highlight words via config file.
 642
 643        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 644        
 645        Args:
 646            words (list): Words that should be highlighted on the JC8Call UI
 647        '''
 648        if isinstance(words, str):
 649            words = [word.strip() for word in words.split(',')]
 650
 651        if len(words) == 0:
 652            words = '@Invalid()'
 653        else:
 654            words = ', '.join(words)
 655
 656        self._client.config.set('Configuration', 'PrimaryHighlightWords', words)
 657
 658    def get_secondary_highlight_words(self):
 659        '''Get secondary highlight words via config file.
 660
 661        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 662        
 663        Returns:
 664            list: Words that should be highlighted on the JC8Call UI
 665        '''
 666        words = self._client.config.get('Configuration', 'SecondaryHighlightWords')
 667
 668        if words == '@Invalid()':
 669            words = []
 670        elif words is not None:
 671            words = words.split(', ')
 672
 673        return words
 674
 675    def set_secondary_highlight_words(self, words):
 676        '''Set secondary highlight words via config file.
 677
 678        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 679        
 680        Args:
 681            words (list): Words that should be highlighted on the JC8Call UI
 682        '''
 683        if isinstance(words, str):
 684            words = [word.strip() for word in words.split(',')]
 685
 686        if len(words) == 0:
 687            words = '@Invalid()'
 688        else:
 689            words = ', '.join(words)
 690
 691        self._client.config.set('Configuration', 'SecondaryHighlightWords', words)
 692
 693    def submode_to_speed(self, submode):
 694        '''Map submode *int* to speed *str*.
 695
 696        | Submode | Speed |
 697        | -------- | -------- |
 698        | 0 | normal |
 699        | 1 | fast |
 700        | 2 | turbo |
 701        | 4 | slow |
 702        | 8 | ultra |
 703
 704        Args:
 705            submode (int): Submode to map to text
 706
 707        Returns:
 708            str: Speed as text
 709        '''
 710        # map integer to text
 711        speeds = {4:'slow', 0:'normal', 1:'fast', 2:'turbo', 8:'ultra'}
 712
 713        if submode is not None and int(submode) in speeds:
 714            return speeds[int(submode)]
 715        else:
 716            raise ValueError('Invalid submode \'' + str(submode) + '\'')
 717
 718    def get_speed(self, update=False):
 719        '''Get JS8Call modem speed.
 720
 721        Possible modem speeds:
 722        - slow
 723        - normal
 724        - fast
 725        - turbo
 726        - ultra
 727
 728        Args:
 729            update (bool): Update speed if True or use local state if False, defaults to False
 730
 731        Returns:
 732            str: JS8call modem speed setting
 733        '''
 734        speed = self._client.js8call.get_state('speed')
 735
 736        if update or speed is None:
 737            msg = Message()
 738            msg.set('type', Message.MODE_GET_SPEED)
 739            self._client.js8call.send(msg)
 740            speed = self._client.js8call.watch('speed')
 741
 742        return self.submode_to_speed(speed)
 743
 744    def set_speed(self, speed):
 745        '''Set JS8Call modem speed via config file.
 746
 747        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 748
 749        Possible modem speeds:
 750        - slow
 751        - normal
 752        - fast
 753        - turbo
 754        - ultra
 755
 756        Args:
 757            speed (str): Speed to set
 758
 759        Returns:
 760            str: JS8Call modem speed setting
 761
 762        '''
 763        if isinstance(speed, str):
 764            speeds = {'slow':4, 'normal':0, 'fast':1, 'turbo':2, 'ultra':8}
 765            if speed in speeds:
 766                speed = speeds[speed]
 767            else:
 768                raise ValueError('Invalid speed: ' + str(speed))
 769
 770        return self._client.config.set('Common', 'SubMode', speed)
 771
 772#        TODO this code sets speed via API, which doesn't work as of JS8Call v2.2
 773#        msg = Message()
 774#        msg.set('type', Message.MODE_SET_SPEED)
 775#        msg.set('params', {'SPEED': speed})
 776#        self._client.js8call.send(msg)
 777#        time.sleep(self._client._set_get_delay)
 778#        return self.get_speed()
 779
 780    def get_freq(self, update=False):
 781        '''Get JS8Call dial frequency.
 782
 783        Args:
 784            update (bool): Update if True or use local state if False, defaults to False
 785
 786        Returns:
 787            int: Dial frequency in Hz
 788        '''
 789        freq = self._client.js8call.get_state('dial')
 790
 791        if update or freq is None:
 792            msg = Message()
 793            msg.type = Message.RIG_GET_FREQ
 794            self._client.js8call.send(msg)
 795            freq = self._client.js8call.watch('dial')
 796
 797        return freq
 798
 799    def set_freq(self, freq):
 800        '''Set JS8Call dial frequency.
 801
 802        Args:
 803            freq (int): Dial frequency in Hz
 804
 805        Returns:
 806            int: Dial frequency in Hz
 807        '''
 808        msg = Message()
 809        msg.set('type', Message.RIG_SET_FREQ)
 810        msg.set('params', {'DIAL': freq, 'OFFSET': self._client.js8call.get_state('offset')})
 811        self._client.js8call.send(msg)
 812        time.sleep(self._client._set_get_delay)
 813        return self.get_freq(update = True)
 814
 815    def get_band(self):
 816        '''Get frequency band designation.
 817
 818        Returns:
 819            str: Band designator like \'40m\' or Client.OOB (out-of-band)
 820        '''
 821        return Client.freq_to_band(self.get_freq())
 822
 823    def get_offset(self, update=False):
 824        '''Get JS8Call offset frequency.
 825
 826        Args:
 827            update (bool): Update if True or use local state if False, defaults to False
 828
 829        Returns:
 830            int: Offset frequency in Hz
 831        '''
 832        offset = self._client.js8call.get_state('offset')
 833        
 834        if update or offset is None:
 835            msg = Message()
 836            msg.type = Message.RIG_GET_FREQ
 837            self._client.js8call.send(msg)
 838            offset = self._client.js8call.watch('offset')
 839
 840        return offset
 841
 842    def set_offset(self, offset):
 843        '''Set JS8Call offset frequency.
 844
 845        Args:
 846            offset (int): Offset frequency in Hz
 847
 848        Returns:
 849            int: Offset frequency in Hz
 850        '''
 851        msg = Message()
 852        msg.set('type', Message.RIG_SET_FREQ)
 853        msg.set('params', {'DIAL': self._client.js8call.get_state('dial'), 'OFFSET': offset})
 854        self._client.js8call.send(msg)
 855        time.sleep(self._client._set_get_delay)
 856        return self.get_offset(update = True)
 857
 858    def get_station_callsign(self, update=False):
 859        '''Get JS8Call callsign.
 860
 861        Args:
 862            update (bool): Update if True or use local state if False, defaults to False
 863
 864        Returns:
 865            str: JS8Call configured callsign
 866        '''
 867        callsign = self._client.js8call.get_state('callsign')
 868
 869        if update or callsign is None:
 870            msg = Message()
 871            msg.type = Message.STATION_GET_CALLSIGN
 872            self._client.js8call.send(msg)
 873            callsign = self._client.js8call.watch('callsign')
 874
 875        return callsign
 876
 877    def set_station_callsign(self, callsign):
 878        '''Set JS8Call callsign.
 879
 880        Callsign must be a maximum of 9 characters and contain at least one number.
 881
 882        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 883
 884        Args:
 885            callsign (str): Callsign to set
 886
 887        Returns:
 888            str: JS8Call configured callsign
 889        '''
 890        callsign = callsign.upper()
 891
 892        if len(callsign) <= 9 and any(char.isdigit() for char in callsign):
 893            return self._client.config.set('Configuration', 'MyCall', callsign)
 894        else:
 895            raise ValueError('callsign must be <= 9 characters in length and contain at least 1 number')
 896
 897    def get_idle_timeout(self):
 898        '''Get JS8Call idle timeout.
 899
 900        Returns:
 901            int: Idle timeout in minutes
 902        '''
 903        return self._client.config.get('Configuration', 'TxIdleWatchdog', value_type=int)
 904
 905    def set_idle_timeout(self, timeout):
 906        '''Set JS8Call idle timeout.
 907
 908        If the JS8Call idle timeout is between 1 and 5 minutes, JS8Call will force the idle timeout to 5 minutes on the next application start or exit.
 909
 910        The maximum idle timeout is 1440 minutes (24 hours).
 911
 912        Disable the idle timeout by setting it to 0 (zero).
 913
 914        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
 915
 916        Args:
 917            timeout (int): Idle timeout in minutes
 918
 919        Returns:
 920            int: Current idle timeout in minutes
 921
 922        Raises:
 923            ValueError: Idle timeout must be between 0 and 1440 minutes
 924        '''
 925        if timeout < 0 or timeout > 1440:
 926            raise ValueError('Idle timeout must be between 0 and 1440 minutes')
 927
 928        self._client.config.set('Configuration', 'TxIdleWatchdog', timeout)
 929        return self.get_idle_timeout()
 930
 931    def get_distance_units_miles(self):
 932        '''Get JS8Call distance unit setting.
 933        
 934        Returns:
 935            bool: True if distance units are set to miles, False if km
 936        '''
 937        return self._client.config.get('Configuration', 'Miles', bool)
 938        
 939    def set_distance_units_miles(self, units_miles):
 940        '''Set JS8Call distance unit setting.
 941        
 942        Args:
 943            units_miles (bool): Set units to miles if True, set to km if False
 944            
 945        Returns:
 946            bool: True if distance units are set to miles, False if km
 947        '''
 948        self._client.config.set('Configuration', 'Miles', str(units_miles).lower())
 949        return self.get_distance_units_miles()
 950        
 951    def get_distance_units(self):
 952        '''Get JS8Call distance units.
 953        
 954        Returns:
 955            str: Configured distance units: 'mi' or 'km'
 956        '''
 957        if self.get_distance_units_miles():
 958            return 'mi'
 959        else:
 960            return 'km'
 961        
 962    def set_distance_units(self, units):
 963        ''' Set JS8Call distance units.
 964        
 965        Args:
 966            units (str): Distance units: 'mi', 'miles', 'km', or 'kilometers'
 967            
 968        Returns:
 969            str: Configured distance units: 'miles' or 'km'
 970        '''
 971        if units.lower() in ['mi', 'miles']:
 972            self.set_distance_units_miles(True)
 973            return self.get_distance_units()
 974        elif units.lower() in ['km', 'kilometers']:
 975            self.set_distance_units_miles(False)
 976            return self.get_distance_units()
 977        else:
 978            raise ValueError('Distance units must be: mi, miles, km, or kilometers')
 979    
 980    def get_station_grid(self, update=False):
 981        '''Get JS8Call grid square.
 982
 983        Args:
 984            update (bool): Update if True or use local state if False, defaults to False
 985
 986        Returns:
 987            str: JS8Call configured grid square
 988        '''
 989        grid = self._client.js8call.get_state('grid')
 990
 991        if update or grid is None:
 992            msg = Message()
 993            msg.type = Message.STATION_GET_GRID
 994            self._client.js8call.send(msg)
 995            grid = self._client.js8call.watch('grid')
 996
 997        return grid
 998
 999    def set_station_grid(self, grid):
1000        '''Set JS8Call grid square.
1001
1002        Args:
1003            grid (str): Grid square
1004
1005        Returns:
1006            str: JS8Call configured grid square
1007        '''
1008        grid = grid.upper()
1009        msg = Message()
1010        msg.type = Message.STATION_SET_GRID
1011        msg.value = grid
1012        self._client.js8call.send(msg)
1013        time.sleep(self._client._set_get_delay)
1014        return self.get_station_grid(update = True)
1015
1016    def get_station_info(self, update=False):
1017        '''Get JS8Call station information.
1018
1019        Args:
1020            update (bool): Update if True or use local state if False, defaults to False
1021
1022        Returns:
1023            str: JS8Call configured station information
1024        '''
1025        info = self._client.js8call.get_state('info')
1026
1027        if update or info is None:
1028            msg = Message()
1029            msg.type = Message.STATION_GET_INFO
1030            self._client.js8call.send(msg)
1031            info = self._client.js8call.watch('info')
1032
1033        return info
1034
1035    def set_station_info(self, info):
1036        '''Set JS8Call station information.
1037
1038        *info* updated via API (if connected) and set in JS8Call configuration file.
1039
1040        Args:
1041            info (str): Station information
1042
1043        Returns:
1044            str: JS8Call configured station information
1045        '''
1046        if self._client.online:
1047            msg = Message()
1048            msg.type = Message.STATION_SET_INFO
1049            msg.value = info
1050            self._client.js8call.send(msg)
1051            time.sleep(self._client._set_get_delay)
1052            info = self.get_station_info(update = True)
1053
1054        # save to config file to preserve over restart
1055        self._client.config.set('Configuration', 'MyInfo', info)
1056        return info
1057
1058    def append_pyjs8call_to_station_info(self):
1059        '''Append pyjs8call info to station info
1060
1061        A string like ', PYJS8CALL V0.0.0' is appended to the current station info.
1062        Example: 'QRPLABS QDX, 40M DIPOLE 33FT, PYJS8CALL V0.2.2'
1063
1064        If a string like ', PYJS8CALL' or ',PYJS8CALL' is found in the current station info, that substring (and everything after it) is dropped before appending the new pyjs8call info.
1065
1066        Returns:
1067            str: JS8Call configured station information
1068        '''
1069        info = self.get_station_info().upper()
1070        
1071        if ', PYJS8CALL' in info:
1072            info = info.split(', PYJS8CALL')[0]
1073        elif ',PYJS8CALL' in info:
1074            info = info.split(',PYJS8CALL')[0]
1075            
1076        info = '{}, PYJS8CALL {}'.format(info, pyjs8call.__version__)
1077        return self.set_station_info(info)
1078
1079    def get_bandwidth(self, speed=None):
1080        '''Get JS8Call signal bandwidth based on modem speed.
1081
1082        Uses JS8Call configured speed if no speed is given.
1083
1084        | Speed | Bandwidth |
1085        | -------- | -------- |
1086        | slow | 25 Hz |
1087        | normal | 50 Hz |
1088        | fast | 80 Hz |
1089        | turbo | 160 Hz |
1090        | ultra | 250 Hz |
1091
1092        Args:
1093            speed (str): Speed setting, defaults to None
1094
1095        Returns:
1096            int: Bandwidth of JS8Call signal
1097        '''
1098        if speed is None:
1099            speed = self.get_speed()
1100        elif isinstance(speed, int):
1101            speed = self.submode_to_speed(speed)
1102
1103        bandwidths = {'slow':25, 'normal':50, 'fast':80, 'turbo':160, 'ultra':250}
1104
1105        if speed in bandwidths:
1106            return bandwidths[speed]
1107        else:
1108            raise ValueError('Invalid speed \'' + speed + '\'')
1109
1110    def get_window_duration(self, speed=None):
1111        '''Get JS8Call rx/tx window duration based on modem speed.
1112
1113        Uses JS8Call configured speed if no speed is given.
1114
1115        | Speed | Duration |
1116        | -------- | -------- |
1117        | slow | 30 seconds |
1118        | normal | 15 seconds |
1119        | fast | 10 seconds |
1120        | turbo | 6 seconds |
1121        | ultra | 4 seconds |
1122
1123        Args:
1124            speed (str): Speed setting, defaults to None
1125
1126        Returns:
1127            int: Duration of JS8Call rx/tx window in seconds
1128        '''
1129        if speed is None:
1130            speed = self.get_speed()
1131        elif isinstance(speed, int):
1132            speed = self.submode_to_speed(speed)
1133
1134        duration = {'slow': 30, 'normal': 15, 'fast': 10, 'turbo': 6, 'ultra':4}
1135        return duration[speed]
1136
1137    def enable_daily_restart(self, restart_time='02:00'):
1138        '''Enable daily JS8Call restart at specified time.
1139
1140        The intended use of this function is to allow the removal of the *timer.out* file, which grows in size until it consumes all available disk space. This file cannot be removed while the application is running, but is automatically removed during the pyjs8call restart process.
1141
1142        This function adds a schedule entry. See *pyjs8call.schedulemonitor* for more information.
1143
1144        Args:
1145            restart_time (str): Local restart time in 24-hour format (ex. '23:30'), defaults to '02:00'
1146        '''
1147        # add schedule entry to restart application daily with no settings changes
1148        self._daily_restart_schedule = self._client.schedule.add(restart_time, restart=True)
1149
1150    def disable_daily_restart(self):
1151        '''Disable daily JS8Call restart.
1152        
1153        This function removes the schedule entry created by *enable_daily_restart*. See *pyjs8call.schedulemonitor* for more information.
1154        '''
1155        if self._daily_restart_schedule is None:
1156            return
1157            
1158        self._client.schedule.remove(self._daily_restart_schedule.dict()['time'], schedule=self._daily_restart_schedule)
1159        self._daily_restart_schedule = None
1160
1161    def daily_restart_enabled(self):
1162        '''Whether daily JS8Call restart is enabled.
1163
1164        Returns:
1165            bool: True if associated schedule entry is set, False otherwise
1166        '''
1167        if self._daily_restart_schedule is not None:
1168            return True
1169        else:
1170            return False
1171
1172    def get_daily_restart_time(self):
1173        '''Get daily JS8Call restart time.
1174
1175        Returns:
1176            str or None: Local restart time in 24-hour format (ex. '23:30'), or None if not enabled
1177        '''
1178        if self._daily_restart_schedule is None:
1179            return
1180
1181        return self._daily_restart_schedule.start.strftime('%H:%M')

Settings function container.

This class is initilized by pyjs8call.client.Client.

Settings(client)
 46    def __init__(self, client):
 47        '''Initialize settings object.
 48
 49        Returns:
 50            pyjs8call.client.Settings: Constructed setting object
 51        '''
 52        self._client = client
 53        self._daily_restart_schedule = None
 54        self.loaded_settings = None
 55        '''Loaded settings container (see python3 configparser for more information)'''
 56
 57        self._settings_map = {
 58            'station' : {
 59                'callsign': lambda value: self.set_station_callsign(value),
 60                'grid': lambda value: self.set_station_grid(value),
 61                'speed': lambda value: self.set_speed(value),
 62                'freq': lambda value: self.set_freq(value),
 63                'frequency': lambda value: self.set_freq(value),
 64                'offset': lambda value: self.set_offset(value),
 65                'info': lambda value: self.set_station_info(value),
 66                'append_pyjs8call_info': lambda value: self.append_pyjs8call_to_station_info(),
 67                'daily_restart': lambda value: self.enable_daily_restart(value) if value else self.disable_daily_restart()
 68            },
 69            'general': {
 70                'groups': lambda value: self.set_groups(value),
 71                'multi_speed_decode': lambda value: self.enable_multi_decode() if value else self.disable_multi_decode(),
 72                'autoreply_on_at_startup': lambda value: self.enable_autoreply_startup() if value else self.disable_autoreply_startup(),
 73                'autoreply_confirmation': lambda value: self.enable_autoreply_confirmation() if value else self.disable_autoreply_confirmation(),
 74                'allcall': lambda value: self.enable_allcall() if value else self.disable_allcall(),
 75                'reporting': lambda value: self.enable_reporting() if value else self.disable_reporting(),
 76                'transmit': lambda value: self.enable_transmit() if value else self.disable_transmit(),
 77                'idle_timeout': lambda value: self.set_idle_timeout(value),
 78                'distance_units': lambda value: self.set_distance_units(value)
 79            },
 80            'heartbeat': {
 81                'enable': lambda value: self._client.heartbeat.enable() if value else self._client.heartbeat.disable(),
 82                'interval': lambda value: self.set_heartbeat_interval(value),
 83                'acknowledgements': lambda value: self.enable_heartbeat_acknowledgements() if value else self.disable_heartbeat_acknowledgements(),
 84                'pause_during_qso': lambda value: self.pause_heartbeat_during_qso() if value else self.allow_heartbeat_during_qso()
 85            },
 86            'profile': {
 87                'profile': lambda value: self.set_profile(value),
 88                'set_profile_on_exit': lambda value: self._client.set_profile_on_exit(value)
 89            },
 90            'highlight': {
 91                'primary_words': lambda value: self.set_primary_highlight_words(value),
 92                'secondary_words': lambda value: self.set_secondary_highlight_words(value)
 93            },
 94            'spots': {
 95                'watch_stations': lambda value: self._client.spots.set_watched_stations(value),
 96                'watch_groups': lambda value: self._client.spots.set_watched_groups(value)
 97            },
 98            'notifications': {
 99                'enable': lambda value: self._client.notifications.enable() if value else self._client.notifications.disable(),
100                'smtp_server': lambda value: self._client.notifications.set_smtp_server(value),
101                'smtp_port': lambda value: self._client.notifications.set_smtp_server_port(value),
102                'smtp_email_address': lambda value: self._client.notifications.set_smtp_email_address(value),
103                'smtp_password': lambda value: self._client.notifications.set_smtp_password(value),
104                'notification_email_address': lambda value: self._client.notifications.set_email_destination(value),
105                'notification_email_subject': lambda value: self._client.notifications.set_email_subject(value),
106                'incoming': lambda value: self._client.notifications.enable_incoming() if value else self._client.notifications.disable_incoming(),
107                'spots': lambda value: self._client.notifications.enable_spots() if value else self._client.notifications.disable_spots(),
108                'station_spots': lambda value: self._client.notifications.enable_station_spots() if value else self._client.notifications.disable_station_spots(),
109                'group_spots': lambda value: self._client.notifications.enable_group_spots() if value else self._client.notifications.disable_group_spots()
110            }
111        }
112
113        # settings set via js8call config file
114        self._pre_start_settings = {
115            'station' : [
116                'callsign',
117                'speed'
118            ],
119            'general': [
120                'groups', 
121                'multi_speed_decode',
122                'autoreplay_on_at_startup',
123                'autoreply_confirmation',
124                'allcall',
125                'reporting',
126                'transmit',
127                'idle_timeout',
128                'distance_units'
129            ],
130            'heartbeat': [
131                'interval',
132                'acknowledgements',
133                'pause_during_qso'
134            ],
135            'profile': [
136                'profile',
137                'set_profile_on_exit'
138            ],
139            'highlight': [
140                'primary_words',
141                'secondary_words'
142            ],
143            'spots': [
144            ],
145            'notifications': [
146            ]
147        }

Initialize settings object.

Returns:

pyjs8call.client.Settings: Constructed setting object

loaded_settings

Loaded settings container (see python3 configparser for more information)

def load(self, settings_path):
149    def load(self, settings_path):
150        '''Load pyjs8call settings from file.
151
152        The settings file referenced here is specific to pyjs8call, and is not the same as the JS8Call configuration file. The pyjs8call settings file is not required.
153
154        This function must be called before calling *client.start()*. Settings that must be set before or after starting the JS8Call application are handled automatically. Settings that affect the JS8Call config file are set immediately. All other settings are set after *client.start()* is called.
155
156        Example settings file:
157
158        ```
159        [station]
160
161        callsign=CALL0SIGN
162        grid=EM19
163        speed=normal
164        freq=7078000
165        offset=1750
166        info=QDX 5W, DIPOLE 30FT
167        append_pyjs8call_info=true
168        daily_restart=02:00
169
170        [general]
171
172        groups=@TTP, @AMRRON
173        multi_speed_decode=true
174        autoreply_on_at_startup=true
175        autoreply_confirmation=false
176        allcall=true
177        reporting=true
178        transmit=true
179        idle_timeout=0
180        distance_units=miles
181
182        [heartbeat]
183
184        enable=true
185        interval=15
186        acknowledgements=true
187        pause_during_qso=true
188
189        [profile]
190
191        profile=Default
192        set_profile_on_exit=Default
193
194        [highlight]
195
196        primary_words=KT7RUN, OH8STN
197        secondary_words=simplyequipped
198
199        [spots]
200
201        watch_stations=KT7RUN, OH8STN
202        watch_groups=@TTP, @AMRRON
203
204        [notifications]
205
206        enable=true
207        smtp_server=smtp.gmail.com
208        smtp_port=465
209        smtp_email_address=email@address.com
210        smtp_password=APP_PASSWORD
211        notification_email_address=0123456789@vtext.com
212        notification_email_subject=
213        incoming=true
214        spots=false
215        station_spots=true
216        group_spots=true
217        ```
218
219        Args:
220            settings_path (str): Relative or absolute path to settings file
221
222        Raises:
223            OSError: Specified settings file not found
224        '''
225        settings_path = os.path.expanduser(settings_path)
226        settings_path = os.path.abspath(settings_path)
227
228        if not os.path.exists(settings_path):
229            raise FileNotFoundError('Specified settings file not found: {}'.format(settings_path))
230
231        self.loaded_settings = configparser.ConfigParser(interpolation = None)
232        self.loaded_settings.read(settings_path)
233
234        self.apply_loaded_settings()

Load pyjs8call settings from file.

The settings file referenced here is specific to pyjs8call, and is not the same as the JS8Call configuration file. The pyjs8call settings file is not required.

This function must be called before calling client.start(). Settings that must be set before or after starting the JS8Call application are handled automatically. Settings that affect the JS8Call config file are set immediately. All other settings are set after client.start() is called.

Example settings file:

[station]

callsign=CALL0SIGN
grid=EM19
speed=normal
freq=7078000
offset=1750
info=QDX 5W, DIPOLE 30FT
append_pyjs8call_info=true
daily_restart=02:00

[general]

groups=@TTP, @AMRRON
multi_speed_decode=true
autoreply_on_at_startup=true
autoreply_confirmation=false
allcall=true
reporting=true
transmit=true
idle_timeout=0
distance_units=miles

[heartbeat]

enable=true
interval=15
acknowledgements=true
pause_during_qso=true

[profile]

profile=Default
set_profile_on_exit=Default

[highlight]

primary_words=KT7RUN, OH8STN
secondary_words=simplyequipped

[spots]

watch_stations=KT7RUN, OH8STN
watch_groups=@TTP, @AMRRON

[notifications]

enable=true
smtp_server=smtp.gmail.com
smtp_port=465
smtp_email_address=email@address.com
smtp_password=APP_PASSWORD
notification_email_address=0123456789@vtext.com
notification_email_subject=
incoming=true
spots=false
station_spots=true
group_spots=true
Arguments:
  • settings_path (str): Relative or absolute path to settings file
Raises:
  • OSError: Specified settings file not found
def apply_loaded_settings(self, post_start=False):
236    def apply_loaded_settings(self, post_start=False):
237        '''Apply loaded pyjs8call settings.
238
239        This function is called internally by *load_settings()* and *client.start()*.
240
241        Args:
242            post_start (bool): Post start processing if True, pre start processing if False, defaults to False
243        '''
244        for section in self.loaded_settings.sections():
245            #skip unsupported section
246            if section not in self._settings_map:
247                continue
248
249            for key, value in self.loaded_settings[section].items():
250                # skip unsupported key
251                if key not in self._settings_map[section]:
252                    continue
253
254                # skip post start settings during pre start processing
255                if not post_start and key in self._pre_start_settings[section]:
256                    value = self._parse_loaded_value(value)
257                    self._settings_map[section][key](value)
258
259                # skip pre start settings during post start processing
260                if post_start and not key in self._pre_start_settings[section]:
261                    value = self._parse_loaded_value(value)
262                    self._settings_map[section][key](value)

Apply loaded pyjs8call settings.

This function is called internally by load_settings() and client.start().

Arguments:
  • post_start (bool): Post start processing if True, pre start processing if False, defaults to False
def enable_heartbeat_networking(self):
283    def enable_heartbeat_networking(self):
284        '''Enable heartbeat networking via config file.
285        
286        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
287        
288        Note that this function disables JS8Call application heartbeat networking via the config file. To enable the pyjs8call heartbeat network messaging module see *client.heartbeat.enable()*.
289        '''
290        self._client.config.set('Common', 'SubModeHB', 'true')

Enable heartbeat networking via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Note that this function disables JS8Call application heartbeat networking via the config file. To enable the pyjs8call heartbeat network messaging module see client.heartbeat.enable().

def disable_heartbeat_networking(self):
292    def disable_heartbeat_networking(self):
293        '''Disable heartbeat networking via config file.
294        
295        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
296        
297        Note that this function disables JS8Call application heartbeat networking via the config file. To disable the pyjs8call heartbeat network messaging module see *client.heartbeat.disable()*.
298        '''
299        self._client.config.set('Common', 'SubModeHB', 'false')

Disable heartbeat networking via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Note that this function disables JS8Call application heartbeat networking via the config file. To disable the pyjs8call heartbeat network messaging module see client.heartbeat.disable().

def heartbeat_networking_enabled(self):
301    def heartbeat_networking_enabled(self):
302        '''Whether heartbeat networking enabled in config file.
303        
304        Returns:
305            bool: True if heartbeat networking enabled, False otherwise
306        '''
307        return self._client.config.get('Common', 'SubModeHB', bool)

Whether heartbeat networking enabled in config file.

Returns:

bool: True if heartbeat networking enabled, False otherwise

def get_heartbeat_interval(self):
309    def get_heartbeat_interval(self):
310        '''Get heartbeat networking interval.
311        
312        Returns:
313            int: Heartbeat networking time interval in minutes
314        '''
315        return self._client.config.get('Common', 'HBInterval', int)

Get heartbeat networking interval.

Returns:

int: Heartbeat networking time interval in minutes

def set_heartbeat_interval(self, interval):
317    def set_heartbeat_interval(self, interval):
318        '''Set the heartbeat networking interval.
319        
320        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
321
322        Args:
323            interval (int): New heartbeat networking time interval in minutes
324        
325        Returns:
326            int: Current heartbeat networking time interval in minutes
327        '''
328        return self._client.config.set('Common', 'HBInterval', interval)

Set the heartbeat networking interval.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Arguments:
  • interval (int): New heartbeat networking time interval in minutes
Returns:

int: Current heartbeat networking time interval in minutes

def enable_heartbeat_acknowledgements(self):
330    def enable_heartbeat_acknowledgements(self):
331        '''Enable heartbeat acknowledgements via config file.
332        
333        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
334
335        Also enables JS8Call heartbeat networking, since heartbeat acknowledgements will not be enabled without heartbeat networking enabled first. This only enables the feature within JS8Call, and does not casue heartbeats to be sent.
336        '''
337        self.enable_heartbeat_networking()
338        self._client.config.set('Common', 'SubModeHBAck', 'true')

Enable heartbeat acknowledgements via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Also enables JS8Call heartbeat networking, since heartbeat acknowledgements will not be enabled without heartbeat networking enabled first. This only enables the feature within JS8Call, and does not casue heartbeats to be sent.

def disable_heartbeat_acknowledgements(self):
340    def disable_heartbeat_acknowledgements(self):
341        '''Disable heartbeat acknowledgements via config file.
342        
343        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
344        '''
345        self._client.config.set('Common', 'SubModeHBAck', 'false')

Disable heartbeat acknowledgements via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def heartbeat_acknowledgements_enabled(self):
347    def heartbeat_acknowledgements_enabled(self):
348        '''Whether heartbeat acknowledgements enabled in config file.
349        
350        Returns:
351            bool: True if heartbeat acknowledgements enabled, False otherwise
352        '''
353        return self._client.config.get('Common', 'SubModeHBAck', bool)

Whether heartbeat acknowledgements enabled in config file.

Returns:

bool: True if heartbeat acknowledgements enabled, False otherwise

def pause_heartbeat_during_qso(self):
355    def pause_heartbeat_during_qso(self):
356        '''Pause heartbeat messages during QSO via config file.
357        
358        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
359        '''
360        self._client.config.set('Configuration', 'HeartbeatQSOPause', 'true')

Pause heartbeat messages during QSO via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def allow_heartbeat_during_qso(self):
362    def allow_heartbeat_during_qso(self):
363        '''Allow heartbeat messages during QSO via config file.
364        
365        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
366        '''
367        self._client.config.set('Configuration', 'HeartbeatQSOPause', 'false')

Allow heartbeat messages during QSO via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def heartbeat_during_qso_paused(self):
369    def heartbeat_during_qso_paused(self):
370        '''Whether heartbeat messages paused during QSO in config file.
371        
372        Returns:
373            bool: True if heartbeat messages paused during QSO, False otherwise
374        '''
375        return self._client.config.get('Configuration', 'HeartbeatQSOPause', bool)

Whether heartbeat messages paused during QSO in config file.

Returns:

bool: True if heartbeat messages paused during QSO, False otherwise

def enable_multi_decode(self):
377    def enable_multi_decode(self):
378        '''Enable multi-speed decoding via config file.
379        
380        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
381        '''
382        self._client.config.set('Common', 'SubModeHBMultiDecode', 'true')

Enable multi-speed decoding via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def disable_multi_decode(self):
384    def disable_multi_decode(self):
385        '''Disable multi-speed decoding via config file.
386        
387        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
388        '''
389        self._client.config.set('Common', 'SubModeMultiDecode', 'false')

Disable multi-speed decoding via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def multi_decode_enabled(self):
391    def multi_decode_enabled(self):
392        '''Whether multi-decode enabled in config file.
393        
394        Returns:
395            bool: True if multi-decode enabled, False otherwise
396        '''
397        return self._client.config.get('Common', 'SubModeMultiDecode', bool)

Whether multi-decode enabled in config file.

Returns:

bool: True if multi-decode enabled, False otherwise

def enable_autoreply_startup(self):
399    def enable_autoreply_startup(self):
400        '''Enable autoreply on start-up via config file.
401        
402        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
403        '''
404        self._client.config.set('Configuration', 'AutoreplyOnAtStartup', 'true')

Enable autoreply on start-up via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def disable_autoreply_startup(self):
406    def disable_autoreply_startup(self):
407        '''Disable autoreply on start-up via config file.
408        
409        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
410        '''
411        self._client.config.set('Configuration', 'AutoreplyOnAtStartup', 'false')

Disable autoreply on start-up via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def autoreply_startup_enabled(self):
413    def autoreply_startup_enabled(self):
414        '''Whether autoreply enabled at start-up in config file.
415        
416        Returns:
417            bool: True if autoreply is enabled at start-up, False otherwise
418        '''
419        return self._client.config.get('Configuration', 'AutoreplyOnAtStartup', bool)

Whether autoreply enabled at start-up in config file.

Returns:

bool: True if autoreply is enabled at start-up, False otherwise

def enable_autoreply_confirmation(self):
421    def enable_autoreply_confirmation(self):
422        '''Enable autoreply confirmation via config file.
423        
424        When running headless the autoreply confirmation dialog box will be inaccessible.
425        
426        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
427        '''
428        self._client.config.set('Configuration', 'AutoreplyConfirmation', 'true')

Enable autoreply confirmation via config file.

When running headless the autoreply confirmation dialog box will be inaccessible.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def disable_autoreply_confirmation(self):
430    def disable_autoreply_confirmation(self):
431        '''Disable autoreply confirmation via config file.
432        
433        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
434        '''
435        self._client.config.set('Configuration', 'AutoreplyConfirmation', 'false')

Disable autoreply confirmation via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def autoreply_confirmation_enabled(self):
437    def autoreply_confirmation_enabled(self):
438        '''Whether autoreply confirmation enabled in config file.
439        
440        Returns:
441            bool: True if autoreply confirmation enabled, False otherwise
442        '''
443        return self._client.config.get('Configuration', 'AutoreplyConfirmation', bool)

Whether autoreply confirmation enabled in config file.

Returns:

bool: True if autoreply confirmation enabled, False otherwise

def enable_allcall(self):
445    def enable_allcall(self):
446        '''Enable @ALLCALL participation via config file.
447        
448        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
449        '''
450        self._client.config.set('Configuration', 'AvoidAllcall', 'false')

Enable @ALLCALL participation via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def disable_allcall(self):
452    def disable_allcall(self):
453        '''Disable @ALLCALL participation via config file.
454        
455        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
456        '''
457        self._client.config.set('Configuration', 'AvoidAllcall', 'true')

Disable @ALLCALL participation via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def allcall_enabled(self):
459    def allcall_enabled(self):
460        '''Whether @ALLCALL participation enabled in config file.
461        
462        Returns:
463            bool: True if @ALLCALL participation enabled, False otherwise
464        '''
465        return not self._client.config.get('Configuration', 'AvoidAllcall', bool)

Whether @ALLCALL participation enabled in config file.

Returns:

bool: True if @ALLCALL participation enabled, False otherwise

def enable_reporting(self):
467    def enable_reporting(self):
468        '''Enable PSKReporter reporting via config file.
469        
470        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
471        '''
472        self._client.config.set('Configuration', 'PSKReporter', 'true')

Enable PSKReporter reporting via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def disable_reporting(self):
474    def disable_reporting(self):
475        '''Disable PSKReporter reporting via config file.
476        
477        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
478        '''
479        self._client.config.set('Configuration', 'PSKReporter', 'false')

Disable PSKReporter reporting via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def reporting_enabled(self):
481    def reporting_enabled(self):
482        '''Whether PSKReporter reporting enabled in config file.
483        
484        Returns:
485            bool: True if reporting enabled, False otherwise
486        '''
487        return self._client.config.get('Configuration', 'PSKReporter', bool)

Whether PSKReporter reporting enabled in config file.

Returns:

bool: True if reporting enabled, False otherwise

def enable_transmit(self):
489    def enable_transmit(self):
490        '''Enable JS8Call transmitting via config file.
491        
492        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
493        '''
494        self._client.config.set('Configuration', 'TransmitOFF', 'false')

Enable JS8Call transmitting via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def disable_transmit(self):
496    def disable_transmit(self):
497        '''Disable JS8Call transmitting via config file.
498        
499        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
500        '''
501        self._client.config.set('Configuration', 'TransmitOFF', 'true')

Disable JS8Call transmitting via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

def transmit_enabled(self):
503    def transmit_enabled(self):
504        '''Whether JS8Call transmitting enabled in config file.
505        
506        Returns:
507            bool: True if transmitting enabled, False otherwise
508        '''
509        return not self._client.config.get('Configuration', 'TransmitOFF', bool)

Whether JS8Call transmitting enabled in config file.

Returns:

bool: True if transmitting enabled, False otherwise

def get_profile(self):
511    def get_profile(self):
512        '''Get active JS8call configuration profile via config file.
513
514        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
515
516        Returns:
517            str: Name of the active configuration profile
518        '''
519        return self._client.config.get_active_profile()

Get active JS8call configuration profile via config file.

This is a convenience function. See pyjs8call.confighandler for other configuration related functions.

Returns:

str: Name of the active configuration profile

def get_profile_list(self):
521    def get_profile_list(self):
522        '''Get list of JS8Call configuration profiles via config file.
523
524        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
525
526        Returns:
527            list: List of configuration profile names
528        '''
529        return self._client.config.get_profile_list()

Get list of JS8Call configuration profiles via config file.

This is a convenience function. See pyjs8call.confighandler for other configuration related functions.

Returns:

list: List of configuration profile names

def set_profile(self, profile, restore_on_exit=False, create=False):
531    def set_profile(self, profile, restore_on_exit=False, create=False):
532        '''Set active JS8Call configuration profile via config file.
533        
534        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
535        
536        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
537
538        Args:
539            profile (str): Profile name
540            restore_on_exit (bool): Restore previous profile on exit, defaults to False
541            create (bool): Create a new profile (copying from Default) if the specified profile does not exist, defaults to False
542
543        Raises:
544            ValueError: Specified profile name does not exist
545        '''
546        if profile not in self.get_profile_list():
547            if create:
548                # copy from Default profile
549                self.create_new_profile(profile)
550            else:
551                raise ValueError('Config profile \'' + profile + '\' does not exist')
552
553        if restore_on_exit:
554            self._client._previous_profile = self.get_profile()
555            
556        # set profile as active
557        self._client.config.change_profile(profile)

Set active JS8Call configuration profile via config file.

This is a convenience function. See pyjs8call.confighandler for other configuration related functions.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Arguments:
  • profile (str): Profile name
  • restore_on_exit (bool): Restore previous profile on exit, defaults to False
  • create (bool): Create a new profile (copying from Default) if the specified profile does not exist, defaults to False
Raises:
  • ValueError: Specified profile name does not exist
def create_new_profile(self, new_profile, copy_profile='Default'):
559    def create_new_profile(self, new_profile, copy_profile='Default'):
560        '''Create new JS8Call configuration profile.
561            
562        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
563            
564        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
565    
566        Args:
567            new_profile (str): Name of new profile to create
568            copy_profile (str): Name of an existing profile to copy when creating the new profile, defaults to 'Default'
569        '''
570        self._client.config.create_new_profile(new_profile, copy_profile)

Create new JS8Call configuration profile.

This is a convenience function. See pyjs8call.confighandler for other configuration related functions.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Arguments:
  • new_profile (str): Name of new profile to create
  • copy_profile (str): Name of an existing profile to copy when creating the new profile, defaults to 'Default'
def get_groups_list(self):
572    def get_groups_list(self):
573        '''Get list of configured JS8Call groups via config file.
574
575        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
576
577        Returns:
578            list: List of configured group names
579        '''
580        return self._client.config.get_groups()

Get list of configured JS8Call groups via config file.

This is a convenience function. See pyjs8call.confighandler for other configuration related functions.

Returns:

list: List of configured group names

def add_group(self, group):
582    def add_group(self, group):
583        '''Add configured JS8Call group via config file.
584        
585        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
586        
587        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
588
589        Args:
590            group (str): Group name
591        '''
592        self._client.config.add_group(group)

Add configured JS8Call group via config file.

This is a convenience function. See pyjs8call.confighandler for other configuration related functions.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Arguments:
  • group (str): Group name
def remove_group(self, group):
594    def remove_group(self, group):
595        '''Remove configured JS8Call group via config file.
596        
597        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
598        
599        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
600
601        Args:
602            group (str): Group name
603        '''
604        self._client.config.remove_group(group)

Remove configured JS8Call group via config file.

This is a convenience function. See pyjs8call.confighandler for other configuration related functions.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Arguments:
  • group (str): Group name
def set_groups(self, groups):
606    def set_groups(self, groups):
607        '''Set configured JS8Call groups via config file.
608        
609        This is a convenience function. See pyjs8call.confighandler for other configuration related functions.
610        
611        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
612
613        Args:
614            groups (list): List of group names
615        '''
616        if isinstance(groups, str):
617            groups = groups.split(',')
618
619        groups = ['@' + group.strip(' @') for group in groups]
620        groups = ', '.join(groups)
621        self._client.config.set('Configuration', 'MyGroups', groups)

Set configured JS8Call groups via config file.

This is a convenience function. See pyjs8call.confighandler for other configuration related functions.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Arguments:
  • groups (list): List of group names
def get_primary_highlight_words(self):
623    def get_primary_highlight_words(self):
624        '''Get primary highlight words via config file.
625
626        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
627        
628        Returns:
629            list: Words that should be highlighted on the JC8Call UI
630        '''
631        words = self._client.config.get('Configuration', 'PrimaryHighlightWords')
632
633        if words == '@Invalid()':
634            words = []
635        elif words is not None:
636            words = words.split(', ')
637
638        return words

Get primary highlight words via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Returns:

list: Words that should be highlighted on the JC8Call UI

def set_primary_highlight_words(self, words):
640    def set_primary_highlight_words(self, words):
641        '''Set primary highlight words via config file.
642
643        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
644        
645        Args:
646            words (list): Words that should be highlighted on the JC8Call UI
647        '''
648        if isinstance(words, str):
649            words = [word.strip() for word in words.split(',')]
650
651        if len(words) == 0:
652            words = '@Invalid()'
653        else:
654            words = ', '.join(words)
655
656        self._client.config.set('Configuration', 'PrimaryHighlightWords', words)

Set primary highlight words via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Arguments:
  • words (list): Words that should be highlighted on the JC8Call UI
def get_secondary_highlight_words(self):
658    def get_secondary_highlight_words(self):
659        '''Get secondary highlight words via config file.
660
661        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
662        
663        Returns:
664            list: Words that should be highlighted on the JC8Call UI
665        '''
666        words = self._client.config.get('Configuration', 'SecondaryHighlightWords')
667
668        if words == '@Invalid()':
669            words = []
670        elif words is not None:
671            words = words.split(', ')
672
673        return words

Get secondary highlight words via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Returns:

list: Words that should be highlighted on the JC8Call UI

def set_secondary_highlight_words(self, words):
675    def set_secondary_highlight_words(self, words):
676        '''Set secondary highlight words via config file.
677
678        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
679        
680        Args:
681            words (list): Words that should be highlighted on the JC8Call UI
682        '''
683        if isinstance(words, str):
684            words = [word.strip() for word in words.split(',')]
685
686        if len(words) == 0:
687            words = '@Invalid()'
688        else:
689            words = ', '.join(words)
690
691        self._client.config.set('Configuration', 'SecondaryHighlightWords', words)

Set secondary highlight words via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Arguments:
  • words (list): Words that should be highlighted on the JC8Call UI
def submode_to_speed(self, submode):
693    def submode_to_speed(self, submode):
694        '''Map submode *int* to speed *str*.
695
696        | Submode | Speed |
697        | -------- | -------- |
698        | 0 | normal |
699        | 1 | fast |
700        | 2 | turbo |
701        | 4 | slow |
702        | 8 | ultra |
703
704        Args:
705            submode (int): Submode to map to text
706
707        Returns:
708            str: Speed as text
709        '''
710        # map integer to text
711        speeds = {4:'slow', 0:'normal', 1:'fast', 2:'turbo', 8:'ultra'}
712
713        if submode is not None and int(submode) in speeds:
714            return speeds[int(submode)]
715        else:
716            raise ValueError('Invalid submode \'' + str(submode) + '\'')

Map submode int to speed str.

Submode Speed
0 normal
1 fast
2 turbo
4 slow
8 ultra
Arguments:
  • submode (int): Submode to map to text
Returns:

str: Speed as text

def get_speed(self, update=False):
718    def get_speed(self, update=False):
719        '''Get JS8Call modem speed.
720
721        Possible modem speeds:
722        - slow
723        - normal
724        - fast
725        - turbo
726        - ultra
727
728        Args:
729            update (bool): Update speed if True or use local state if False, defaults to False
730
731        Returns:
732            str: JS8call modem speed setting
733        '''
734        speed = self._client.js8call.get_state('speed')
735
736        if update or speed is None:
737            msg = Message()
738            msg.set('type', Message.MODE_GET_SPEED)
739            self._client.js8call.send(msg)
740            speed = self._client.js8call.watch('speed')
741
742        return self.submode_to_speed(speed)

Get JS8Call modem speed.

Possible modem speeds:

  • slow
  • normal
  • fast
  • turbo
  • ultra
Arguments:
  • update (bool): Update speed if True or use local state if False, defaults to False
Returns:

str: JS8call modem speed setting

def set_speed(self, speed):
744    def set_speed(self, speed):
745        '''Set JS8Call modem speed via config file.
746
747        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
748
749        Possible modem speeds:
750        - slow
751        - normal
752        - fast
753        - turbo
754        - ultra
755
756        Args:
757            speed (str): Speed to set
758
759        Returns:
760            str: JS8Call modem speed setting
761
762        '''
763        if isinstance(speed, str):
764            speeds = {'slow':4, 'normal':0, 'fast':1, 'turbo':2, 'ultra':8}
765            if speed in speeds:
766                speed = speeds[speed]
767            else:
768                raise ValueError('Invalid speed: ' + str(speed))
769
770        return self._client.config.set('Common', 'SubMode', speed)

Set JS8Call modem speed via config file.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Possible modem speeds:

  • slow
  • normal
  • fast
  • turbo
  • ultra
Arguments:
  • speed (str): Speed to set
Returns:

str: JS8Call modem speed setting

def get_freq(self, update=False):
780    def get_freq(self, update=False):
781        '''Get JS8Call dial frequency.
782
783        Args:
784            update (bool): Update if True or use local state if False, defaults to False
785
786        Returns:
787            int: Dial frequency in Hz
788        '''
789        freq = self._client.js8call.get_state('dial')
790
791        if update or freq is None:
792            msg = Message()
793            msg.type = Message.RIG_GET_FREQ
794            self._client.js8call.send(msg)
795            freq = self._client.js8call.watch('dial')
796
797        return freq

Get JS8Call dial frequency.

Arguments:
  • update (bool): Update if True or use local state if False, defaults to False
Returns:

int: Dial frequency in Hz

def set_freq(self, freq):
799    def set_freq(self, freq):
800        '''Set JS8Call dial frequency.
801
802        Args:
803            freq (int): Dial frequency in Hz
804
805        Returns:
806            int: Dial frequency in Hz
807        '''
808        msg = Message()
809        msg.set('type', Message.RIG_SET_FREQ)
810        msg.set('params', {'DIAL': freq, 'OFFSET': self._client.js8call.get_state('offset')})
811        self._client.js8call.send(msg)
812        time.sleep(self._client._set_get_delay)
813        return self.get_freq(update = True)

Set JS8Call dial frequency.

Arguments:
  • freq (int): Dial frequency in Hz
Returns:

int: Dial frequency in Hz

def get_band(self):
815    def get_band(self):
816        '''Get frequency band designation.
817
818        Returns:
819            str: Band designator like \'40m\' or Client.OOB (out-of-band)
820        '''
821        return Client.freq_to_band(self.get_freq())

Get frequency band designation.

Returns:

str: Band designator like '40m' or Client.OOB (out-of-band)

def get_offset(self, update=False):
823    def get_offset(self, update=False):
824        '''Get JS8Call offset frequency.
825
826        Args:
827            update (bool): Update if True or use local state if False, defaults to False
828
829        Returns:
830            int: Offset frequency in Hz
831        '''
832        offset = self._client.js8call.get_state('offset')
833        
834        if update or offset is None:
835            msg = Message()
836            msg.type = Message.RIG_GET_FREQ
837            self._client.js8call.send(msg)
838            offset = self._client.js8call.watch('offset')
839
840        return offset

Get JS8Call offset frequency.

Arguments:
  • update (bool): Update if True or use local state if False, defaults to False
Returns:

int: Offset frequency in Hz

def set_offset(self, offset):
842    def set_offset(self, offset):
843        '''Set JS8Call offset frequency.
844
845        Args:
846            offset (int): Offset frequency in Hz
847
848        Returns:
849            int: Offset frequency in Hz
850        '''
851        msg = Message()
852        msg.set('type', Message.RIG_SET_FREQ)
853        msg.set('params', {'DIAL': self._client.js8call.get_state('dial'), 'OFFSET': offset})
854        self._client.js8call.send(msg)
855        time.sleep(self._client._set_get_delay)
856        return self.get_offset(update = True)

Set JS8Call offset frequency.

Arguments:
  • offset (int): Offset frequency in Hz
Returns:

int: Offset frequency in Hz

def get_station_callsign(self, update=False):
858    def get_station_callsign(self, update=False):
859        '''Get JS8Call callsign.
860
861        Args:
862            update (bool): Update if True or use local state if False, defaults to False
863
864        Returns:
865            str: JS8Call configured callsign
866        '''
867        callsign = self._client.js8call.get_state('callsign')
868
869        if update or callsign is None:
870            msg = Message()
871            msg.type = Message.STATION_GET_CALLSIGN
872            self._client.js8call.send(msg)
873            callsign = self._client.js8call.watch('callsign')
874
875        return callsign

Get JS8Call callsign.

Arguments:
  • update (bool): Update if True or use local state if False, defaults to False
Returns:

str: JS8Call configured callsign

def set_station_callsign(self, callsign):
877    def set_station_callsign(self, callsign):
878        '''Set JS8Call callsign.
879
880        Callsign must be a maximum of 9 characters and contain at least one number.
881
882        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
883
884        Args:
885            callsign (str): Callsign to set
886
887        Returns:
888            str: JS8Call configured callsign
889        '''
890        callsign = callsign.upper()
891
892        if len(callsign) <= 9 and any(char.isdigit() for char in callsign):
893            return self._client.config.set('Configuration', 'MyCall', callsign)
894        else:
895            raise ValueError('callsign must be <= 9 characters in length and contain at least 1 number')

Set JS8Call callsign.

Callsign must be a maximum of 9 characters and contain at least one number.

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Arguments:
  • callsign (str): Callsign to set
Returns:

str: JS8Call configured callsign

def get_idle_timeout(self):
897    def get_idle_timeout(self):
898        '''Get JS8Call idle timeout.
899
900        Returns:
901            int: Idle timeout in minutes
902        '''
903        return self._client.config.get('Configuration', 'TxIdleWatchdog', value_type=int)

Get JS8Call idle timeout.

Returns:

int: Idle timeout in minutes

def set_idle_timeout(self, timeout):
905    def set_idle_timeout(self, timeout):
906        '''Set JS8Call idle timeout.
907
908        If the JS8Call idle timeout is between 1 and 5 minutes, JS8Call will force the idle timeout to 5 minutes on the next application start or exit.
909
910        The maximum idle timeout is 1440 minutes (24 hours).
911
912        Disable the idle timeout by setting it to 0 (zero).
913
914        It is recommended that this function be called before calling *client.start()*. If this function is called after *client.start()* then the application will have to be restarted to utilize the new config file settings. See *client.restart()*.
915
916        Args:
917            timeout (int): Idle timeout in minutes
918
919        Returns:
920            int: Current idle timeout in minutes
921
922        Raises:
923            ValueError: Idle timeout must be between 0 and 1440 minutes
924        '''
925        if timeout < 0 or timeout > 1440:
926            raise ValueError('Idle timeout must be between 0 and 1440 minutes')
927
928        self._client.config.set('Configuration', 'TxIdleWatchdog', timeout)
929        return self.get_idle_timeout()

Set JS8Call idle timeout.

If the JS8Call idle timeout is between 1 and 5 minutes, JS8Call will force the idle timeout to 5 minutes on the next application start or exit.

The maximum idle timeout is 1440 minutes (24 hours).

Disable the idle timeout by setting it to 0 (zero).

It is recommended that this function be called before calling client.start(). If this function is called after client.start() then the application will have to be restarted to utilize the new config file settings. See client.restart().

Arguments:
  • timeout (int): Idle timeout in minutes
Returns:

int: Current idle timeout in minutes

Raises:
  • ValueError: Idle timeout must be between 0 and 1440 minutes
def get_distance_units_miles(self):
931    def get_distance_units_miles(self):
932        '''Get JS8Call distance unit setting.
933        
934        Returns:
935            bool: True if distance units are set to miles, False if km
936        '''
937        return self._client.config.get('Configuration', 'Miles', bool)

Get JS8Call distance unit setting.

Returns:

bool: True if distance units are set to miles, False if km

def set_distance_units_miles(self, units_miles):
939    def set_distance_units_miles(self, units_miles):
940        '''Set JS8Call distance unit setting.
941        
942        Args:
943            units_miles (bool): Set units to miles if True, set to km if False
944            
945        Returns:
946            bool: True if distance units are set to miles, False if km
947        '''
948        self._client.config.set('Configuration', 'Miles', str(units_miles).lower())
949        return self.get_distance_units_miles()

Set JS8Call distance unit setting.

Arguments:
  • units_miles (bool): Set units to miles if True, set to km if False
Returns:

bool: True if distance units are set to miles, False if km

def get_distance_units(self):
951    def get_distance_units(self):
952        '''Get JS8Call distance units.
953        
954        Returns:
955            str: Configured distance units: 'mi' or 'km'
956        '''
957        if self.get_distance_units_miles():
958            return 'mi'
959        else:
960            return 'km'

Get JS8Call distance units.

Returns:

str: Configured distance units: 'mi' or 'km'

def set_distance_units(self, units):
962    def set_distance_units(self, units):
963        ''' Set JS8Call distance units.
964        
965        Args:
966            units (str): Distance units: 'mi', 'miles', 'km', or 'kilometers'
967            
968        Returns:
969            str: Configured distance units: 'miles' or 'km'
970        '''
971        if units.lower() in ['mi', 'miles']:
972            self.set_distance_units_miles(True)
973            return self.get_distance_units()
974        elif units.lower() in ['km', 'kilometers']:
975            self.set_distance_units_miles(False)
976            return self.get_distance_units()
977        else:
978            raise ValueError('Distance units must be: mi, miles, km, or kilometers')

Set JS8Call distance units.

Arguments:
  • units (str): Distance units: 'mi', 'miles', 'km', or 'kilometers'
Returns:

str: Configured distance units: 'miles' or 'km'

def get_station_grid(self, update=False):
980    def get_station_grid(self, update=False):
981        '''Get JS8Call grid square.
982
983        Args:
984            update (bool): Update if True or use local state if False, defaults to False
985
986        Returns:
987            str: JS8Call configured grid square
988        '''
989        grid = self._client.js8call.get_state('grid')
990
991        if update or grid is None:
992            msg = Message()
993            msg.type = Message.STATION_GET_GRID
994            self._client.js8call.send(msg)
995            grid = self._client.js8call.watch('grid')
996
997        return grid

Get JS8Call grid square.

Arguments:
  • update (bool): Update if True or use local state if False, defaults to False
Returns:

str: JS8Call configured grid square

def set_station_grid(self, grid):
 999    def set_station_grid(self, grid):
1000        '''Set JS8Call grid square.
1001
1002        Args:
1003            grid (str): Grid square
1004
1005        Returns:
1006            str: JS8Call configured grid square
1007        '''
1008        grid = grid.upper()
1009        msg = Message()
1010        msg.type = Message.STATION_SET_GRID
1011        msg.value = grid
1012        self._client.js8call.send(msg)
1013        time.sleep(self._client._set_get_delay)
1014        return self.get_station_grid(update = True)

Set JS8Call grid square.

Arguments:
  • grid (str): Grid square
Returns:

str: JS8Call configured grid square

def get_station_info(self, update=False):
1016    def get_station_info(self, update=False):
1017        '''Get JS8Call station information.
1018
1019        Args:
1020            update (bool): Update if True or use local state if False, defaults to False
1021
1022        Returns:
1023            str: JS8Call configured station information
1024        '''
1025        info = self._client.js8call.get_state('info')
1026
1027        if update or info is None:
1028            msg = Message()
1029            msg.type = Message.STATION_GET_INFO
1030            self._client.js8call.send(msg)
1031            info = self._client.js8call.watch('info')
1032
1033        return info

Get JS8Call station information.

Arguments:
  • update (bool): Update if True or use local state if False, defaults to False
Returns:

str: JS8Call configured station information

def set_station_info(self, info):
1035    def set_station_info(self, info):
1036        '''Set JS8Call station information.
1037
1038        *info* updated via API (if connected) and set in JS8Call configuration file.
1039
1040        Args:
1041            info (str): Station information
1042
1043        Returns:
1044            str: JS8Call configured station information
1045        '''
1046        if self._client.online:
1047            msg = Message()
1048            msg.type = Message.STATION_SET_INFO
1049            msg.value = info
1050            self._client.js8call.send(msg)
1051            time.sleep(self._client._set_get_delay)
1052            info = self.get_station_info(update = True)
1053
1054        # save to config file to preserve over restart
1055        self._client.config.set('Configuration', 'MyInfo', info)
1056        return info

Set JS8Call station information.

info updated via API (if connected) and set in JS8Call configuration file.

Arguments:
  • info (str): Station information
Returns:

str: JS8Call configured station information

def append_pyjs8call_to_station_info(self):
1058    def append_pyjs8call_to_station_info(self):
1059        '''Append pyjs8call info to station info
1060
1061        A string like ', PYJS8CALL V0.0.0' is appended to the current station info.
1062        Example: 'QRPLABS QDX, 40M DIPOLE 33FT, PYJS8CALL V0.2.2'
1063
1064        If a string like ', PYJS8CALL' or ',PYJS8CALL' is found in the current station info, that substring (and everything after it) is dropped before appending the new pyjs8call info.
1065
1066        Returns:
1067            str: JS8Call configured station information
1068        '''
1069        info = self.get_station_info().upper()
1070        
1071        if ', PYJS8CALL' in info:
1072            info = info.split(', PYJS8CALL')[0]
1073        elif ',PYJS8CALL' in info:
1074            info = info.split(',PYJS8CALL')[0]
1075            
1076        info = '{}, PYJS8CALL {}'.format(info, pyjs8call.__version__)
1077        return self.set_station_info(info)

Append pyjs8call info to station info

A string like ', PYJS8CALL V0.0.0' is appended to the current station info. Example: 'QRPLABS QDX, 40M DIPOLE 33FT, PYJS8CALL V0.2.2'

If a string like ', PYJS8CALL' or ',PYJS8CALL' is found in the current station info, that substring (and everything after it) is dropped before appending the new pyjs8call info.

Returns:

str: JS8Call configured station information

def get_bandwidth(self, speed=None):
1079    def get_bandwidth(self, speed=None):
1080        '''Get JS8Call signal bandwidth based on modem speed.
1081
1082        Uses JS8Call configured speed if no speed is given.
1083
1084        | Speed | Bandwidth |
1085        | -------- | -------- |
1086        | slow | 25 Hz |
1087        | normal | 50 Hz |
1088        | fast | 80 Hz |
1089        | turbo | 160 Hz |
1090        | ultra | 250 Hz |
1091
1092        Args:
1093            speed (str): Speed setting, defaults to None
1094
1095        Returns:
1096            int: Bandwidth of JS8Call signal
1097        '''
1098        if speed is None:
1099            speed = self.get_speed()
1100        elif isinstance(speed, int):
1101            speed = self.submode_to_speed(speed)
1102
1103        bandwidths = {'slow':25, 'normal':50, 'fast':80, 'turbo':160, 'ultra':250}
1104
1105        if speed in bandwidths:
1106            return bandwidths[speed]
1107        else:
1108            raise ValueError('Invalid speed \'' + speed + '\'')

Get JS8Call signal bandwidth based on modem speed.

Uses JS8Call configured speed if no speed is given.

Speed Bandwidth
slow 25 Hz
normal 50 Hz
fast 80 Hz
turbo 160 Hz
ultra 250 Hz
Arguments:
  • speed (str): Speed setting, defaults to None
Returns:

int: Bandwidth of JS8Call signal

def get_window_duration(self, speed=None):
1110    def get_window_duration(self, speed=None):
1111        '''Get JS8Call rx/tx window duration based on modem speed.
1112
1113        Uses JS8Call configured speed if no speed is given.
1114
1115        | Speed | Duration |
1116        | -------- | -------- |
1117        | slow | 30 seconds |
1118        | normal | 15 seconds |
1119        | fast | 10 seconds |
1120        | turbo | 6 seconds |
1121        | ultra | 4 seconds |
1122
1123        Args:
1124            speed (str): Speed setting, defaults to None
1125
1126        Returns:
1127            int: Duration of JS8Call rx/tx window in seconds
1128        '''
1129        if speed is None:
1130            speed = self.get_speed()
1131        elif isinstance(speed, int):
1132            speed = self.submode_to_speed(speed)
1133
1134        duration = {'slow': 30, 'normal': 15, 'fast': 10, 'turbo': 6, 'ultra':4}
1135        return duration[speed]

Get JS8Call rx/tx window duration based on modem speed.

Uses JS8Call configured speed if no speed is given.

Speed Duration
slow 30 seconds
normal 15 seconds
fast 10 seconds
turbo 6 seconds
ultra 4 seconds
Arguments:
  • speed (str): Speed setting, defaults to None
Returns:

int: Duration of JS8Call rx/tx window in seconds

def enable_daily_restart(self, restart_time='02:00'):
1137    def enable_daily_restart(self, restart_time='02:00'):
1138        '''Enable daily JS8Call restart at specified time.
1139
1140        The intended use of this function is to allow the removal of the *timer.out* file, which grows in size until it consumes all available disk space. This file cannot be removed while the application is running, but is automatically removed during the pyjs8call restart process.
1141
1142        This function adds a schedule entry. See *pyjs8call.schedulemonitor* for more information.
1143
1144        Args:
1145            restart_time (str): Local restart time in 24-hour format (ex. '23:30'), defaults to '02:00'
1146        '''
1147        # add schedule entry to restart application daily with no settings changes
1148        self._daily_restart_schedule = self._client.schedule.add(restart_time, restart=True)

Enable daily JS8Call restart at specified time.

The intended use of this function is to allow the removal of the timer.out file, which grows in size until it consumes all available disk space. This file cannot be removed while the application is running, but is automatically removed during the pyjs8call restart process.

This function adds a schedule entry. See pyjs8call.schedulemonitor for more information.

Arguments:
  • restart_time (str): Local restart time in 24-hour format (ex. '23:30'), defaults to '02:00'
def disable_daily_restart(self):
1150    def disable_daily_restart(self):
1151        '''Disable daily JS8Call restart.
1152        
1153        This function removes the schedule entry created by *enable_daily_restart*. See *pyjs8call.schedulemonitor* for more information.
1154        '''
1155        if self._daily_restart_schedule is None:
1156            return
1157            
1158        self._client.schedule.remove(self._daily_restart_schedule.dict()['time'], schedule=self._daily_restart_schedule)
1159        self._daily_restart_schedule = None

Disable daily JS8Call restart.

This function removes the schedule entry created by enable_daily_restart. See pyjs8call.schedulemonitor for more information.

def daily_restart_enabled(self):
1161    def daily_restart_enabled(self):
1162        '''Whether daily JS8Call restart is enabled.
1163
1164        Returns:
1165            bool: True if associated schedule entry is set, False otherwise
1166        '''
1167        if self._daily_restart_schedule is not None:
1168            return True
1169        else:
1170            return False

Whether daily JS8Call restart is enabled.

Returns:

bool: True if associated schedule entry is set, False otherwise

def get_daily_restart_time(self):
1172    def get_daily_restart_time(self):
1173        '''Get daily JS8Call restart time.
1174
1175        Returns:
1176            str or None: Local restart time in 24-hour format (ex. '23:30'), or None if not enabled
1177        '''
1178        if self._daily_restart_schedule is None:
1179            return
1180
1181        return self._daily_restart_schedule.start.strftime('%H:%M')

Get daily JS8Call restart time.

Returns:

str or None: Local restart time in 24-hour format (ex. '23:30'), or None if not enabled