Add webhook trigger condition for automations
Per-automation webhook URL with auto-generated 128-bit hex token.
External services (Home Assistant, IFTTT, curl) can POST to
/api/v1/webhooks/{token} with {"action": "activate"|"deactivate"}
to control automation state — no API key required (token is auth).
Backend: WebhookCondition model, engine state tracking with
immediate evaluation, webhook endpoint, schema/route updates.
Frontend: webhook option in condition editor, URL display with
copy button, card badge, i18n for en/ru/zh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -270,3 +270,34 @@
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Webhook URL row */
|
||||
.webhook-url-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.webhook-url-row input {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-size: 0.8rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.btn-webhook-copy {
|
||||
white-space: nowrap;
|
||||
padding: 6px 12px !important;
|
||||
font-size: 0.8rem !important;
|
||||
}
|
||||
|
||||
.webhook-save-hint {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ import {
|
||||
import {
|
||||
loadAutomations, openAutomationEditor, closeAutomationEditorModal,
|
||||
saveAutomationEditor, addAutomationCondition,
|
||||
toggleAutomationEnabled, deleteAutomation,
|
||||
toggleAutomationEnabled, deleteAutomation, copyWebhookUrl,
|
||||
expandAllAutomationSections, collapseAllAutomationSections,
|
||||
} from './features/automations.js';
|
||||
import {
|
||||
@@ -311,6 +311,7 @@ Object.assign(window, {
|
||||
addAutomationCondition,
|
||||
toggleAutomationEnabled,
|
||||
deleteAutomation,
|
||||
copyWebhookUrl,
|
||||
expandAllAutomationSections,
|
||||
collapseAllAutomationSections,
|
||||
|
||||
|
||||
@@ -132,6 +132,9 @@ function createAutomationCard(automation, sceneMap = new Map()) {
|
||||
if (c.condition_type === 'mqtt') {
|
||||
return `<span class="stream-card-prop stream-card-prop-full">${ICON_RADIO} ${t('automations.condition.mqtt')}: ${escapeHtml(c.topic || '')} = ${escapeHtml(c.payload || '*')}</span>`;
|
||||
}
|
||||
if (c.condition_type === 'webhook') {
|
||||
return `<span class="stream-card-prop">🔗 ${t('automations.condition.webhook')}</span>`;
|
||||
}
|
||||
return `<span class="stream-card-prop">${c.condition_type}</span>`;
|
||||
});
|
||||
const logicLabel = automation.condition_logic === 'and' ? t('automations.logic.and') : t('automations.logic.or');
|
||||
@@ -386,6 +389,7 @@ function addAutomationConditionRow(condition) {
|
||||
<option value="system_idle" ${condType === 'system_idle' ? 'selected' : ''}>${t('automations.condition.system_idle')}</option>
|
||||
<option value="display_state" ${condType === 'display_state' ? 'selected' : ''}>${t('automations.condition.display_state')}</option>
|
||||
<option value="mqtt" ${condType === 'mqtt' ? 'selected' : ''}>${t('automations.condition.mqtt')}</option>
|
||||
<option value="webhook" ${condType === 'webhook' ? 'selected' : ''}>${t('automations.condition.webhook')}</option>
|
||||
</select>
|
||||
<button type="button" class="btn-remove-condition" onclick="this.closest('.automation-condition-row').remove()" title="Remove">✕</button>
|
||||
</div>
|
||||
@@ -475,6 +479,30 @@ function addAutomationConditionRow(condition) {
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
if (type === 'webhook') {
|
||||
if (data.token) {
|
||||
const webhookUrl = window.location.origin + '/api/v1/webhooks/' + data.token;
|
||||
container.innerHTML = `
|
||||
<div class="condition-fields">
|
||||
<small class="condition-always-desc">${t('automations.condition.webhook.hint')}</small>
|
||||
<div class="condition-field">
|
||||
<label>${t('automations.condition.webhook.url')}</label>
|
||||
<div class="webhook-url-row">
|
||||
<input type="text" class="condition-webhook-url" value="${escapeHtml(webhookUrl)}" readonly>
|
||||
<button type="button" class="btn btn-secondary btn-webhook-copy" onclick="copyWebhookUrl(this)">${t('automations.condition.webhook.copy')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" class="condition-webhook-token" value="${escapeHtml(data.token)}">
|
||||
</div>`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="condition-fields">
|
||||
<small class="condition-always-desc">${t('automations.condition.webhook.hint')}</small>
|
||||
<p class="webhook-save-hint">${t('automations.condition.webhook.save_first')}</p>
|
||||
</div>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const appsValue = (data.apps || []).join('\n');
|
||||
const matchType = data.match_type || 'running';
|
||||
container.innerHTML = `
|
||||
@@ -608,6 +636,11 @@ function getAutomationEditorConditions() {
|
||||
payload: row.querySelector('.condition-mqtt-payload').value,
|
||||
match_mode: row.querySelector('.condition-mqtt-match-mode').value || 'exact',
|
||||
});
|
||||
} else if (condType === 'webhook') {
|
||||
const tokenInput = row.querySelector('.condition-webhook-token');
|
||||
const cond = { condition_type: 'webhook' };
|
||||
if (tokenInput && tokenInput.value) cond.token = tokenInput.value;
|
||||
conditions.push(cond);
|
||||
} else {
|
||||
const matchType = row.querySelector('.condition-match-type').value;
|
||||
const appsText = row.querySelector('.condition-apps').value.trim();
|
||||
@@ -677,6 +710,15 @@ export async function toggleAutomationEnabled(automationId, enable) {
|
||||
}
|
||||
}
|
||||
|
||||
export function copyWebhookUrl(btn) {
|
||||
const input = btn.closest('.webhook-url-row').querySelector('.condition-webhook-url');
|
||||
navigator.clipboard.writeText(input.value).then(() => {
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = t('automations.condition.webhook.copied');
|
||||
setTimeout(() => { btn.textContent = orig; }, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteAutomation(automationId, automationName) {
|
||||
const msg = t('automations.delete.confirm').replace('{name}', automationName);
|
||||
const confirmed = await showConfirm(msg);
|
||||
|
||||
@@ -607,6 +607,12 @@
|
||||
"automations.condition.mqtt.match_mode.contains": "Contains",
|
||||
"automations.condition.mqtt.match_mode.regex": "Regex",
|
||||
"automations.condition.mqtt.hint": "Activate when an MQTT topic receives a matching payload",
|
||||
"automations.condition.webhook": "Webhook",
|
||||
"automations.condition.webhook.hint": "Activate via an HTTP call from external services (Home Assistant, IFTTT, curl, etc.)",
|
||||
"automations.condition.webhook.url": "Webhook URL:",
|
||||
"automations.condition.webhook.copy": "Copy",
|
||||
"automations.condition.webhook.copied": "Copied!",
|
||||
"automations.condition.webhook.save_first": "Save the automation first to generate a webhook URL",
|
||||
"automations.scene": "Scene:",
|
||||
"automations.scene.hint": "Scene preset to activate when conditions are met",
|
||||
"automations.scene.search_placeholder": "Search scenes...",
|
||||
|
||||
@@ -607,6 +607,12 @@
|
||||
"automations.condition.mqtt.match_mode.contains": "Содержит",
|
||||
"automations.condition.mqtt.match_mode.regex": "Регулярное выражение",
|
||||
"automations.condition.mqtt.hint": "Активировать при получении совпадающего значения по MQTT топику",
|
||||
"automations.condition.webhook": "Вебхук",
|
||||
"automations.condition.webhook.hint": "Активировать через HTTP-запрос от внешних сервисов (Home Assistant, IFTTT, curl и т.д.)",
|
||||
"automations.condition.webhook.url": "URL вебхука:",
|
||||
"automations.condition.webhook.copy": "Скопировать",
|
||||
"automations.condition.webhook.copied": "Скопировано!",
|
||||
"automations.condition.webhook.save_first": "Сначала сохраните автоматизацию для генерации URL вебхука",
|
||||
"automations.scene": "Сцена:",
|
||||
"automations.scene.hint": "Пресет сцены для активации при выполнении условий",
|
||||
"automations.scene.search_placeholder": "Поиск сцен...",
|
||||
|
||||
@@ -607,6 +607,12 @@
|
||||
"automations.condition.mqtt.match_mode.contains": "包含",
|
||||
"automations.condition.mqtt.match_mode.regex": "正则表达式",
|
||||
"automations.condition.mqtt.hint": "当 MQTT 主题收到匹配的消息时激活",
|
||||
"automations.condition.webhook": "Webhook",
|
||||
"automations.condition.webhook.hint": "通过外部服务的 HTTP 请求激活(Home Assistant、IFTTT、curl 等)",
|
||||
"automations.condition.webhook.url": "Webhook URL:",
|
||||
"automations.condition.webhook.copy": "复制",
|
||||
"automations.condition.webhook.copied": "已复制!",
|
||||
"automations.condition.webhook.save_first": "请先保存自动化以生成 Webhook URL",
|
||||
"automations.scene": "场景:",
|
||||
"automations.scene.hint": "条件满足时激活的场景预设",
|
||||
"automations.scene.search_placeholder": "搜索场景...",
|
||||
|
||||
Reference in New Issue
Block a user