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:
@@ -2,7 +2,7 @@
|
||||
* Automations — automation cards, editor, condition builder, process picker, scene selector.
|
||||
*/
|
||||
|
||||
import { apiKey, _automationsLoading, set_automationsLoading, automationsCacheObj, scenePresetsCache } from '../core/state.ts';
|
||||
import { apiKey, _automationsLoading, set_automationsLoading, automationsCacheObj, scenePresetsCache, _cachedHASources } from '../core/state.ts';
|
||||
import { fetchWithAuth, escapeHtml } from '../core/api.ts';
|
||||
import { t } from '../core/i18n.ts';
|
||||
import { showToast, showConfirm, setTabRefreshing } from '../core/ui.ts';
|
||||
@@ -248,6 +248,7 @@ const CONDITION_PILL_RENDERERS: Record<string, ConditionPillRenderer> = {
|
||||
},
|
||||
mqtt: (c) => `<span class="stream-card-prop stream-card-prop-full">${ICON_RADIO} ${t('automations.condition.mqtt')}: ${escapeHtml(c.topic || '')} = ${escapeHtml(c.payload || '*')}</span>`,
|
||||
webhook: (c) => `<span class="stream-card-prop">${ICON_WEB} ${t('automations.condition.webhook')}</span>`,
|
||||
home_assistant: (c) => `<span class="stream-card-prop stream-card-prop-full">${_icon(P.home)} ${t('automations.condition.home_assistant')}: ${escapeHtml(c.entity_id || '')} = ${escapeHtml(c.state || '*')}</span>`,
|
||||
};
|
||||
|
||||
function createAutomationCard(automation: Automation, sceneMap = new Map()) {
|
||||
@@ -515,11 +516,11 @@ export function addAutomationCondition() {
|
||||
_autoGenerateAutomationName();
|
||||
}
|
||||
|
||||
const CONDITION_TYPE_KEYS = ['always', 'startup', 'application', 'time_of_day', 'system_idle', 'display_state', 'mqtt', 'webhook'];
|
||||
const CONDITION_TYPE_KEYS = ['always', 'startup', 'application', 'time_of_day', 'system_idle', 'display_state', 'mqtt', 'webhook', 'home_assistant'];
|
||||
const CONDITION_TYPE_ICONS = {
|
||||
always: P.refreshCw, startup: P.power, application: P.smartphone,
|
||||
time_of_day: P.clock, system_idle: P.moon, display_state: P.monitor,
|
||||
mqtt: P.radio, webhook: P.globe,
|
||||
mqtt: P.radio, webhook: P.globe, home_assistant: P.home,
|
||||
};
|
||||
|
||||
const MATCH_TYPE_KEYS = ['running', 'topmost', 'topmost_fullscreen', 'fullscreen'];
|
||||
@@ -726,6 +727,44 @@ function addAutomationConditionRow(condition: any) {
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
if (type === 'home_assistant') {
|
||||
const haSourceId = data.ha_source_id || '';
|
||||
const entityId = data.entity_id || '';
|
||||
const haState = data.state || '';
|
||||
const matchMode = data.match_mode || 'exact';
|
||||
// Build HA source options from cached data
|
||||
const haOptions = _cachedHASources.map((s: any) =>
|
||||
`<option value="${s.id}" ${s.id === haSourceId ? 'selected' : ''}>${escapeHtml(s.name)}</option>`
|
||||
).join('');
|
||||
container.innerHTML = `
|
||||
<div class="condition-fields">
|
||||
<small class="condition-always-desc">${t('automations.condition.home_assistant.hint')}</small>
|
||||
<div class="condition-field">
|
||||
<label>${t('automations.condition.home_assistant.ha_source')}</label>
|
||||
<select class="condition-ha-source-id">
|
||||
<option value="">—</option>
|
||||
${haOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div class="condition-field">
|
||||
<label>${t('automations.condition.home_assistant.entity_id')}</label>
|
||||
<input type="text" class="condition-ha-entity-id" value="${escapeHtml(entityId)}" placeholder="binary_sensor.front_door">
|
||||
</div>
|
||||
<div class="condition-field">
|
||||
<label>${t('automations.condition.home_assistant.state')}</label>
|
||||
<input type="text" class="condition-ha-state" value="${escapeHtml(haState)}" placeholder="on">
|
||||
</div>
|
||||
<div class="condition-field">
|
||||
<label>${t('automations.condition.home_assistant.match_mode')}</label>
|
||||
<select class="condition-ha-match-mode">
|
||||
<option value="exact" ${matchMode === 'exact' ? 'selected' : ''}>${t('automations.condition.mqtt.match_mode.exact')}</option>
|
||||
<option value="contains" ${matchMode === 'contains' ? 'selected' : ''}>${t('automations.condition.mqtt.match_mode.contains')}</option>
|
||||
<option value="regex" ${matchMode === 'regex' ? 'selected' : ''}>${t('automations.condition.mqtt.match_mode.regex')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
if (type === 'webhook') {
|
||||
if (data.token) {
|
||||
const webhookUrl = getBaseOrigin() + '/api/v1/webhooks/' + data.token;
|
||||
@@ -835,6 +874,14 @@ function getAutomationEditorConditions() {
|
||||
const cond: any = { condition_type: 'webhook' };
|
||||
if (tokenInput && tokenInput.value) cond.token = tokenInput.value;
|
||||
conditions.push(cond);
|
||||
} else if (condType === 'home_assistant') {
|
||||
conditions.push({
|
||||
condition_type: 'home_assistant',
|
||||
ha_source_id: (row.querySelector('.condition-ha-source-id') as HTMLSelectElement).value,
|
||||
entity_id: (row.querySelector('.condition-ha-entity-id') as HTMLInputElement).value.trim(),
|
||||
state: (row.querySelector('.condition-ha-state') as HTMLInputElement).value,
|
||||
match_mode: (row.querySelector('.condition-ha-match-mode') as HTMLSelectElement).value || 'exact',
|
||||
});
|
||||
} else {
|
||||
const matchType = (row.querySelector('.condition-match-type') as HTMLSelectElement).value;
|
||||
const appsText = (row.querySelector('.condition-apps') as HTMLTextAreaElement).value.trim();
|
||||
|
||||
Reference in New Issue
Block a user