This commit is contained in:
Mateusz Gruszczyński
2026-09-02 10:12:51 +02:00
parent 8180d22cef
commit e50c94b6a3
19 changed files with 323 additions and 108 deletions
+70 -3
View File
@@ -29,6 +29,29 @@ function newFlowId(prefix = 'node') {
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function sharedFlowInputRequiresComparison(kind) {
return ['outdoor_temperature','device_temperature','zone_temperature','ha_state','ha_numeric','ha_attribute','house_mode','device_state','zone_state','group_state'].includes(kind);
}
function sharedFlowReferenceComparisonDefaults(item) {
const kind = item?.kind || '';
if (['outdoor_temperature','device_temperature','zone_temperature'].includes(kind)) return { operator:'lt', value:20 };
if (kind === 'ha_numeric') return { operator:'lt', value:0 };
if (kind === 'ha_state') return { operator:'eq', value:'on' };
if (kind === 'ha_attribute') return { operator:'eq', value:'' };
if (kind === 'house_mode') return { operator:'eq', value:'cool' };
if (kind === 'device_state') return { operator:'eq', value:'true' };
if (kind === 'zone_state') return { operator:'eq', value:'true' };
if (kind === 'group_state') return { operator:'eq', value:'true' };
return {};
}
function sharedFlowReferenceDefaultConfig(item) {
const config = { input_id:item?.id || '' };
if (item && sharedFlowInputRequiresComparison(item.kind)) Object.assign(config, sharedFlowReferenceComparisonDefaults(item));
return config;
}
function flowDefaultConfig(kind) {
if (kind === 'weekday') return { days: [1, 2, 3, 4, 5] };
if (kind === 'time_range') return { start: '06:00', end: '08:00' };
@@ -46,7 +69,7 @@ function flowDefaultConfig(kind) {
if (kind === 'group_state') return { group_id: app.groups[0]?.id || '', field: 'power_enabled', operator: 'eq', value: 'true' };
if (kind === 'night_mode') return {};
if (kind === 'constant') return { value: true };
if (kind === 'shared_input') return { input_id: app.flowSharedInputs?.[0]?.id || '' };
if (kind === 'shared_input') return sharedFlowReferenceDefaultConfig(app.flowSharedInputs?.[0]);
if (kind === 'zone_thermostat') return { zone_id: app.zones[0]?.id || '', preset: 'comfort', setpoint: 21, mode: 'auto', cooldown_seconds: 60 };
if (kind === 'device_action') return { device_id: app.devices[0]?.id || '', power: true, mode: '', target_temperature: null, fan_speed: null, swing_vertical: null, swing_horizontal: null, quiet: null, turbo: null, light: null, air: null, xfan: null, health: null, sleep: null, cooldown_seconds: 60 };
if (kind === 'group_action') return { group_id: app.groups[0]?.id || '', power: true, mode: '', preset: '', setpoint: 21, cooldown_seconds: 60 };
@@ -109,7 +132,16 @@ function flowNodeSummary(node) {
if (node.kind === 'group_state') return `${app.groups.find(g => g.id === c.group_id)?.name || tr('groups.group')} · ${c.field || 'power_enabled'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
if (node.kind === 'night_mode') return tr('flow.nightModeActive');
if (node.kind === 'constant') return c.value === false ? tr('common.no') : tr('common.yes');
if (node.kind === 'shared_input') { const item = (app.flowSharedInputs || []).find(value => value.id === c.input_id); return item ? `${item.name} · ${sharedFlowInputSummary(item)}` : tr('flow.sharedInputMissing'); }
if (node.kind === 'shared_input') {
const item = (app.flowSharedInputs || []).find(value => value.id === c.input_id);
if (!item) return tr('flow.sharedInputMissing');
if (sharedFlowInputRequiresComparison(item.kind)) {
return c.operator
? `${item.name} · ${sharedFlowInputSourceSummary(item)} · ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`
: `${item.name} · ${tr('flow.selectOperator')}`;
}
return `${item.name} · ${sharedFlowInputSourceSummary(item)}`;
}
if (node.kind === 'logic_and') return tr('flow.allConditions');
if (node.kind === 'logic_or') return tr('flow.anyCondition');
if (node.kind === 'logic_not') return tr('flow.invertCondition');
@@ -161,6 +193,20 @@ function flowSelectOptions(items, selected, nameFn = item => item.name) {
}
function flowOperatorOptions(selected) { return [['lt','<'],['lte','≤'],['gt','>'],['gte','≥'],['eq','='],['neq','≠']].map(([v,l]) => `<option value="${v}" ${v === selected ? 'selected' : ''}>${l}</option>`).join(''); }
function sharedFlowReferenceComparisonFields(item, config) {
if (!item || !sharedFlowInputRequiresComparison(item.kind)) return '';
const defaults = sharedFlowReferenceComparisonDefaults(item);
const selected = config.operator || '';
const numeric = ['outdoor_temperature','device_temperature','zone_temperature','ha_numeric'].includes(item.kind);
const fullOperators = numeric || item.kind === 'ha_attribute';
const operatorOptions = `${!selected ? `<option value="" selected disabled>${esc(tr('flow.selectOperator'))}</option>` : ''}${fullOperators ? flowOperatorOptions(selected) : [['eq','='],['neq','≠']].map(([value,label]) => `<option value="${value}" ${value === selected ? 'selected' : ''}>${label}</option>`).join('')}`;
const value = config.value ?? defaults.value ?? '';
let valueField = `<input data-flow-config="value" value="${esc(value)}">`;
if (numeric) valueField = `<input type="number" step="0.1" data-flow-config="value" value="${Number(value ?? 0)}">`;
else if (item.kind === 'house_mode') valueField = `<select data-flow-config="value">${['cool','heat','off'].map(v => `<option value="${v}" ${String(value) === v ? 'selected' : ''}>${esc(v === 'off' ? tr('common.off') : tr(`mode.${v}`))}</option>`).join('')}</select>`;
return `<p class="field-note">${esc(tr('flow.sharedInputFlowComparisonHint'))}</p><div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${operatorOptions}</select></label><label><span>${esc(tr('flow.value'))}</span>${valueField}</label></div>`;
}
function renderFlowInspector() {
const host = $('#flowInspector'), node = flowNodeById(app.flowSelectedNodeId); if (!host) return;
if (!node) { host.innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.selectBlock'))}</strong><span>${esc(tr('flow.selectBlockHint'))}</span></div>`; return; }
@@ -182,7 +228,12 @@ function renderFlowInspector() {
else if (node.kind === 'group_state') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-flow-config="group_id">${flowSelectOptions(app.groups, c.group_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-flow-config="field"><option value="power_enabled" selected>power_enabled</option></select></label>${flowTextComparisonFields(c)}`;
else if (node.kind === 'night_mode') fields = `<p class="field-note">${esc(tr('flow.nightModeHint'))}</p>`;
else if (node.kind === 'constant') fields = `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value"><option value="true" ${c.value !== false ? 'selected' : ''}>${esc(tr('common.yes'))}</option><option value="false" ${c.value === false ? 'selected' : ''}>${esc(tr('common.no'))}</option></select></label>`;
else if (node.kind === 'shared_input') fields = (app.flowSharedInputs || []).length ? `<label><span>${esc(tr('flow.sharedInputTitle'))}</span><select data-flow-config="input_id">${flowSelectOptions(app.flowSharedInputs || [], c.input_id)}</select></label><p class="field-note">${esc(tr('flow.sharedInputNodeHint'))}</p>` : `<div class="empty compact"><strong>${esc(tr('flow.sharedInputsEmpty'))}</strong><span>${esc(tr('flow.sharedInputNodeEmptyHint'))}</span></div>`;
else if (node.kind === 'shared_input') {
const item = (app.flowSharedInputs || []).find(value => value.id === c.input_id) || app.flowSharedInputs?.[0];
fields = (app.flowSharedInputs || []).length
? `<label><span>${esc(tr('flow.sharedInputTitle'))}</span><select data-flow-config="input_id">${flowSelectOptions(app.flowSharedInputs || [], c.input_id)}</select></label><p class="field-note">${esc(tr('flow.sharedInputNodeHint'))}</p>${sharedFlowReferenceComparisonFields(item, c)}`
: `<div class="empty compact"><strong>${esc(tr('flow.sharedInputsEmpty'))}</strong><span>${esc(tr('flow.sharedInputNodeEmptyHint'))}</span></div>`;
}
else if (node.kind === 'logic_and') fields = `<p class="field-note">${esc(tr('flow.andHint'))}</p>`;
else if (node.kind === 'logic_or') fields = `<p class="field-note">${esc(tr('flow.orHint'))}</p>`;
else if (node.kind === 'logic_not') fields = `<p class="field-note">${esc(tr('flow.notHint'))}</p>`;
@@ -698,6 +749,22 @@ function updateFlowConfig(input) {
const node = flowNodeById(app.flowSelectedNodeId); if (!node) return;
const key = input.dataset.flowConfig; if (!key) return;
if (key === 'days') node.config.days = $$('[data-flow-config="days"]', $('#flowInspector')).filter(el => el.checked).map(el => Number(el.value));
else if (node.kind === 'shared_input' && key === 'input_id') {
const item = (app.flowSharedInputs || []).find(value => value.id === input.value);
node.config = sharedFlowReferenceDefaultConfig(item);
}
else if (node.kind === 'shared_input' && key === 'operator') {
node.config.operator = input.value;
if (node.config.value == null) {
const item = (app.flowSharedInputs || []).find(value => value.id === node.config.input_id);
node.config.value = sharedFlowReferenceComparisonDefaults(item).value ?? '';
}
}
else if (node.kind === 'shared_input' && key === 'value') {
const item = (app.flowSharedInputs || []).find(value => value.id === node.config.input_id);
const numeric = ['outdoor_temperature','device_temperature','zone_temperature','ha_numeric'].includes(item?.kind);
node.config.value = numeric ? (input.value === '' ? null : Number(input.value)) : input.value;
}
else if (['power','swing_vertical','swing_horizontal','quiet','turbo','light','air','xfan','health','sleep'].includes(key)) node.config[key] = input.value === '' ? null : input.value === 'true';
else if (key === 'value' && node.kind === 'constant') node.config[key] = input.value === 'true';
else if (['value','setpoint','target_temperature','cooldown_seconds','fan_speed'].includes(key) && ['outdoor_temperature','device_temperature','zone_temperature','ha_numeric','zone_thermostat','device_action','group_action'].includes(node.kind)) node.config[key] = input.value === '' ? null : Number(input.value);
+36 -21
View File
@@ -36,9 +36,31 @@ function flowSharedInputKinds() {
];
}
function sharedFlowInputSummary(item) {
function flowSharedInputDefaultConfig(kind) {
if (kind === 'device_temperature') return { device_id: app.devices[0]?.id || '' };
if (kind === 'zone_temperature') return { zone_id: app.zones[0]?.id || '' };
if (kind === 'ha_state' || kind === 'ha_numeric' || kind === 'ha_available') return { entity_id: '' };
if (kind === 'ha_attribute') return { entity_id: '', attribute: '' };
if (kind === 'device_state') return { device_id: app.devices[0]?.id || '', field: 'online' };
if (kind === 'zone_state') return { zone_id: app.zones[0]?.id || '', field: 'demand' };
if (kind === 'group_state') return { group_id: app.groups[0]?.id || '', field: 'power_enabled' };
if (kind === 'constant') return { value: true };
return {};
}
function sharedFlowInputSourceSummary(item) {
if (!item) return '—';
try { return flowNodeSummary({ kind: item.kind, config: item.config || {} }); } catch (_) { return item.kind || '—'; }
const c = item.config || {};
if (item.kind === 'outdoor_temperature') return tr('flow.node.outdoorTemperature');
if (item.kind === 'device_temperature') return app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice');
if (item.kind === 'zone_temperature') return app.zones.find(value => value.id === c.zone_id)?.name || tr('common.noZone');
if (item.kind === 'ha_state' || item.kind === 'ha_numeric' || item.kind === 'ha_available') return c.entity_id || 'entity_id';
if (item.kind === 'ha_attribute') return `${c.entity_id || 'entity_id'}.${c.attribute || 'attribute'}`;
if (item.kind === 'house_mode') return tr('flow.houseMode');
if (item.kind === 'device_state') return `${app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice')} · ${c.field || 'state'}`;
if (item.kind === 'zone_state') return `${app.zones.find(value => value.id === c.zone_id)?.name || tr('common.noZone')} · ${c.field || 'state'}`;
if (item.kind === 'group_state') return `${app.groups.find(value => value.id === c.group_id)?.name || tr('groups.group')} · ${c.field || 'power_enabled'}`;
return flowNodeSummary({ kind:item.kind, config:c });
}
function isHaSharedInputKind(kind) { return ['ha_state','ha_numeric','ha_attribute','ha_available'].includes(kind); }
@@ -56,7 +78,7 @@ function renderFlowSharedInputs() {
? `<div class="flow-shared-usage"><small>${esc(tr('flow.sharedInputUsedBy', { count:usages.length }))}</small><div>${usages.slice(0, 4).map(flow => `<button type="button" class="link-button" data-open-shared-flow="${esc(flow.id)}" title="${esc(tr('flow.openReferencedFlow', { name:flow.name }))}">${esc(flow.name)}</button>`).join('')}${usages.length > 4 ? `<span class="muted">+${usages.length - 4}</span>` : ''}</div></div>`
: `<small class="flow-shared-unused">${esc(tr('flow.sharedInputUnused'))}</small>`;
return `<div class="flow-shared-input-row">
<div><strong>${esc(item.name)}</strong><small>${esc(flowNodeTitle(FLOW_NODE_META[item.kind] || { title:item.kind }))} · ${esc(sharedFlowInputSummary(item))}</small><code>${esc(item.id)}</code>${usageMarkup}</div>
<div><strong>${esc(item.name)}</strong><small>${esc(flowNodeTitle(FLOW_NODE_META[item.kind] || { title:item.kind }))} · ${esc(sharedFlowInputSourceSummary(item))}</small><code>${esc(item.id)}</code>${usageMarkup}</div>
<div class="flow-shared-input-actions"><button type="button" class="secondary" data-flow-shared-edit="${esc(item.id)}">${esc(tr('actions.edit'))}</button><button type="button" class="danger" data-flow-shared-delete="${esc(item.id)}">${esc(tr('actions.delete'))}</button></div>
</div>`;
}).join('') : `<div class="empty compact"><strong>${esc(tr('flow.sharedInputsEmpty'))}</strong><span>${esc(tr('flow.sharedInputsEmptyHint'))}</span></div>`;
@@ -68,28 +90,22 @@ function sharedFlowOptions(items, selected, label = item => item.name) {
return items.map(item => `<option value="${esc(item.id)}" ${item.id === selected ? 'selected' : ''}>${esc(label(item))}</option>`).join('');
}
function sharedFlowOperatorOptions(selected = 'eq') {
return [['lt','<'],['lte','≤'],['gt','>'],['gte','≥'],['eq','='],['neq','≠']].map(([value,label]) => `<option value="${value}" ${value === selected ? 'selected' : ''}>${label}</option>`).join('');
}
function renderFlowSharedInputFields(kind, config = {}) {
const host = $('#flowSharedInputFields'); if (!host) return;
const c = config || {};
const comparison = (temperature = false) => `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator">${sharedFlowOperatorOptions(c.operator || 'lt')}</select></label><label><span>${esc(temperature ? tr('automations.thresholdC') : tr('flow.value'))}</span><input type="number" step="0.1" data-shared-config="value" value="${Number(c.value ?? 0)}"></label></div>`;
const textComparison = () => `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator"><option value="eq" ${c.operator !== 'neq' ? 'selected' : ''}>=</option><option value="neq" ${c.operator === 'neq' ? 'selected' : ''}>≠</option></select></label><label><span>${esc(tr('flow.value'))}</span><input data-shared-config="value" value="${esc(c.value ?? '')}"></label></div>`;
let fields = '';
if (kind === 'constant') fields = `<label><span>${esc(tr('flow.value'))}</span><select data-shared-config="value"><option value="true" ${c.value !== false ? 'selected' : ''}>${esc(tr('common.yes'))}</option><option value="false" ${c.value === false ? 'selected' : ''}>${esc(tr('common.no'))}</option></select></label>`;
else if (kind === 'ha_state') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label>${textComparison()}`;
else if (kind === 'ha_numeric') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.energy_price"></label>${comparison(false)}`;
else if (kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-shared-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label><div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator">${sharedFlowOperatorOptions(c.operator || 'eq')}</select></label><label><span>${esc(tr('flow.value'))}</span><input data-shared-config="value" value="${esc(c.value ?? '')}"></label></div>`;
else if (kind === 'ha_state') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label>`;
else if (kind === 'ha_numeric') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.energy_price"></label>`;
else if (kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-shared-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label>`;
else if (kind === 'ha_available') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label><p class="field-note">${esc(tr('flow.haAvailableHint'))}</p>`;
else if (kind === 'outdoor_temperature') fields = comparison(true);
else if (kind === 'device_temperature') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label>${comparison(true)}`;
else if (kind === 'zone_temperature') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label>${comparison(true)}`;
else if (kind === 'house_mode') fields = `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-shared-config="operator"><option value="eq" ${c.operator !== 'neq' ? 'selected' : ''}>=</option><option value="neq" ${c.operator === 'neq' ? 'selected' : ''}>≠</option></select></label><label><span>${esc(tr('flow.houseMode'))}</span><select data-shared-config="value">${['cool','heat','off'].map(v => `<option value="${v}" ${c.value === v ? 'selected' : ''}>${esc(v === 'off' ? tr('common.off') : tr(`mode.${v}`))}</option>`).join('')}</select></label></div>`;
else if (kind === 'device_state') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-config="field">${['enabled','online','power','mode','fan_speed','swing_vertical','swing_horizontal','quiet','turbo','light','air','xfan','health','sleep'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>${textComparison()}`;
else if (kind === 'zone_state') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-config="field">${['enabled','mode','active_preset','demand','control_owner','device_manual_override','local_thermostat_power'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>${textComparison()}`;
else if (kind === 'group_state') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-shared-config="group_id">${sharedFlowOptions(app.groups, c.group_id)}</select></label><input type="hidden" data-shared-config="field" value="power_enabled">${textComparison()}`;
else if (kind === 'outdoor_temperature') fields = `<p class="field-note">${esc(tr('flow.sharedInputSourceOnlyHint'))}</p>`;
else if (kind === 'device_temperature') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label>`;
else if (kind === 'zone_temperature') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label>`;
else if (kind === 'house_mode') fields = `<p class="field-note">${esc(tr('flow.sharedInputSourceOnlyHint'))}</p>`;
else if (kind === 'device_state') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-config="field">${['enabled','online','power','mode','fan_speed','swing_vertical','swing_horizontal','quiet','turbo','light','air','xfan','health','sleep'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>`;
else if (kind === 'zone_state') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-config="field">${['enabled','mode','active_preset','demand','control_owner','device_manual_override','local_thermostat_power'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>`;
else if (kind === 'group_state') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-shared-config="group_id">${sharedFlowOptions(app.groups, c.group_id)}</select></label><input type="hidden" data-shared-config="field" value="power_enabled">`;
else if (kind === 'night_mode') fields = `<p class="field-note">${esc(tr('flow.nightModeHint'))}</p>`;
host.innerHTML = fields;
const testPanel = $('#flowSharedInputTestPanel'), testResult = $('#flowSharedInputTestResult');
@@ -106,7 +122,7 @@ function openFlowSharedInputEditor(id = '') {
form.name.value = item?.name || '';
form.kind.value = item?.kind || 'constant';
form.dataset.editingId = item?.id || '';
renderFlowSharedInputFields(form.kind.value, item?.config || flowDefaultConfig(form.kind.value));
renderFlowSharedInputFields(form.kind.value, item?.config || flowSharedInputDefaultConfig(form.kind.value));
dialog.showModal();
setTimeout(() => form.name.focus(), 0);
}
@@ -117,7 +133,6 @@ function collectFlowSharedInputConfig(kind) {
const key = field.dataset.sharedConfig;
let value = field.value;
if (kind === 'constant' && key === 'value') value = value === 'true';
else if (field.type === 'number') value = Number(value);
config[key] = value;
});
return config;
+15 -24
View File
@@ -293,28 +293,19 @@ document.addEventListener('change', event => {
$('#addFlowSharedInput')?.addEventListener('click', () => openFlowSharedInputEditor());
$('#flowSharedInputKind')?.addEventListener('change', event => {
renderFlowSharedInputFields(event.target.value, flowDefaultConfig(event.target.value));
renderFlowSharedInputFields(event.target.value, flowSharedInputDefaultConfig(event.target.value));
});
function compareFlowSharedHaValue(actual, operator, expected) {
if (['lt','lte','gt','gte'].includes(operator)) {
const a = Number(actual), b = Number(expected);
if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
if (operator === 'lt') return a < b;
if (operator === 'lte') return a <= b;
if (operator === 'gt') return a > b;
return a >= b;
}
const left = actual == null ? '' : String(actual);
const right = expected == null ? '' : String(expected);
return operator === 'neq' ? left !== right : left === right;
}
function evaluateFlowSharedHaTest(kind, config, result) {
if (kind === 'ha_available') return { actual:result.state, matched:result.available === true };
let actual = result.state;
if (kind === 'ha_attribute') actual = result.attributes?.[config.attribute];
return { actual, matched:compareFlowSharedHaValue(actual, config.operator || 'eq', config.value) };
if (kind === 'ha_available') return { actual:result.available === true, valid:true };
if (kind === 'ha_attribute') {
const actual = result.attributes?.[config.attribute];
return { actual, valid:actual !== undefined };
}
if (kind === 'ha_numeric') {
const actual = Number(result.state);
return { actual, valid:Number.isFinite(actual) };
}
return { actual:result.state, valid:true };
}
$('#flowSharedInputTest')?.addEventListener('click', async event => {
@@ -336,10 +327,10 @@ $('#flowSharedInputTest')?.addEventListener('click', async event => {
const result = await api('/api/integrations/home-assistant/entity', { method:'POST', body:{ entity_id:entityId } });
const evaluation = evaluateFlowSharedHaTest(kind, config, result);
const actual = evaluation.actual == null ? 'null' : (typeof evaluation.actual === 'object' ? JSON.stringify(evaluation.actual) : String(evaluation.actual));
const expectation = kind === 'ha_available' ? tr('flow.sharedInputTestAvailable') : `${config.operator || 'eq'} ${config.value ?? ''}`;
resultHost.classList.toggle('pass', evaluation.matched);
resultHost.classList.toggle('fail', !evaluation.matched);
resultHost.innerHTML = `<div><strong>${esc(evaluation.matched ? tr('flow.sharedInputTestMatched') : tr('flow.sharedInputTestNotMatched'))}</strong><span class="badge ${evaluation.matched ? 'active' : ''}">${esc(result.available ? tr('flow.sharedInputTestAvailable') : tr('flow.sharedInputTestUnavailable'))}</span></div><dl><div><dt>${esc(tr('flow.sharedInputTestCurrent'))}</dt><dd><code>${esc(actual)}</code></dd></div><div><dt>${esc(tr('flow.sharedInputTestExpected'))}</dt><dd><code>${esc(expectation)}</code></dd></div></dl><small>${esc(result.entity_id)}${result.last_updated ? ` · ${esc(dateTime(result.last_updated))}` : ''}</small>`;
const success = evaluation.valid;
resultHost.classList.toggle('pass', success);
resultHost.classList.toggle('fail', !success);
resultHost.innerHTML = `<div><strong>${esc(success ? tr('flow.sharedInputTestValueRead') : tr('flow.sharedInputTestUnavailable'))}</strong><span class="badge ${success ? 'active' : ''}">${esc(result.available ? tr('flow.sharedInputTestAvailable') : tr('flow.sharedInputTestUnavailable'))}</span></div><dl><div><dt>${esc(tr('flow.sharedInputTestCurrent'))}</dt><dd><code>${esc(actual)}</code></dd></div></dl><small>${esc(result.entity_id)}${result.last_updated ? ` · ${esc(dateTime(result.last_updated))}` : ''}</small>`;
} catch (error) {
resultHost.classList.remove('pass'); resultHost.classList.add('fail');
resultHost.innerHTML = `<strong>${esc(tr('flow.sharedInputTestUnavailable'))}</strong><span>${esc(error.message)}</span>`;