From 5e954f238b6e9563dd78d5ded63a9e80404aeb91 Mon Sep 17 00:00:00 2001 From: Daniel Berlin Date: Tue, 11 Aug 2026 11:52:38 -0400 Subject: [PATCH 01/21] feat(aux): add auxiliary relay settings defaults and get_aux_list helper --- common/common.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/common/common.py b/common/common.py index b5421b9a5..fd8b46dc5 100644 --- a/common/common.py +++ b/common/common.py @@ -231,12 +231,22 @@ def default_settings(): }, "outputs": { "auger": 14, + "aux1": None, + "aux2": None, + "aux3": None, + "aux4": None, "dc_fan": 26, "fan": 15, "igniter": 18, "power": 4, "pwm": 13 }, + "aux_labels": { + "aux1": "Aux 1", + "aux2": "Aux 2", + "aux3": "Aux 3", + "aux4": "Aux 4" + }, "system" : { "SPI0" : { "CE0" : 8, # In case a non-standard CE/CS is utilized @@ -2414,6 +2424,32 @@ def get_probe_info(probe_info): return probe_structure +AUX_OUTPUT_NAMES = ['aux1', 'aux2', 'aux3', 'aux4'] + +def get_aux_list(settings): + """ + Build the list of configured auxiliary relays. + + An auxiliary relay is considered configured when its pin in + settings['platform']['outputs'] is not None. Relays that are not + configured are omitted entirely, which is what hides them from every UI. + + :param settings: Settings dictionary + :return: List of dictionaries, i.e. [{'name' : 'aux1', 'label' : 'Work Light'}] + """ + aux_list = [] + outputs = settings['platform'].get('outputs', {}) + labels = settings['platform'].get('aux_labels', {}) + + for name in AUX_OUTPUT_NAMES: + if outputs.get(name, None) is not None: + aux_list.append({ + 'name' : name, + 'label' : labels.get(name, name) + }) + + return aux_list + def read_probe_status(probe_info): """ Creates a structured status report for all probes in the system by combining probe configuration From 687a89a45292771eda5dee2e17ef0342a6b54e61 Mon Sep 17 00:00:00 2001 From: Daniel Berlin Date: Tue, 11 Aug 2026 11:57:27 -0400 Subject: [PATCH 02/21] feat(aux): add auxiliary relay support to the prototype platform --- grillplat/prototype.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/grillplat/prototype.py b/grillplat/prototype.py index 6532ee235..92d021036 100644 --- a/grillplat/prototype.py +++ b/grillplat/prototype.py @@ -49,6 +49,13 @@ def __init__(self, config): self.out_pins['power'] = False self.in_pins['selector'] = False + ''' Auxiliary relays - only those with a real pin assigned exist ''' + self.aux = {} + configured_outputs = config.get('outputs', {}) + for aux_name in ['aux1', 'aux2', 'aux3', 'aux4']: + if configured_outputs.get(aux_name, None) is not None: + self.aux[aux_name] = False + def auger_on(self): self.out_pins['auger'] = True @@ -96,6 +103,39 @@ def power_on(self): def power_off(self): self.out_pins['power'] = False + ''' Auxiliary relays cannot be energized without main power ''' + self._all_aux_off() + + def aux_names(self): + ''' Return the list of configured auxiliary relay names. ''' + return list(self.aux.keys()) + + def aux_on(self, name): + if name not in self.aux: + self.logger.debug(f'aux_on: Auxiliary relay [{name}] is not configured - ignoring.') + return + self.aux[name] = True + + def aux_off(self, name): + if name not in self.aux: + self.logger.debug(f'aux_off: Auxiliary relay [{name}] is not configured - ignoring.') + return + self.aux[name] = False + + def aux_toggle(self, name): + if name not in self.aux: + self.logger.debug(f'aux_toggle: Auxiliary relay [{name}] is not configured - ignoring.') + return + self.aux[name] = not self.aux[name] + + def get_aux_status(self, name): + ''' Return the state of an auxiliary relay, or None if it is not configured. ''' + return self.aux.get(name, None) + + def _all_aux_off(self): + ''' De-energize every auxiliary relay. Called whenever main power drops. ''' + for name in self.aux: + self.aux[name] = False def get_input_status(self): return (self.in_pins['selector']) @@ -109,6 +149,8 @@ def get_output_status(self): self.current['igniter'] = self.out_pins['igniter'] self.current['power'] = self.out_pins['power'] self.current['fan'] = self.out_pins['fan'] + for name in self.aux: + self.current[name] = self.aux[name] if self.dc_fan: self.current['pwm'] = 100 - (self.out_pins['pwm'] * 100) self.current['frequency'] = self.frequency From d8ea1ce77365eb45e81808a5dfabf695faa13ea3 Mon Sep 17 00:00:00 2001 From: Daniel Berlin Date: Tue, 11 Aug 2026 12:03:30 -0400 Subject: [PATCH 03/21] feat(aux): add auxiliary relay support to the raspberry pi platform --- grillplat/raspberry_pi_all.py | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/grillplat/raspberry_pi_all.py b/grillplat/raspberry_pi_all.py index d97240cea..7952ec57c 100644 --- a/grillplat/raspberry_pi_all.py +++ b/grillplat/raspberry_pi_all.py @@ -85,6 +85,13 @@ def __init__(self, config): self.igniter = OutputDevice(self.out_pins['igniter'], active_high=active_high, initial_value=False) self.power = OutputDevice(self.out_pins['power'], active_high=active_high, initial_value=False) + ''' Auxiliary relays - only those with a real pin assigned are constructed ''' + self.aux = {} + for aux_name in ['aux1', 'aux2', 'aux3', 'aux4']: + aux_pin = self.out_pins.get(aux_name, None) + if aux_pin is not None: + self.aux[aux_name] = OutputDevice(aux_pin, active_high=active_high, initial_value=False) + def auger_on(self): self.logger.debug('auger_on: Turning on auger') self.auger.on() @@ -147,6 +154,45 @@ def power_on(self): def power_off(self): self.logger.debug('power_off: Powering off grill platform') self.power.off() + ''' Auxiliary relays cannot be energized without main power ''' + self._all_aux_off() + + + def aux_names(self): + ''' Return the list of configured auxiliary relay names. ''' + return list(self.aux.keys()) + + def aux_on(self, name): + if name not in self.aux: + self.logger.debug(f'aux_on: Auxiliary relay [{name}] is not configured - ignoring.') + return + self.logger.debug(f'aux_on: Turning on auxiliary relay [{name}]') + self.aux[name].on() + + def aux_off(self, name): + if name not in self.aux: + self.logger.debug(f'aux_off: Auxiliary relay [{name}] is not configured - ignoring.') + return + self.logger.debug(f'aux_off: Turning off auxiliary relay [{name}]') + self.aux[name].off() + + def aux_toggle(self, name): + if name not in self.aux: + self.logger.debug(f'aux_toggle: Auxiliary relay [{name}] is not configured - ignoring.') + return + self.logger.debug(f'aux_toggle: Toggling auxiliary relay [{name}]') + self.aux[name].toggle() + + def get_aux_status(self, name): + ''' Return the state of an auxiliary relay, or None if it is not configured. ''' + if name not in self.aux: + return None + return self.aux[name].is_active + + def _all_aux_off(self): + ''' De-energize every auxiliary relay. Called whenever main power drops. ''' + for name in self.aux: + self.aux[name].off() def get_input_status(self): if self.in_pins['selector'] is not None and self.standalone == False: @@ -159,6 +205,8 @@ def get_output_status(self): self.current['igniter'] = self.igniter.is_active self.current['power'] = self.power.is_active self.current['fan'] = self.fan.is_active + for name in self.aux: + self.current[name] = self.aux[name].is_active if self.dc_fan: # self.logger.debug('get_output_status: self.current_fan_speed_percent = ' + str(self.current_fan_speed_percent)) # This is a little verbose, even for debug logging self.current['pwm'] = self.current_fan_speed_percent @@ -208,6 +256,8 @@ def cleanup(self): self.igniter.close() self.auger.close() self.fan.close() + for name in self.aux: + self.aux[name].close() self.pwm.stop() if self.selector is not None: self.selector.close() From bd8ebbe08226d5ce2131eb6800cf8d8ac78d8591 Mon Sep 17 00:00:00 2001 From: Daniel Berlin Date: Tue, 11 Aug 2026 12:12:15 -0400 Subject: [PATCH 04/21] feat(aux): process auxiliary relay requests in the control script --- common/common.py | 2 ++ control.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/common/common.py b/common/common.py index fd8b46dc5..344e183e0 100644 --- a/common/common.py +++ b/common/common.py @@ -685,6 +685,8 @@ def default_control(): 'pwm' : 100 } + control['aux'] = {} # Pending auxiliary relay requests, i.e. {'aux1' : True}. Cleared once applied. + control['smartstart'] = { 'startuptemp' : 0, 'profile_selected' : 0 diff --git a/control.py b/control.py index f31de8766..4d0871618 100755 --- a/control.py +++ b/control.py @@ -148,6 +148,7 @@ DisplayModule = importlib.import_module(f'display.{display_name}') display_config = settings['display']['config'][display_name] display_config['probe_info'] = get_probe_info(settings['probe_settings']['probe_map']['probe_info']) + display_config['aux_info'] = get_aux_list(settings) disp_rotation = display_config.get('rotation', 0) except: @@ -302,6 +303,39 @@ def _process_system_commands(grill_platform): } system_output.push(result) +def _process_aux_requests(grill_platform): + """ + Apply any pending auxiliary relay requests. + + Auxiliary relays are user owned - the controller never drives them - so this is + deliberately NOT gated on Manual mode or settings['safety']['allow_manual_changes'], + and it does not participate in the manual_override timers. + + Requests are transient: they are cleared as soon as they have been applied, so that + Stop mode resetting the control structure cannot strand a stale desired state. + """ + control = read_control() + requests = control.get('aux', {}) + + if not requests: + return + + configured = grill_platform.aux_names() + + for name, state in requests.items(): + if name not in configured: + eventLogger.debug(f'Auxiliary relay [{name}] is not configured - ignoring request.') + continue + if state: + grill_platform.aux_on(name) + eventLogger.debug(f'Auxiliary Relay [{name}] ON') + else: + grill_platform.aux_off(name) + eventLogger.debug(f'Auxiliary Relay [{name}] OFF') + + control['aux'] = {} + write_control(control, direct_write=True, origin='control') + def _work_cycle(mode, grill_platform, probe_complex, display_device, dist_device): """ Work Cycle Function @@ -579,6 +613,7 @@ def _work_cycle(mode, grill_platform, probe_complex, display_device, dist_device control = read_control() _process_system_commands(grill_platform) + _process_aux_requests(grill_platform) # Check if new mode has been requested if control['updated']: @@ -1286,6 +1321,7 @@ def exit_handler(): # Check for system commands _process_system_commands(grill_platform) + _process_aux_requests(grill_platform) # Check if there were updates to any of the settings that were flagged if control['settings_update']: From ccad2ac353c577dbcddc833b410994e9f645c6db Mon Sep 17 00:00:00 2001 From: Daniel Berlin Date: Tue, 11 Aug 2026 12:24:23 -0400 Subject: [PATCH 05/21] feat(aux): add /api/set/aux endpoint for auxiliary relay control --- common/common.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/common/common.py b/common/common.py index 344e183e0..5d4edc0f1 100644 --- a/common/common.py +++ b/common/common.py @@ -3183,6 +3183,32 @@ def process_command(action=None, arglist=[], origin='unknown', direct_write=Fals data['result'] = 'ERROR' data['message'] = f'Before changing manual outputs, system must be put into Manual mode.' + elif arglist[0] == 'aux': + ''' + Auxiliary Relay Control + Note: Auxiliary relays are never driven by the controller, so unlike the manual + commands above, no mode change or safety override is required. + /api/set/aux/{aux1|aux2|aux3|aux4}/{true/false/toggle} + ''' + aux_names = [aux['name'] for aux in get_aux_list(settings)] + + if arglist[1] not in aux_names: + data['result'] = 'ERROR' + data['message'] = f'Auxiliary relay [{arglist[1]}] is not configured. Configured relays: {aux_names}' + else: + if arglist[2] == 'toggle': + status = read_status() + arglist[2] = 'false' if status['outpins'].get(arglist[1], False) else 'true' + + if arglist[2] in ['true', 'false']: + if control.get('aux', None) is None: + control['aux'] = {} + control['aux'][arglist[1]] = True if arglist[2] == 'true' else False + write_control(control, direct_write=direct_write, origin=origin) + else: + data['result'] = 'ERROR' + data['message'] = f'Auxiliary relay command [{arglist[2]}] not recognized. Use true, false or toggle.' + else: data['result'] = 'ERROR' data['message'] = f'Set API Argument: {arglist[0]} not recognized.' From 63706488b4e609b5e05899f5ed61e1507d33f99b Mon Sep 17 00:00:00 2001 From: Daniel Berlin Date: Tue, 11 Aug 2026 12:35:35 -0400 Subject: [PATCH 06/21] feat(aux): add auxiliary relay toggles to the default dashboard --- .../dash/static/default/js/dash_default.js | 32 +++++++++++++++++++ .../default/_macro_dash_default.html | 7 ++++ 2 files changed, 39 insertions(+) diff --git a/blueprints/dash/static/default/js/dash_default.js b/blueprints/dash/static/default/js/dash_default.js index 5e3dd2966..8016b9642 100644 --- a/blueprints/dash/static/default/js/dash_default.js +++ b/blueprints/dash/static/default/js/dash_default.js @@ -19,6 +19,8 @@ var probesReady = false; // Pre-initialized state var last_fan_status = null; var last_auger_status = null; var last_igniter_status = null; +var last_aux_status = {}; +var aux_labels = {}; var last_pmode_status = null; var last_lid_open_status = false; var last_probe_status = {}; @@ -337,6 +339,25 @@ function updateProbeCards() { }; }; + ['aux1', 'aux2', 'aux3', 'aux4'].forEach(function(aux_name) { + if (!(aux_name in current.status.outpins)) { + return; + } + if (current.status.outpins[aux_name] != last_aux_status[aux_name]) { + last_aux_status[aux_name] = current.status.outpins[aux_name]; + var element = document.getElementById(aux_name + '_status'); + if (element === null) { + return; + } + var label = aux_labels[aux_name] || aux_name; + if (last_aux_status[aux_name]) { + element.innerHTML = ''; + } else { + element.innerHTML = ''; + } + } + }); + if (current.status.p_mode != last_pmode_status) { last_pmode_status = current.status.p_mode; if (last_pmode_status == 0) { @@ -1116,6 +1137,17 @@ $(document).ready(function(){ dash_api_set('lid_open/toggle'); }); + ['aux1', 'aux2', 'aux3', 'aux4'].forEach(function(aux_name) { + var element = document.getElementById(aux_name + '_status'); + if (element === null) { + return; + } + aux_labels[aux_name] = element.querySelector('i').getAttribute('title').replace(/ (ON|OFF)$/, ''); + $('#' + aux_name + '_status').click(function() { + dash_api_set('aux/' + aux_name + '/toggle'); + }); + }); + // Initialize Dashboard Data dashGetData(); diff --git a/blueprints/dash/templates/default/_macro_dash_default.html b/blueprints/dash/templates/default/_macro_dash_default.html index 2f8d0595c..f0fab439a 100644 --- a/blueprints/dash/templates/default/_macro_dash_default.html +++ b/blueprints/dash/templates/default/_macro_dash_default.html @@ -389,6 +389,13 @@