diff --git a/.gitignore b/.gitignore index 00e2d79fd..f395a3846 100755 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ generated/ +# Agent scratch / tooling state - never commit +.superpowers/ +.serena/ +.worktrees/ + *.pyc control.err.log diff --git a/blueprints/dash/static/basic/js/dash_basic.js b/blueprints/dash/static/basic/js/dash_basic.js index 87c5dd770..a1787dc53 100644 --- a/blueprints/dash/static/basic/js/dash_basic.js +++ b/blueprints/dash/static/basic/js/dash_basic.js @@ -15,6 +15,8 @@ var notify_status = {}; // store all notify statuses 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 display_mode = null; @@ -58,6 +60,29 @@ function initNotifyIndicators(probes) { }; // Update temperatures on probe status cards +// Auxiliary relay icons render as either (before Font Awesome's SVG +// conversion runs) or (after conversion, which replaces the +// element and moves the tooltip text into an inner node). These +// helpers update whichever form is currently in the DOM using only +// property/textContent assignment (never innerHTML), so a user-supplied +// label can never be parsed as markup. +function getAuxIcon(container) { + return container.querySelector('svg, i'); +} + +function setAuxIconTooltip(icon, text) { + if (icon.tagName.toLowerCase() === 'svg') { + var titleEl = icon.querySelector('title'); + if (titleEl === null) { + titleEl = document.createElementNS('http://www.w3.org/2000/svg', 'title'); + icon.insertBefore(titleEl, icon.firstChild); + } + titleEl.textContent = text; + } else { + icon.title = text; + } +} + function updateProbeCards() { req = $.ajax({ url : '/api/current', @@ -231,6 +256,31 @@ 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 icon = getAuxIcon(element); + if (icon === null) { + return; + } + var label = aux_labels[aux_name] || aux_name; + if (last_aux_status[aux_name]) { + setAuxIconTooltip(icon, label + ' ON'); + icon.style.color = 'rgb(0, 190, 0)'; + } else { + setAuxIconTooltip(icon, label + ' OFF'); + icon.style.color = 'rgb(150, 150, 150)'; + } + } + }); + if (current.status.p_mode != last_pmode_status) { last_pmode_status = current.status.p_mode; if (last_pmode_status == 0) { @@ -785,8 +835,20 @@ function dashClearErrorCounter() { $(document).ready(function(){ // Setup Listeners $('#reloadPage').click(function() { - // Reload page when server side changes detected. - location.reload(); + // Reload page when server side changes detected. + location.reload(); + }); + + ['aux1', 'aux2', 'aux3', 'aux4'].forEach(function(aux_name) { + var element = document.getElementById(aux_name + '_status'); + if (element === null) { + return; + } + var icon = getAuxIcon(element); + aux_labels[aux_name] = (icon !== null && icon.dataset.label) ? icon.dataset.label : aux_name; + $('#' + aux_name + '_status').click(function() { + cp_api_set('aux/' + aux_name + '/toggle'); + }); }); // Initialize Dashboard Data diff --git a/blueprints/dash/static/default/js/dash_default.js b/blueprints/dash/static/default/js/dash_default.js index 5e3dd2966..aa8642813 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 = {}; @@ -146,6 +148,29 @@ function initNotifyIndicators() { }; // Update temperatures on probe status cards +// Auxiliary relay icons render as either <i> (before Font Awesome's SVG +// conversion runs) or <svg> (after conversion, which replaces the <i> +// element and moves the tooltip text into an inner <title> node). These +// helpers update whichever form is currently in the DOM using only +// property/textContent assignment (never innerHTML), so a user-supplied +// label can never be parsed as markup. +function getAuxIcon(container) { + return container.querySelector('svg, i'); +} + +function setAuxIconTooltip(icon, text) { + if (icon.tagName.toLowerCase() === 'svg') { + var titleEl = icon.querySelector('title'); + if (titleEl === null) { + titleEl = document.createElementNS('http://www.w3.org/2000/svg', 'title'); + icon.insertBefore(titleEl, icon.firstChild); + } + titleEl.textContent = text; + } else { + icon.title = text; + } +} + function updateProbeCards() { req = $.ajax({ url : '/api/current', @@ -337,6 +362,31 @@ 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 icon = getAuxIcon(element); + if (icon === null) { + return; + } + var label = aux_labels[aux_name] || aux_name; + if (last_aux_status[aux_name]) { + setAuxIconTooltip(icon, label + ' ON'); + icon.style.color = 'rgb(0, 190, 0)'; + } else { + setAuxIconTooltip(icon, label + ' OFF'); + icon.style.color = 'rgb(150, 150, 150)'; + } + } + }); + if (current.status.p_mode != last_pmode_status) { last_pmode_status = current.status.p_mode; if (last_pmode_status == 0) { @@ -1116,6 +1166,18 @@ $(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; + } + var icon = getAuxIcon(element); + aux_labels[aux_name] = (icon !== null && icon.dataset.label) ? icon.dataset.label : aux_name; + $('#' + aux_name + '_status').click(function() { + dash_api_set('aux/' + aux_name + '/toggle'); + }); + }); + // Initialize Dashboard Data dashGetData(); diff --git a/blueprints/dash/templates/basic/_macro_dash_basic.html b/blueprints/dash/templates/basic/_macro_dash_basic.html index 10b3f301c..6df1fb0c4 100644 --- a/blueprints/dash/templates/basic/_macro_dash_basic.html +++ b/blueprints/dash/templates/basic/_macro_dash_basic.html @@ -313,7 +313,7 @@ <h5>Current: <span id="hopperPellets"></span></h5> </div> {% endmacro %} -{% macro render_status_card() %} +{% macro render_status_card(settings) %} <div class="card shadow"> <div class="card-header bg-primary text-white text-center"> <i class="fas fa-info-circle"></i>  Status: <span id="mode_status" style="color:yellow"><b>Stop</b></span> @@ -332,6 +332,13 @@ <h5>Current: <span id="hopperPellets"></span></h5> <td> <span id="igniter_status"><i class="fas fa-fire fa-2x" data-toggle="tooltip" data-placement="top" title="Igniter ON" style="color:rgb(150, 150, 150)"></i></span>  </td> + {% for aux_name in ['aux1', 'aux2', 'aux3', 'aux4'] %} + {% if settings['platform']['outputs'].get(aux_name) is not none %} + <td> + <span id="{{ aux_name }}_status"><i class="fas fa-plug fa-2x" data-toggle="tooltip" data-placement="top" title="{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }} OFF" data-label="{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }}" style="color:rgb(150, 150, 150)"></i></span>  + </td> + {% endif %} + {% endfor %} <td> <span id="pmode_status" class="fa-stack" style="vertical-align: top;"> <i class="far fa-square fa-stack-2x" style="color:rgb(150, 150, 150)" data-toggle="tooltip" data-placement="top" title="P-Mode"></i> @@ -343,6 +350,11 @@ <h5>Current: <span id="hopperPellets"></span></h5> <td><span class="badge badge-secondary">Fan</span></td> <td><span class="badge badge-secondary">Auger</span></td> <td><span class="badge badge-secondary">Igniter</span></td> + {% for aux_name in ['aux1', 'aux2', 'aux3', 'aux4'] %} + {% if settings['platform']['outputs'].get(aux_name) is not none %} + <td><span class="badge badge-secondary">{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }}</span></td> + {% endif %} + {% endfor %} <td><span class="badge badge-secondary">P-Mode</span></td> </tr> </tbody> diff --git a/blueprints/dash/templates/basic/dash_basic.html b/blueprints/dash/templates/basic/dash_basic.html index 597683d0a..e862c9276 100644 --- a/blueprints/dash/templates/basic/dash_basic.html +++ b/blueprints/dash/templates/basic/dash_basic.html @@ -44,7 +44,7 @@ style="display:none" {% endif %}> <br> - {{ render_status_card() }} + {{ render_status_card(settings) }} <br> </div> </div> <!-- End of Row --> diff --git a/blueprints/dash/templates/default/_macro_dash_default.html b/blueprints/dash/templates/default/_macro_dash_default.html index 2f8d0595c..b8466349b 100644 --- a/blueprints/dash/templates/default/_macro_dash_default.html +++ b/blueprints/dash/templates/default/_macro_dash_default.html @@ -389,6 +389,13 @@ <h4 class="modal-title"><i class="fas fa-temperature-high"></i>  {{ probe_d <td> <span id="igniter_status"><i class="fas fa-fire fa-2x" data-toggle="tooltip" data-placement="top" title="Igniter ON" style="color:rgb(150, 150, 150)"></i></span>  </td> + {% for aux_name in ['aux1', 'aux2', 'aux3', 'aux4'] %} + {% if settings['platform']['outputs'].get(aux_name) is not none %} + <td> + <span id="{{ aux_name }}_status"><i class="fas fa-plug fa-2x" data-toggle="tooltip" data-placement="top" title="{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }} OFF" data-label="{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }}" style="color:rgb(150, 150, 150)"></i></span>  + </td> + {% endif %} + {% endfor %} <td id="pmode_icon"> <span id="pmode_status" class="fa-stack" style="vertical-align: top;"> <i class="far fa-square fa-stack-2x" style="color:rgb(150, 150, 150)" data-toggle="tooltip" data-placement="top" title="P-Mode"></i> @@ -405,6 +412,11 @@ <h4 class="modal-title"><i class="fas fa-temperature-high"></i>  {{ probe_d <td><span class="badge badge-secondary">Fan</span></td> <td><span class="badge badge-secondary">Auger</span></td> <td><span class="badge badge-secondary">Igniter</span></td> + {% for aux_name in ['aux1', 'aux2', 'aux3', 'aux4'] %} + {% if settings['platform']['outputs'].get(aux_name) is not none %} + <td><span class="badge badge-secondary">{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }}</span></td> + {% endif %} + {% endfor %} <td id="pmode_badge"> <span class="badge badge-secondary">P-Mode</span> </td> diff --git a/blueprints/mobile/socket_io.py b/blueprints/mobile/socket_io.py index d3e4ee6f3..f0a2b6c03 100644 --- a/blueprints/mobile/socket_io.py +++ b/blueprints/mobile/socket_io.py @@ -214,7 +214,8 @@ def _get_dash_data(settings, pelletdb): 'outputs': { 'fan': status['outpins']['fan'], 'auger': status['outpins']['auger'], - 'igniter': status['outpins']['igniter'] + 'igniter': status['outpins']['igniter'], + 'aux': {aux['name']: status['outpins'].get(aux['name'], False) for aux in get_aux_list(settings)} }, 'recipeStatus': { 'recipeMode': status['recipe'], diff --git a/blueprints/settings/routes.py b/blueprints/settings/routes.py index f901bab9c..39615f828 100644 --- a/blueprints/settings/routes.py +++ b/blueprints/settings/routes.py @@ -1,6 +1,6 @@ from flask import render_template, request, render_template_string, jsonify -from common.common import read_settings, read_control, write_settings, write_control, read_generic_json, generate_uuid, convert_settings_units +from common.common import read_settings, read_control, write_settings, write_control, read_generic_json, generate_uuid, convert_settings_units, get_aux_list from common.app import is_not_blank, is_checked from . import settings_bp @@ -563,6 +563,24 @@ def settings_page(action=None): write_settings(settings) + if request.method == 'POST' and action == 'aux': + response = request.form + + for aux in get_aux_list(settings): + field = f'aux_label_{aux["name"]}' + if field in response: + label = response[field].strip()[:25] + if label != '': + settings['platform']['aux_labels'][aux['name']] = label + + event['type'] = 'updated' + event['text'] = 'Successfully updated auxiliary relay settings.' + + control['settings_update'] = True + + write_settings(settings) + write_control(control, origin='app') + if request.method == 'POST' and action == 'pellets': response = request.form diff --git a/blueprints/settings/templates/settings/index.html b/blueprints/settings/templates/settings/index.html index 64283e900..b31f6c728 100644 --- a/blueprints/settings/templates/settings/index.html +++ b/blueprints/settings/templates/settings/index.html @@ -78,6 +78,7 @@ <h6 class="nav-category-header text-muted font-weight-bold mt-2 mb-1 px-3">Front <!-- Operations --> <h6 class="nav-category-header text-muted font-weight-bold mt-3 mb-1 px-3">Operations</h6> <a class="nav-link" id="v-pills-safety-tab" data-toggle="pill" href="#v-pills-safety" role="tab" aria-controls="v-pills-safety" aria-selected="false">Safety</a> + <a class="nav-link" id="v-pills-aux-tab" data-toggle="pill" href="#v-pills-aux" role="tab" aria-controls="v-pills-aux" aria-selected="false">Auxiliary Relays</a> <a class="nav-link" id="v-pills-startup-tab" data-toggle="pill" href="#v-pills-startup" role="tab" aria-controls="v-pills-startup" aria-selected="false">Startup & Shutdown</a> <a class="nav-link" id="v-pills-work-mode-tab" data-toggle="pill" href="#v-pills-work-mode" role="tab" aria-controls="v-pills-work-mode" aria-selected="false">Controller Settings</a> {% if settings['platform']['dc_fan'] %} @@ -1313,6 +1314,42 @@ <h5> </div><!-- End of Card --> <br><br><br> </div><!-- End of Tab --> + <!-- ============================ Auxiliary Relay Settings ========================== --> + <div class="tab-pane fade" id="v-pills-aux" role="tabpanel" aria-labelledby="v-pills-aux-tab"> + <form name="auxsettings" action="/settings/aux#v-pills-aux" method="POST"> + <div class="card shadow"> + <div class="card-header"> + <h5><i class="fas fa-plug"></i>  Auxiliary Relays</h5> + </div> + <div class="card-body"> + {% set aux_list = [] %} + {% for aux_name in ['aux1', 'aux2', 'aux3', 'aux4'] %} + {% if settings['platform']['outputs'].get(aux_name) is not none %} + {% set _ = aux_list.append(aux_name) %} + <div class="input-group mb-3"> + <div class="input-group-prepend"> + <span class="input-group-text" data-toggle="tooltip" title="Name shown on the dashboard and display for this relay."> + <i class="fas fa-plug"></i>  {{ aux_name|upper }} (GPIO{{ settings['platform']['outputs'][aux_name] }}) + </span> + </div> + <input id="aux_label_{{ aux_name }}" type="text" maxlength="25" class="form-control" value="{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }}" name="aux_label_{{ aux_name }}"> + </div> + {% endif %} + {% endfor %} + {% if aux_list|length == 0 %} + <span class="badge badge-warning">NOTE:</span> + <i class="small"> No auxiliary relays are configured. Assign a GPIO pin to one or more auxiliary relays in the configuration wizard to enable them.</i> + {% else %} + <span class="badge badge-warning">NOTE:</span> + <i class="small"> Auxiliary relays are never controlled by PiFire during a cook. They switch off whenever the main power relay switches off. Names are limited to 25 characters so they fit on the touch display.</i> + {% endif %} + </div> + <div class="card-footer bg-light"> + <button type="submit" class="btn btn-outline-primary" {% if aux_list|length == 0 %}disabled{% endif %}>Save</button> + </div> + </div> + </form> + </div> <!-- ============================ Pellet Settings ========================== --> <div class="tab-pane fade" id="v-pills-pellets" role="tabpanel" aria-labelledby="v-pills-pellets-tab"> <div class="card shadow"> diff --git a/blueprints/wizard/routes.py b/blueprints/wizard/routes.py index 1f20226e4..ee22c8a0c 100644 --- a/blueprints/wizard/routes.py +++ b/blueprints/wizard/routes.py @@ -33,15 +33,21 @@ def wizard_page(action=None): if action=='finish': if control['mode'] == 'Stop': wizardInstallInfo = prepare_wizard_data(r) - store_wizard_install_info(wizardInstallInfo) - set_wizard_install_status(0, 'Starting Install...', '') - os.system(f'{python_exec} wizard.py &') # Kickoff Installation - return render_template( - 'wizard/wizard-finish.html', - page_theme=settings['globals'].get('page_theme', 'light'), - grill_name=settings['globals'].get('grill_name', ''), - wizardData=wizardData - ) + profile_selected = wizardInstallInfo['modules']['grillplatform']['profile_selected'][0] + settings_dependencies = wizardData['modules']['grillplatform'][profile_selected]['settings_dependencies'] + errors += find_platform_pin_collisions(settings_dependencies, wizardInstallInfo['modules']['grillplatform']['settings']) + if not errors: + store_wizard_install_info(wizardInstallInfo) + set_wizard_install_status(0, 'Starting Install...', '') + os.system(f'{python_exec} wizard.py &') # Kickoff Installation + return render_template( + 'wizard/wizard-finish.html', + page_theme=settings['globals'].get('page_theme', 'light'), + grill_name=settings['globals'].get('grill_name', ''), + wizardData=wizardData + ) + # else: fall through to the shared render below so the user sees the errors. + # Nothing is stored and no install is launched. if action=='modulecard': module = r['module'] diff --git a/blueprints/wizard/wizard.py b/blueprints/wizard/wizard.py index 5a3339856..070cee493 100644 --- a/blueprints/wizard/wizard.py +++ b/blueprints/wizard/wizard.py @@ -170,4 +170,83 @@ def prepare_wizard_data(form_data): if config.startswith(module_ + 'config_'): wizardInstallInfo['modules'][module]['config'][config.replace(module_ + 'config_', '')] = value - return(wizardInstallInfo) \ No newline at end of file + return(wizardInstallInfo) + +def find_platform_pin_collisions(settings_dependencies, submitted_settings): + """ + Detects GPIO pins assigned to more than one grill platform function in a submitted + wizard configuration. + + Fields are checked when either of two things claims the pin before/independently of + control.py ever running: + 1. grillplat/raspberry_pi_all.py's GrillPlatform.__init__ turns the field into a pin + object - mirrored exactly, so fields left inert by other settings (e.g. the AC fan + pin on a DC-fan build, the selector pin on a standalone build) are never flagged: + - outputs.auger, outputs.igniter, outputs.power are always claimed + - outputs.fan is claimed only when dc_fan is False + - outputs.dc_fan and outputs.pwm are claimed only when dc_fan is True + - inputs.selector is claimed only when standalone is False + - outputs.aux1..aux4 are claimed only when not 'None' + 2. board-config.py writes the pin into the boot config as a device-tree overlay, so the + KERNEL claims it at boot, before GrillPlatform.__init__ ever runs - if an aux relay + (or the selector) is assigned the same pin, GrillPlatform.__init__ loses that race + and raises, which lands PiFire on the simulated prototype platform exactly as if the + collision were with another GrillPlatform pin: + - system.1WIRE is always claimed when set (dtoverlay=w1-gpio, board-config.py's + set_onewire_gpio()) + - system.SPI0.CE0 and system.SPI0.CE1 are always claimed when set (the SPI0 + overlay enabled by board-config.py's enable_spi() fixes these pins) + - inputs.shutdown is always claimed when set (dtoverlay=gpio-shutdown, + board-config.py's enable_gpio_shutdown()) + + Deliberately NOT checked, despite also being pin-valued fields in the manifest: + device_display_*, device_distance_*, device_input_*. None of these are touched by + GrillPlatform.__init__ or by a board-config.py boot overlay verified for this guard, and + device_distance_trig in particular is hardcoded to the same pin as output_auger on the + pcb_4.x.x default, so treating it as claimed would refuse that board's own stock + configuration. + + Parameters: + - settings_dependencies (dict): wizardData['modules']['grillplatform'][<profile>]['settings_dependencies']. + Used only to look up each field's friendly_name for the error message. + - submitted_settings (dict): wizardInstallInfo['modules']['grillplatform']['settings'], the raw + setting-name -> submitted-string-value mapping produced by prepare_wizard_data(). + + Returns: + - list of str: one human-readable error message per colliding pair of fields, naming both + fields and the shared pin. Empty if there are no collisions. + """ + dc_fan = submitted_settings.get('dc_fan') == 'True' + standalone = submitted_settings.get('standalone') == 'True' + + pin_fields = [ + 'output_auger', 'output_igniter', 'output_power', + 'output_aux1', 'output_aux2', 'output_aux3', 'output_aux4', + 'system_1wire', 'system_spi0_ce0', 'system_spi0_ce1', + 'input_shutdown', + ] + if dc_fan: + pin_fields += ['output_dc_fan', 'output_pwm'] + else: + pin_fields.append('output_fan') + if not standalone: + pin_fields.append('input_selector') + + claimed_by = {} # pin value (str) -> (field name, friendly name) + errors = [] + for field in pin_fields: + value = submitted_settings.get(field) + if value is None or value == 'None': + continue # unset/'Not Installed' pins are never claimed + + friendly_name = settings_dependencies.get(field, {}).get('friendly_name', field) + if value in claimed_by: + _, other_friendly_name = claimed_by[value] + errors.append( + f'{other_friendly_name} and {friendly_name} are both set to GPIO{value}. ' + f'Each pin may only be assigned to one function.' + ) + else: + claimed_by[value] = (field, friendly_name) + + return errors diff --git a/common/common.py b/common/common.py index b5421b9a5..5d4edc0f1 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 @@ -675,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 @@ -2414,6 +2426,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 @@ -3145,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.' diff --git a/control.py b/control.py index f31de8766..6e6c2b108 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,42 @@ def _process_system_commands(grill_platform): } system_output.push(result) +def _process_aux_requests(grill_platform, control): + """ + 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. + + Takes the caller's in-memory `control` dict (rather than re-reading it) and mutates it + in place, so that any later stale write of that same object by the caller carries the + cleared aux state instead of resurrecting the just-applied request. + """ + 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 +616,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, control) # Check if new mode has been requested if control['updated']: @@ -1286,6 +1324,7 @@ def exit_handler(): # Check for system commands _process_system_commands(grill_platform) + _process_aux_requests(grill_platform, control) # Check if there were updates to any of the settings that were flagged if control['settings_update']: diff --git a/display/base_flex.py b/display/base_flex.py index c6095c2d2..10f350ba2 100644 --- a/display/base_flex.py +++ b/display/base_flex.py @@ -24,6 +24,10 @@ from PIL import Image from common import read_control, write_control, is_real_hardware, read_generic_json, read_settings, write_settings, read_status, read_current +''' Colour used to show an auxiliary relay is energised on the aux menu, matching the + web dashboards' colour for an "on" aux plug icon. ''' +AUX_RELAY_ON_COLOR = (0, 190, 0, 255) + ''' ================================================================================== Display base class definition @@ -42,6 +46,9 @@ def __init__(self, dev_pins, buttonslevel='HIGH', rotation=0, units='F', config= self.last_in_data = {} self.status_data = None self.last_status_data = {} + # Snapshot of {aux_name: bool} the currently-rendered aux menu's button colours were + # built from; used by _fetch_data() to detect a live state change and force a rebuild. + self.aux_menu_outpins = None self.input_enabled = False self.input_origin = None @@ -109,6 +116,8 @@ def _init_framework(self): self._fixup_display_data() + self._configure_aux_menu() + self._init_assets() def _fixup_display_data(self): @@ -142,6 +151,74 @@ def _fixup_display_data(self): #print(f'Fixed Up: \n{self.display_data[self.display_profile]["menus"]}') + def _configure_aux_menu(self): + ''' + Populate the auxiliary relay menu from the configured relays, and remove the + menu entry point entirely when no relays are configured. + + Mirrors the food probe gauge treatment in _configure_dash: objects that have + no backing configuration are dropped rather than rendered empty. + ''' + menus = self.display_data[self.display_profile].get('menus', {}) + if 'aux' not in menus: + return + + aux_info = self.config.get('aux_info', []) + + if not aux_info: + ''' No auxiliary relays configured - remove the entry point from every main menu ''' + for menu_name, menu in menus.items(): + while 'menu_aux' in menu.get('button_list', []): + index = menu['button_list'].index('menu_aux') + menu['button_list'].pop(index) + menu['button_text'].pop(index) + return + + button_list = ['menu_close'] + button_text = ['Close Menu'] + for aux in aux_info: + button_list.append(f'cmd_aux_toggle_{aux["name"]}') + button_text.append(aux['label']) + button_list.append('menu_close') + button_text.append('Close') + + menus['aux']['button_list'] = button_list + menus['aux']['button_text'] = button_text + + def _aux_relay_states(self): + ''' + Return {aux_name: bool} for every configured auxiliary relay, read from the live + status data. An unconfigured relay never appears in self.config['aux_info'], and a + configured relay that is momentarily missing from status_data['outpins'] is treated + as off. + ''' + outpins = {} + if self.status_data is not None: + outpins = self.status_data.get('outpins', {}) + + return {aux['name']: outpins.get(aux['name'], False) for aux in self.config.get('aux_info', [])} + + def _calc_aux_button_colors(self): + ''' + Build the button_colors list for the aux menu, aligned index-for-index with the + button_list that _configure_aux_menu() produced: green for a 'cmd_aux_toggle_<name>' + button whose relay is on, None (the menu's default colour) for an off relay and for + both menu_close bookend entries. + ''' + button_list = self.display_data[self.display_profile]['menus']['aux'].get('button_list', []) + aux_states = self._aux_relay_states() + + button_colors = [] + for button in button_list: + if button.startswith('cmd_aux_toggle_'): + aux_name = button[len('cmd_aux_toggle_'):] + button_colors.append(AUX_RELAY_ON_COLOR if aux_states.get(aux_name, False) else None) + else: + button_colors.append(None) + + return button_colors + + def _init_display_device(self): ''' Inheriting classes will override this function to init the display device and start the display thread. @@ -295,6 +372,11 @@ def _build_objects(self, background=None): section_data = self.display_data[self.display_profile][self.display_active] elif 'menu_' in self.display_active: section_data = [self.display_data[self.display_profile]['menus'][self.display_active.replace('menu_', '')]] + if self.display_active == 'menu_aux': + # Colours are per-render (live relay state) and must not be baked into the + # static button_list/button_text that _configure_aux_menu() built at init. + section_data[0]['button_colors'] = self._calc_aux_button_colors() + self.aux_menu_outpins = self._aux_relay_states() elif 'input_' in self.display_active: section_data = [self.display_data[self.display_profile]['input'][self.display_active.replace('input_', '')]] section_data[0]['data']['origin'] = self.input_origin @@ -607,6 +689,14 @@ def _fetch_data(self): self.units = self.status_data['units'] + ''' If the aux menu is open and a configured relay's state has changed since the menu + was last built, force a rebuild so the button colours stay current. Menus are only + rebuilt on self.display_init (see the display loops), so there is no per-frame + update path for them like there is for the dash. ''' + if self.display_active == 'menu_aux' and self.aux_menu_outpins is not None: + if self._aux_relay_states() != self.aux_menu_outpins: + self.display_init = True + ''' Wake the display to the dash if it's currently off ''' if self.display_active == None and self.status_data['mode'] != 'Stop': self.display_active = 'dash' @@ -852,6 +942,17 @@ def _command_handler(self): except: self.eventLogger.debug('Fan Toggle Failed.') + if 'aux_toggle_' in self.command: + aux_name = self.command.split('aux_toggle_')[-1] + try: + requests.get(f'http://127.0.0.1:5000/api/set/aux/{aux_name}/toggle') + except: + self.eventLogger.debug(f'Auxiliary Relay [{aux_name}] Toggle Failed.') + # Stay on the aux menu (rather than bouncing to dash) so the user sees the colour + # change; the toggle is applied asynchronously by the control script, so the actual + # colour update happens once _fetch_data() observes the new state (see item 3). + self.display_init = True + if 'lid_open' in self.command: try: requests.get('http://127.0.0.1:5000/api/set/lid_open/toggle') diff --git a/display/dsi_800x480t.json b/display/dsi_800x480t.json index 4b8ed9749..52c527768 100644 --- a/display/dsi_800x480t.json +++ b/display/dsi_800x480t.json @@ -371,8 +371,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Close"], + "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -387,8 +387,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_lid_open", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Lid Open", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Close"], + "button_list" : ["menu_close", "cmd_lid_open", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Lid Open", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -403,8 +403,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Stop", "System", "Close"], + "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Stop", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -419,8 +419,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Close"], + "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -584,6 +584,22 @@ "button_text" : ["Close Menu", "OK"], "button_value" : [], "touch_areas" : [] + }, + "aux" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "aux_menu", + "title_text" : "Auxiliary", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close"], + "button_text" : ["Close Menu"], + "button_value" : [], + "touch_areas" : [] } }, "input" : { @@ -971,8 +987,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Close"], + "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -987,8 +1003,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_lid_open", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Lid Open", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Close"], + "button_list" : ["menu_close", "cmd_lid_open", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Lid Open", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -1003,8 +1019,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Stop", "System", "Close"], + "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Stop", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -1019,8 +1035,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Close"], + "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -1184,6 +1200,22 @@ "button_text" : ["Close Menu", "OK"], "button_value" : [], "touch_areas" : [] + }, + "aux" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "aux_menu", + "title_text" : "Auxiliary", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close"], + "button_text" : ["Close Menu"], + "button_value" : [], + "touch_areas" : [] } }, "input" : { diff --git a/display/flexobject.py b/display/flexobject.py index 4c11a2758..d717cf467 100644 --- a/display/flexobject.py +++ b/display/flexobject.py @@ -727,6 +727,10 @@ def _draw_object(self): draw = ImageDraw.Draw(canvas) # Create drawing object fg_color = self.objectData['color'] bg_color = (0,0,0,255) + # Optional parallel list of per-button colour overrides (see button_list). An entry of + # None, or the list being absent/shorter than button_list, falls back to fg_color/white + # outline, i.e. today's single-colour rendering. + button_colors = self.objectData.get('button_colors') # Clear any touch areas that might have been defined before self.objectData['touch_areas'] = [] @@ -795,11 +799,21 @@ def _draw_object(self): rect_size = (button_width, button_height) rect_coords = (rect_position[0], rect_position[1], rect_position[0] + rect_size[0], rect_position[1] + rect_size[1]) + + # Per-button colour override, if provided; otherwise today's single menu colour + # with a plain white outline. + if button_colors is not None and index < len(button_colors) and button_colors[index] is not None: + button_color = button_colors[index] + button_outline = button_color + else: + button_color = fg_color + button_outline = (255,255,255,255) + if selected == index: # Reverse colors if selected - draw.rounded_rectangle(rect_coords, radius=8, outline=(255,255,255,255), fill=fg_color) + draw.rounded_rectangle(rect_coords, radius=8, outline=button_outline, fill=button_color) else: - draw.rounded_rectangle(rect_coords, radius=8, outline=(255,255,255,255), fill=bg_color) + draw.rounded_rectangle(rect_coords, radius=8, outline=button_outline, fill=bg_color) # Put button text inside rectangle if len(self.objectData['button_text'][index]) > 25: @@ -810,7 +824,7 @@ def _draw_object(self): # Reverse colors if selected label = self._draw_text(label_displayed, self.objectData['font'], 35, bg_color) else: - label = self._draw_text(label_displayed, self.objectData['font'], 35, fg_color) + label = self._draw_text(label_displayed, self.objectData['font'], 35, button_color) label_x = rect_position[0] + (rect_size[0] // 2) - (label.width // 2) label_y = rect_position[1] + (rect_size[1] // 2) - (label.height // 2) label_position = (label_x, label_y) diff --git a/display/protoflex_800x480.json b/display/protoflex_800x480.json index 894385cfe..4415efefa 100644 --- a/display/protoflex_800x480.json +++ b/display/protoflex_800x480.json @@ -371,8 +371,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Close"], + "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -387,8 +387,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_lid_open", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Lid Open", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Close"], + "button_list" : ["menu_close", "cmd_lid_open", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Lid Open", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -403,8 +403,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Stop", "System", "Close"], + "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Stop", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -419,8 +419,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Close"], + "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -584,6 +584,22 @@ "button_text" : ["Close Menu", "OK"], "button_value" : [], "touch_areas" : [] + }, + "aux" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "aux_menu", + "title_text" : "Auxiliary", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close"], + "button_text" : ["Close Menu"], + "button_value" : [], + "touch_areas" : [] } }, "input" : { @@ -971,8 +987,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Close"], + "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -987,8 +1003,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_lid_open", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Lid Open", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Close"], + "button_list" : ["menu_close", "cmd_lid_open", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Lid Open", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -1003,8 +1019,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Stop", "System", "Close"], + "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Stop", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -1019,8 +1035,8 @@ "title_text" : "Main Menu", "color" : [255,255,255,255], "data" : {}, - "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_close"], - "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Close"], + "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_aux", "menu_close"], + "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Auxiliary", "Close"], "button_value" : [], "touch_areas" : [] }, @@ -1184,6 +1200,22 @@ "button_text" : ["Close Menu", "OK"], "button_value" : [], "touch_areas" : [] + }, + "aux" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "aux_menu", + "title_text" : "Auxiliary", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close"], + "button_text" : ["Close Menu"], + "button_value" : [], + "touch_areas" : [] } }, "input" : { 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 diff --git a/grillplat/raspberry_pi_all.py b/grillplat/raspberry_pi_all.py index d97240cea..a1731f7a0 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,9 @@ def cleanup(self): self.igniter.close() self.auger.close() self.fan.close() + self._all_aux_off() + for name in self.aux: + self.aux[name].close() self.pwm.stop() if self.selector is not None: self.selector.close() diff --git a/notify/mqtt_handler.py b/notify/mqtt_handler.py index aafaaaadd..db5ff2fe9 100644 --- a/notify/mqtt_handler.py +++ b/notify/mqtt_handler.py @@ -19,7 +19,7 @@ import logging import time from socket import getfqdn -from common import create_logger +from common import create_logger, get_aux_list import psutil #from common import write_control @@ -61,6 +61,10 @@ def __init__(self, settings) -> None: # These list the devices aka attributes that will be published to mqtt self.DEVICE_SENSORS = ['auger','igniter','power','fan','notify_event'] + ''' Auxiliary relays are published alongside the fixed outputs when configured ''' + aux_list = get_aux_list(settings) + self._aux_names = set(aux['name'] for aux in aux_list) + self.DEVICE_SENSORS += [aux['name'] for aux in aux_list] self.CONTROL_SENSORS = ['mode','next_mode','s_plus','pwm_control','duty_cycle','status','primary_setpoint'] self.PID_CONFIG_SENSORS = ['PB','Td','Ti','center'] self.PID_CYCLE_TIME_SENSORS = ['HoldCycleTime','LidOpenPauseTime','LidOpenThreshold','PMode','SmokeCycleTime','u_max','u_min'] @@ -299,7 +303,7 @@ def _create_autodiscover(self, context, data): discovery['payload_on'] = True discovery['payload_off'] = False - if device not in {'auger','igniter','power','fan'}: + if device not in set(['auger','igniter','power','fan']) | self._aux_names: discovery['enabled_by_default'] = False elif datatype == str: diff --git a/wizard/wizard_manifest.json b/wizard/wizard_manifest.json index 78559f238..af519a4d1 100644 --- a/wizard/wizard_manifest.json +++ b/wizard/wizard_manifest.json @@ -73,6 +73,30 @@ "options" : { "4" : "GPIO4", "2" : "GPIO2", "3" : "GPIO3", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, "settings" : ["platform", "outputs", "power"] }, + "output_aux1" : { + "friendly_name" : "Auxiliary Relay 1 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 1, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux1"] + }, + "output_aux2" : { + "friendly_name" : "Auxiliary Relay 2 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 2, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux2"] + }, + "output_aux3" : { + "friendly_name" : "Auxiliary Relay 3 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 3, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux3"] + }, + "output_aux4" : { + "friendly_name" : "Auxiliary Relay 4 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 4, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux4"] + }, "output_pwm" : { "friendly_name" : "DC Fan PWM GPIO Pin", "description" : "Select the GPIO used for the DC Fan PWM. Note that this should be a PWM capable GPIO.", @@ -230,6 +254,30 @@ "settings" : ["platform", "outputs", "power"], "hidden" : true }, + "output_aux1" : { + "friendly_name" : "Auxiliary Relay 1 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 1, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux1"] + }, + "output_aux2" : { + "friendly_name" : "Auxiliary Relay 2 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 2, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux2"] + }, + "output_aux3" : { + "friendly_name" : "Auxiliary Relay 3 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 3, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux3"] + }, + "output_aux4" : { + "friendly_name" : "Auxiliary Relay 4 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 4, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux4"] + }, "output_pwm" : { "friendly_name" : "DC Fan PWM GPIO Pin", "description" : "Select the GPIO used for the DC Fan PWM. Note that this should be a PWM capable GPIO.", @@ -408,6 +456,30 @@ "settings" : ["platform", "outputs", "power"], "hidden" : true }, + "output_aux1" : { + "friendly_name" : "Auxiliary Relay 1 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 1, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux1"] + }, + "output_aux2" : { + "friendly_name" : "Auxiliary Relay 2 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 2, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux2"] + }, + "output_aux3" : { + "friendly_name" : "Auxiliary Relay 3 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 3, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux3"] + }, + "output_aux4" : { + "friendly_name" : "Auxiliary Relay 4 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 4, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux4"] + }, "output_pwm" : { "friendly_name" : "DC Fan PWM GPIO Pin", "description" : "Select the GPIO used for the DC Fan PWM. Note that this should be a PWM capable GPIO.", @@ -592,6 +664,30 @@ "settings" : ["platform", "outputs", "power"], "hidden" : true }, + "output_aux1" : { + "friendly_name" : "Auxiliary Relay 1 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 1, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux1"] + }, + "output_aux2" : { + "friendly_name" : "Auxiliary Relay 2 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 2, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux2"] + }, + "output_aux3" : { + "friendly_name" : "Auxiliary Relay 3 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 3, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux3"] + }, + "output_aux4" : { + "friendly_name" : "Auxiliary Relay 4 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 4, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux4"] + }, "output_pwm" : { "friendly_name" : "DC Fan PWM GPIO Pin", "description" : "Select the GPIO used for the DC Fan PWM. Note that this should be a PWM capable GPIO.", @@ -776,6 +872,30 @@ "settings" : ["platform", "outputs", "power"], "hidden" : true }, + "output_aux1" : { + "friendly_name" : "Auxiliary Relay 1 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 1, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux1"] + }, + "output_aux2" : { + "friendly_name" : "Auxiliary Relay 2 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 2, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux2"] + }, + "output_aux3" : { + "friendly_name" : "Auxiliary Relay 3 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 3, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux3"] + }, + "output_aux4" : { + "friendly_name" : "Auxiliary Relay 4 GPIO Pin", + "description" : "Select the GPIO used for auxiliary relay 4, or Not Installed if you do not have one. Auxiliary relays are never controlled by PiFire during a cook - they are yours to switch.", + "options" : { "None" : "Not Installed", "2" : "GPIO2", "3" : "GPIO3", "4" : "GPIO4", "5" : "GPIO5", "6" : "GPIO6", "7" : "GPIO7", "8" : "GPIO8", "9" : "GPIO9", "10" : "GPIO10", "11" : "GPIO11", "12" : "GPIO12", "13" : "GPIO13", "14" : "GPIO14", "15" : "GPIO15", "16" : "GPIO16", "17" : "GPIO17", "18" : "GPIO18", "19" : "GPIO19", "20" : "GPIO20", "21" : "GPIO21", "22" : "GPIO22", "23" : "GPIO23", "24" : "GPIO24", "25" : "GPIO25", "26" : "GPIO26", "27" : "GPIO27" }, + "settings" : ["platform", "outputs", "aux4"] + }, "output_pwm" : { "friendly_name" : "DC Fan PWM GPIO Pin", "description" : "Select the GPIO used for the DC Fan PWM. Note that this should be a PWM capable GPIO.",