feat: Home Assistant integration — WebSocket connection, automation conditions, UI

Add full Home Assistant integration via WebSocket API:
- HARuntime: persistent WebSocket client with auth, auto-reconnect, entity state cache
- HAManager: ref-counted runtime pool (like WeatherManager)
- HomeAssistantCondition: new automation trigger type matching entity states
- REST API: CRUD for HA sources + /test, /entities, /status endpoints
- /api/v1/system/integrations-status: combined MQTT + HA dashboard indicators
- Frontend: HA Sources tab in Streams, condition type in automation editor
- Modal editor with host, token, SSL, entity filters
- websockets>=13.0 dependency added
This commit is contained in:
2026-03-27 22:42:48 +03:00
parent f3d07fc47f
commit 2153dde4b7
26 changed files with 1912 additions and 119 deletions

View File

@@ -213,6 +213,7 @@
{% include 'modals/test-value-source.html' %}
{% include 'modals/sync-clock-editor.html' %}
{% include 'modals/weather-source-editor.html' %}
{% include 'modals/ha-source-editor.html' %}
{% include 'modals/asset-upload.html' %}
{% include 'modals/asset-editor.html' %}
{% include 'modals/settings.html' %}

View File

@@ -0,0 +1,82 @@
<!-- Home Assistant Source Editor Modal -->
<div id="ha-source-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="ha-source-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="ha-source-modal-title" data-i18n="ha_source.add">Add Home Assistant Source</h2>
<button class="modal-close-btn" onclick="closeHASourceModal()" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<form id="ha-source-form" onsubmit="return false;">
<input type="hidden" id="ha-source-id">
<div id="ha-source-error" class="error-message" style="display: none;"></div>
<!-- Name -->
<div class="form-group">
<div class="label-row">
<label for="ha-source-name" data-i18n="ha_source.name">Name:</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_source.name.hint">A descriptive name for this Home Assistant connection</small>
<input type="text" id="ha-source-name" data-i18n-placeholder="ha_source.name.placeholder" placeholder="My Home Assistant" required>
<div id="ha-source-tags-container"></div>
</div>
<!-- Host -->
<div class="form-group">
<div class="label-row">
<label for="ha-source-host" data-i18n="ha_source.host">Host:</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_source.host.hint">Home Assistant host and port, e.g. 192.168.1.100:8123</small>
<input type="text" id="ha-source-host" placeholder="192.168.1.100:8123" required>
</div>
<!-- Token -->
<div class="form-group">
<div class="label-row">
<label for="ha-source-token" data-i18n="ha_source.token">Access Token:</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_source.token.hint">Long-Lived Access Token from HA (Profile > Security > Long-Lived Access Tokens)</small>
<small id="ha-source-token-hint" class="input-hint" style="display:none" data-i18n="ha_source.token.edit_hint">Leave blank to keep the current token</small>
<input type="password" id="ha-source-token" placeholder="eyJ0eXAiOi...">
</div>
<!-- SSL -->
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="ha-source-ssl">
<span data-i18n="ha_source.use_ssl">Use SSL (wss://)</span>
</label>
</div>
<!-- Entity Filters -->
<div class="form-group">
<div class="label-row">
<label for="ha-source-filters" data-i18n="ha_source.entity_filters">Entity Filters (optional):</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_source.entity_filters.hint">Comma-separated glob patterns to filter entities, e.g. sensor.*, binary_sensor.front_door. Leave empty for all entities.</small>
<input type="text" id="ha-source-filters" placeholder="sensor.*, binary_sensor.*">
</div>
<!-- Description -->
<div class="form-group">
<div class="label-row">
<label for="ha-source-description" data-i18n="ha_source.description">Description (optional):</label>
</div>
<input type="text" id="ha-source-description" placeholder="">
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeHASourceModal()" title="Cancel" data-i18n-title="settings.button.cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-secondary" id="ha-source-test-btn" onclick="testHASource()" title="Test" data-i18n-title="ha_source.test" style="display:none">
<svg class="icon" viewBox="0 0 24 24"><path d="M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2"/><path d="M6.453 15h11.094"/><path d="M8.5 2h7"/></svg>
</button>
<button class="btn btn-icon btn-primary" onclick="saveHASource()" title="Save" data-i18n-title="settings.button.save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>