feat: HA light output targets — cast LED colors to Home Assistant lights
Lint & Test / test (push) Has been cancelled

New output target type `ha_light` that sends averaged LED colors to HA
light entities via WebSocket service calls (light.turn_on/turn_off):

Backend:
- HARuntime.call_service(): fire-and-forget WS service calls
- HALightOutputTarget: data model with light mappings, update rate, transition
- HALightTargetProcessor: processing loop with delta detection, rate limiting
- ProcessorManager.add_ha_light_target(): registration
- API schemas/routes updated for ha_light target type

Frontend:
- HA Light Targets section in Targets tab tree nav
- Modal editor: HA source picker, CSS source picker, light entity mappings
- Target cards with start/stop/clone/edit actions
- i18n keys for all new UI strings
This commit is contained in:
2026-03-28 00:08:49 +03:00
parent fb98e6e2b8
commit cb9289f01f
25 changed files with 1679 additions and 164 deletions
@@ -0,0 +1,92 @@
<!-- HA Light Target Editor Modal -->
<div id="ha-light-editor-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="ha-light-editor-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="ha-light-editor-title" data-i18n="ha_light.add">Add HA Light Target</h2>
<button class="modal-close-btn" onclick="closeHALightEditor()" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<form id="ha-light-editor-form" onsubmit="return false;">
<input type="hidden" id="ha-light-editor-id">
<div id="ha-light-editor-error" class="error-message" style="display: none;"></div>
<!-- Name -->
<div class="form-group">
<div class="label-row">
<label for="ha-light-editor-name" data-i18n="ha_light.name">Name:</label>
</div>
<input type="text" id="ha-light-editor-name" data-i18n-placeholder="ha_light.name.placeholder" placeholder="Living Room Lights" required>
<div id="ha-light-tags-container"></div>
</div>
<!-- HA Source -->
<div class="form-group">
<div class="label-row">
<label for="ha-light-editor-ha-source" data-i18n="ha_light.ha_source">HA Connection:</label>
</div>
<select id="ha-light-editor-ha-source"></select>
</div>
<!-- CSS Source -->
<div class="form-group">
<div class="label-row">
<label for="ha-light-editor-css-source" data-i18n="ha_light.css_source">Color Strip Source:</label>
</div>
<select id="ha-light-editor-css-source"></select>
</div>
<!-- Update Rate -->
<div class="form-group">
<div class="label-row">
<label for="ha-light-editor-update-rate">
<span data-i18n="ha_light.update_rate">Update Rate:</span>
<span id="ha-light-editor-update-rate-display">2.0</span> Hz
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="ha_light.update_rate.hint">How often to send color updates to HA lights (0.5-5.0 Hz). Lower values are safer for HA performance.</small>
<input type="range" id="ha-light-editor-update-rate" min="0.5" max="5.0" step="0.5" value="2.0"
oninput="document.getElementById('ha-light-editor-update-rate-display').textContent = parseFloat(this.value).toFixed(1)">
</div>
<!-- Transition -->
<div class="form-group">
<div class="label-row">
<label for="ha-light-editor-transition">
<span data-i18n="ha_light.transition">Transition:</span>
<span id="ha-light-editor-transition-display">0.5</span>s
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="ha_light.transition.hint">Smooth fade duration between colors (HA transition parameter). Higher values give smoother but slower transitions.</small>
<input type="range" id="ha-light-editor-transition" min="0" max="5.0" step="0.1" value="0.5"
oninput="document.getElementById('ha-light-editor-transition-display').textContent = parseFloat(this.value).toFixed(1)">
</div>
<!-- Light Mappings -->
<div class="form-group">
<div class="label-row">
<label data-i18n="ha_light.mappings">Light Mappings:</label>
<button type="button" class="btn btn-sm btn-secondary" onclick="addHALightMapping()" data-i18n-title="ha_light.mappings.add">+</button>
</div>
<small class="input-hint" data-i18n="ha_light.mappings.hint">Map LED ranges to HA light entities. Each mapping averages the LED segment to a single color.</small>
<div id="ha-light-mappings-list"></div>
</div>
<!-- Description -->
<div class="form-group">
<div class="label-row">
<label for="ha-light-editor-description" data-i18n="ha_light.description">Description (optional):</label>
</div>
<input type="text" id="ha-light-editor-description" placeholder="">
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeHALightEditor()" title="Cancel" data-i18n-title="settings.button.cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveHALightEditor()" title="Save" data-i18n-title="settings.button.save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>