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);