v0.12.0-preety_code
This commit is contained in:
+97
-97
@@ -39,24 +39,24 @@ function newFlowId(prefix = 'node') {
|
||||
}
|
||||
|
||||
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);
|
||||
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' };
|
||||
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 || '' };
|
||||
const config = { input_id: item?.id || '' };
|
||||
if (item && sharedFlowInputRequiresComparison(item.kind)) Object.assign(config, sharedFlowReferenceComparisonDefaults(item));
|
||||
return config;
|
||||
}
|
||||
@@ -180,38 +180,38 @@ function flowSharedInputValueSignature(item) {
|
||||
}
|
||||
|
||||
function flowSharedInputLocalObservation(item) {
|
||||
if (!item) return { hasValue:false, value:null };
|
||||
if (!item) return { hasValue: false, value: null };
|
||||
const c = item.config || {};
|
||||
if (item.kind === 'outdoor_temperature') return { hasValue:app.outdoorTemperature !== null && app.outdoorTemperature !== undefined && Number.isFinite(Number(app.outdoorTemperature)), value:app.outdoorTemperature };
|
||||
if (item.kind === 'outdoor_temperature') return { hasValue: app.outdoorTemperature !== null && app.outdoorTemperature !== undefined && Number.isFinite(Number(app.outdoorTemperature)), value: app.outdoorTemperature };
|
||||
if (item.kind === 'device_temperature') {
|
||||
const value = app.devices.find(device => device.id === c.device_id)?.current_temperature;
|
||||
return { hasValue:value !== null && value !== undefined && Number.isFinite(Number(value)), value };
|
||||
return { hasValue: value !== null && value !== undefined && Number.isFinite(Number(value)), value };
|
||||
}
|
||||
if (item.kind === 'zone_temperature') {
|
||||
const value = app.zones.find(zone => zone.id === c.zone_id)?.current_temperature;
|
||||
return { hasValue:value !== null && value !== undefined && Number.isFinite(Number(value)), value };
|
||||
return { hasValue: value !== null && value !== undefined && Number.isFinite(Number(value)), value };
|
||||
}
|
||||
if (item.kind === 'house_mode') {
|
||||
const value = app.settings?.house_mode;
|
||||
return { hasValue:value !== null && value !== undefined && value !== '', value };
|
||||
return { hasValue: value !== null && value !== undefined && value !== '', value };
|
||||
}
|
||||
if (item.kind === 'device_state') {
|
||||
const device = app.devices.find(value => value.id === c.device_id), value = device?.[c.field];
|
||||
return { hasValue:value !== undefined && value !== null, value };
|
||||
return { hasValue: value !== undefined && value !== null, value };
|
||||
}
|
||||
if (item.kind === 'zone_state') {
|
||||
const zone = app.zones.find(value => value.id === c.zone_id), value = zone?.[c.field];
|
||||
return { hasValue:value !== undefined && value !== null, value };
|
||||
return { hasValue: value !== undefined && value !== null, value };
|
||||
}
|
||||
if (item.kind === 'group_state') {
|
||||
const group = app.groups.find(value => value.id === c.group_id), value = group?.[c.field || 'power_enabled'];
|
||||
return { hasValue:value !== undefined && value !== null, value };
|
||||
return { hasValue: value !== undefined && value !== null, value };
|
||||
}
|
||||
if (item.kind === 'night_mode') {
|
||||
const value = app.controlPlan?.night_mode_active;
|
||||
return { hasValue:typeof value === 'boolean', value };
|
||||
return { hasValue: typeof value === 'boolean', value };
|
||||
}
|
||||
if (item.kind === 'constant') return { hasValue:true, value:c.value };
|
||||
if (item.kind === 'constant') return { hasValue: true, value: c.value };
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -219,13 +219,13 @@ function flowSharedInputObservation(item) {
|
||||
const local = flowSharedInputLocalObservation(item);
|
||||
if (local) return local;
|
||||
const cached = app.flowSharedInputValueCache?.[item?.id];
|
||||
return cached?.signature === flowSharedInputValueSignature(item) ? cached : { hasValue:false, value:null };
|
||||
return cached?.signature === flowSharedInputValueSignature(item) ? cached : { hasValue: false, value: null };
|
||||
}
|
||||
|
||||
function flowSharedInputCurrentText(item, observation = flowSharedInputObservation(item)) {
|
||||
if (!observation?.hasValue) return '—';
|
||||
const value = observation.value;
|
||||
if (['outdoor_temperature','device_temperature','zone_temperature'].includes(item.kind)) return fmtTemp(value);
|
||||
if (['outdoor_temperature', 'device_temperature', 'zone_temperature'].includes(item.kind)) return fmtTemp(value);
|
||||
if (item.kind === 'ha_numeric') {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return '—';
|
||||
@@ -249,7 +249,7 @@ function renderFlowSharedInputCurrentValues() {
|
||||
}
|
||||
|
||||
async function loadFlowSharedInputHaValue(item) {
|
||||
if (!item || !['ha_state','ha_numeric','ha_attribute','ha_available'].includes(item.kind)) return;
|
||||
if (!item || !['ha_state', 'ha_numeric', 'ha_attribute', 'ha_available'].includes(item.kind)) return;
|
||||
const signature = flowSharedInputValueSignature(item), cached = app.flowSharedInputValueCache?.[item.id];
|
||||
if (cached?.signature === signature && Date.now() - Number(cached.fetchedAt || 0) < 15000) return;
|
||||
if (app.flowSharedInputValueRequests?.[item.id] === signature) return;
|
||||
@@ -257,7 +257,7 @@ async function loadFlowSharedInputHaValue(item) {
|
||||
try {
|
||||
const entityId = String(item.config?.entity_id || '').trim();
|
||||
if (!entityId) throw new Error('missing entity_id');
|
||||
const result = await api('/api/integrations/home-assistant/entity', { method:'POST', body:{ entity_id:entityId } });
|
||||
const result = await api('/api/integrations/home-assistant/entity', { method: 'POST', body: { entity_id: entityId } });
|
||||
let value = result.state, hasValue = true;
|
||||
if (item.kind === 'ha_available') value = result.available === true;
|
||||
else if (item.kind === 'ha_attribute') {
|
||||
@@ -268,11 +268,11 @@ async function loadFlowSharedInputHaValue(item) {
|
||||
hasValue = Number.isFinite(value);
|
||||
}
|
||||
app.flowSharedInputValueCache[item.id] = {
|
||||
signature, fetchedAt:Date.now(), hasValue, value,
|
||||
unit:item.kind === 'ha_numeric' ? result.attributes?.unit_of_measurement : '',
|
||||
signature, fetchedAt: Date.now(), hasValue, value,
|
||||
unit: item.kind === 'ha_numeric' ? result.attributes?.unit_of_measurement : '',
|
||||
};
|
||||
} catch (_) {
|
||||
app.flowSharedInputValueCache[item.id] = { signature, fetchedAt:Date.now(), hasValue:false, value:null };
|
||||
app.flowSharedInputValueCache[item.id] = { signature, fetchedAt: Date.now(), hasValue: false, value: null };
|
||||
} finally {
|
||||
if (app.flowSharedInputValueRequests?.[item.id] === signature) delete app.flowSharedInputValueRequests[item.id];
|
||||
renderFlowSharedInputCurrentValues();
|
||||
@@ -377,7 +377,7 @@ function setFlowZoom(value, { render = true } = {}) {
|
||||
function fitFlowToView({ maxZoom = 1 } = {}) {
|
||||
const workspace = $('#flowWorkspace');
|
||||
const nodes = app.flowDraft?.nodes || [];
|
||||
if (!workspace || !nodes.length) { setFlowZoom(1); if (workspace) workspace.scrollTo({ left:0, top:0, behavior:'smooth' }); return; }
|
||||
if (!workspace || !nodes.length) { setFlowZoom(1); if (workspace) workspace.scrollTo({ left: 0, top: 0, behavior: 'smooth' }); return; }
|
||||
const width = 170, height = 96, pad = 56;
|
||||
const minX = Math.max(0, Math.min(...nodes.map(node => Number(node.x || 0))) - pad);
|
||||
const minY = Math.max(0, Math.min(...nodes.map(node => Number(node.y || 0))) - pad);
|
||||
@@ -387,7 +387,7 @@ function fitFlowToView({ maxZoom = 1 } = {}) {
|
||||
const availableW = Math.max(220, workspace.clientWidth - 24), availableH = Math.max(180, workspace.clientHeight - 24);
|
||||
const zoom = clamp(Math.min(availableW / contentW, availableH / contentH, maxZoom), .45, 1.35);
|
||||
setFlowZoom(zoom);
|
||||
requestAnimationFrame(() => workspace.scrollTo({ left:Math.max(0, minX * app.flowZoom - 12), top:Math.max(0, minY * app.flowZoom - 12), behavior:'smooth' }));
|
||||
requestAnimationFrame(() => workspace.scrollTo({ left: Math.max(0, minX * app.flowZoom - 12), top: Math.max(0, minY * app.flowZoom - 12), behavior: 'smooth' }));
|
||||
}
|
||||
|
||||
function renderFlowBlockLibrary(filter = '') {
|
||||
@@ -423,8 +423,8 @@ function renderFlowEditor() {
|
||||
</article>`;
|
||||
}).join('');
|
||||
$('#flowEmptyHint').hidden = draft.nodes.length > 0;
|
||||
const selectionCount = $('#flowSelectionCount'); if (selectionCount) selectionCount.textContent = (app.flowSelectedNodeIds || []).length ? tr('flow.selectedCount', { count:(app.flowSelectedNodeIds || []).length }) : '';
|
||||
renderFlowNameMode(); renderFlowEnabledLabel(); setFlowZoom(app.flowZoom || 1, { render:false }); renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation(); renderFlowRuntimeInfo(); renderFlowSaveStatus(); refreshFlowSharedInputCurrentValues();
|
||||
const selectionCount = $('#flowSelectionCount'); if (selectionCount) selectionCount.textContent = (app.flowSelectedNodeIds || []).length ? tr('flow.selectedCount', { count: (app.flowSelectedNodeIds || []).length }) : '';
|
||||
renderFlowNameMode(); renderFlowEnabledLabel(); setFlowZoom(app.flowZoom || 1, { render: false }); renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation(); renderFlowRuntimeInfo(); renderFlowSaveStatus(); refreshFlowSharedInputCurrentValues();
|
||||
const status = $('#flowCompileStatus');
|
||||
status.textContent = draft.draft ? tr('flow.draftStatus') : tr('flow.compileCount', { schedules: draft.compiled_schedule_ids?.length || 0, automations: draft.compiled_automation_ids?.length || 0 });
|
||||
status.classList.toggle('flow-draft-badge', draft.draft === true);
|
||||
@@ -447,19 +447,19 @@ function renderFlowEdges() {
|
||||
function flowSelectOptions(items, selected, nameFn = item => item.name) {
|
||||
return items.map(item => `<option value="${esc(item.id)}" ${item.id === selected ? 'selected' : ''}>${esc(nameFn(item))}</option>`).join('');
|
||||
}
|
||||
function flowOperatorOptions(selected) { return [['lt','<'],['lte','≤'],['gt','>'],['gte','≥'],['eq','='],['neq','≠']].map(([v,l]) => `<option value="${v}" ${v === selected ? 'selected' : ''}>${l}</option>`).join(''); }
|
||||
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 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 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>`;
|
||||
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>`;
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ function renderFlowInspector() {
|
||||
if (!node) { host.classList.remove('is-expanded'); host.innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.selectBlock'))}</strong><span>${esc(tr('flow.selectBlockHint'))}</span></div>`; return; }
|
||||
const c = node.config || {}, meta = FLOW_NODE_META[node.kind] || { title: node.kind };
|
||||
let fields = '';
|
||||
if (node.kind === 'weekday') fields = `<div class="flow-day-grid">${[1,2,3,4,5,6,7].map(day => `<label><input type="checkbox" data-flow-config="days" value="${day}" ${(c.days || []).includes(day) ? 'checked' : ''}><span>${esc(tr(`day.${day}`))}</span></label>`).join('')}</div>`;
|
||||
if (node.kind === 'weekday') fields = `<div class="flow-day-grid">${[1, 2, 3, 4, 5, 6, 7].map(day => `<label><input type="checkbox" data-flow-config="days" value="${day}" ${(c.days || []).includes(day) ? 'checked' : ''}><span>${esc(tr(`day.${day}`))}</span></label>`).join('')}</div>`;
|
||||
else if (node.kind === 'time_range') fields = `<div class="two"><label><span>${esc(tr('common.from'))}</span><input type="time" data-flow-config="start" value="${esc(c.start || '06:00')}"></label><label><span>${esc(tr('common.to'))}</span><input type="time" data-flow-config="end" value="${esc(c.end || '08:00')}"></label></div>`;
|
||||
else if (node.kind === 'date_range') fields = `<div class="two"><label><span>${esc(tr('common.from'))}</span><input type="date" data-flow-config="start" value="${esc(c.start || '')}"></label><label><span>${esc(tr('common.to'))}</span><input type="date" data-flow-config="end" value="${esc(c.end || '')}"></label></div>`;
|
||||
else if (node.kind === 'cron_trigger') fields = `<label><span>CRON</span><input data-flow-config="expression" value="${esc(c.expression || '*/5 * * * *')}" placeholder="*/5 * * * *"></label><p class="field-note">${esc(tr('flow.cronHint'))}</p>`;
|
||||
@@ -478,8 +478,8 @@ function renderFlowInspector() {
|
||||
else if (node.kind === 'on_change') fields = `<label><span>${esc(tr('flow.changeMode'))}</span><select data-flow-config="mode"><option value="result" ${c.mode !== 'value' ? 'selected' : ''}>${esc(tr('flow.changeModeResult'))}</option><option value="value" ${c.mode === 'value' ? 'selected' : ''}>${esc(tr('flow.changeModeValue'))}</option></select></label><p class="field-note">${esc(tr('flow.onChangeHint'))}</p>`;
|
||||
else if (node.kind === 'rate_limit') fields = `<div class="two"><label><span>${esc(tr('flow.maxExecutions'))}</span><input type="number" min="1" max="1000" data-flow-config="max_count" value="${Number(c.max_count || 1)}"></label><label><span>${esc(tr('flow.periodSeconds'))}</span><input type="number" min="1" max="2678400" data-flow-config="period_seconds" value="${Number(c.period_seconds || 3600)}"></label></div><p class="field-note">${esc(tr('flow.rateLimitHint'))}</p>`;
|
||||
else if (node.kind === 'delay') fields = `<label><span>${esc(tr('flow.durationSeconds'))}</span><input type="number" min="1" max="604800" data-flow-config="seconds" value="${Number(c.seconds || 30)}"></label><p class="field-note">${esc(tr('flow.delayHint'))}</p>`;
|
||||
else if (node.kind === 'rolling_stat') { const source=c.source || 'outdoor_temperature'; fields = `<label><span>${esc(tr('flow.source'))}</span><select data-flow-config="source">${[['outdoor_temperature',tr('flow.node.outdoorTemperature')],['device_temperature',tr('flow.node.deviceTemperature')],['zone_temperature',tr('flow.node.zoneTemperature')],['ha_numeric',tr('flow.node.haNumeric')]].map(([v,l])=>`<option value="${v}" ${source===v?'selected':''}>${esc(l)}</option>`).join('')}</select></label>${source==='device_temperature'?`<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices,c.device_id)}</select></label>`:''}${source==='zone_temperature'?`<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones,c.zone_id)}</select></label>`:''}${source==='ha_numeric'?`<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id||'')}" placeholder="sensor.temperature"></label>`:''}<div class="two"><label><span>${esc(tr('flow.statistic'))}</span><select data-flow-config="statistic"><option value="mean" ${c.statistic!=='median'?'selected':''}>${esc(tr('flow.mean'))}</option><option value="median" ${c.statistic==='median'?'selected':''}>${esc(tr('flow.median'))}</option></select></label><label><span>${esc(tr('flow.windowSeconds'))}</span><input type="number" min="10" max="604800" data-flow-config="window_seconds" value="${Number(c.window_seconds||300)}"></label></div>${flowComparisonFields(c,false)}`; }
|
||||
else if (node.kind === 'oscillates') { const source=c.source || 'outdoor_temperature'; fields = `<label><span>${esc(tr('flow.source'))}</span><select data-flow-config="source">${[['outdoor_temperature',tr('flow.node.outdoorTemperature')],['device_temperature',tr('flow.node.deviceTemperature')],['zone_temperature',tr('flow.node.zoneTemperature')],['ha_numeric',tr('flow.node.haNumeric')]].map(([v,l])=>`<option value="${v}" ${source===v?'selected':''}>${esc(l)}</option>`).join('')}</select></label>${source==='device_temperature'?`<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices,c.device_id)}</select></label>`:''}${source==='zone_temperature'?`<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones,c.zone_id)}</select></label>`:''}${source==='ha_numeric'?`<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id||'')}" placeholder="sensor.temperature"></label>`:''}<div class="two"><label><span>${esc(tr('flow.windowSeconds'))}</span><input type="number" min="10" max="604800" data-flow-config="window_seconds" value="${Number(c.window_seconds||300)}"></label><label><span>${esc(tr('flow.minSpan'))}</span><input type="number" min="0.001" step="0.1" data-flow-config="min_span" value="${Number(c.min_span??1)}"></label></div><label><span>${esc(tr('flow.minDirectionChanges'))}</span><input type="number" min="1" max="1000" data-flow-config="min_direction_changes" value="${Number(c.min_direction_changes||2)}"></label><p class="field-note">${esc(tr('flow.oscillatesHint'))}</p>`; }
|
||||
else if (node.kind === 'rolling_stat') { const source = c.source || 'outdoor_temperature'; fields = `<label><span>${esc(tr('flow.source'))}</span><select data-flow-config="source">${[['outdoor_temperature', tr('flow.node.outdoorTemperature')], ['device_temperature', tr('flow.node.deviceTemperature')], ['zone_temperature', tr('flow.node.zoneTemperature')], ['ha_numeric', tr('flow.node.haNumeric')]].map(([v, l]) => `<option value="${v}" ${source === v ? 'selected' : ''}>${esc(l)}</option>`).join('')}</select></label>${source === 'device_temperature' ? `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>` : ''}${source === 'zone_temperature' ? `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label>` : ''}${source === 'ha_numeric' ? `<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.temperature"></label>` : ''}<div class="two"><label><span>${esc(tr('flow.statistic'))}</span><select data-flow-config="statistic"><option value="mean" ${c.statistic !== 'median' ? 'selected' : ''}>${esc(tr('flow.mean'))}</option><option value="median" ${c.statistic === 'median' ? 'selected' : ''}>${esc(tr('flow.median'))}</option></select></label><label><span>${esc(tr('flow.windowSeconds'))}</span><input type="number" min="10" max="604800" data-flow-config="window_seconds" value="${Number(c.window_seconds || 300)}"></label></div>${flowComparisonFields(c, false)}`; }
|
||||
else if (node.kind === 'oscillates') { const source = c.source || 'outdoor_temperature'; fields = `<label><span>${esc(tr('flow.source'))}</span><select data-flow-config="source">${[['outdoor_temperature', tr('flow.node.outdoorTemperature')], ['device_temperature', tr('flow.node.deviceTemperature')], ['zone_temperature', tr('flow.node.zoneTemperature')], ['ha_numeric', tr('flow.node.haNumeric')]].map(([v, l]) => `<option value="${v}" ${source === v ? 'selected' : ''}>${esc(l)}</option>`).join('')}</select></label>${source === 'device_temperature' ? `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>` : ''}${source === 'zone_temperature' ? `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label>` : ''}${source === 'ha_numeric' ? `<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.temperature"></label>` : ''}<div class="two"><label><span>${esc(tr('flow.windowSeconds'))}</span><input type="number" min="10" max="604800" data-flow-config="window_seconds" value="${Number(c.window_seconds || 300)}"></label><label><span>${esc(tr('flow.minSpan'))}</span><input type="number" min="0.001" step="0.1" data-flow-config="min_span" value="${Number(c.min_span ?? 1)}"></label></div><label><span>${esc(tr('flow.minDirectionChanges'))}</span><input type="number" min="1" max="1000" data-flow-config="min_direction_changes" value="${Number(c.min_direction_changes || 2)}"></label><p class="field-note">${esc(tr('flow.oscillatesHint'))}</p>`; }
|
||||
else if (node.kind === 'outdoor_temperature') fields = flowComparisonFields(c);
|
||||
else if (node.kind === 'device_temperature') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>${flowComparisonFields(c)}`;
|
||||
else if (node.kind === 'zone_temperature') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label>${flowComparisonFields(c)}`;
|
||||
@@ -487,9 +487,9 @@ function renderFlowInspector() {
|
||||
else if (node.kind === 'ha_numeric') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.outdoor_temperature"></label>${flowComparisonFields(c, false)}`;
|
||||
else if (node.kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-flow-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label>${flowTextComparisonFields(c, true)}`;
|
||||
else if (node.kind === 'ha_available') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label><p class="field-note">${esc(tr('flow.haAvailableHint'))}</p>`;
|
||||
else if (node.kind === 'house_mode') fields = `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-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-flow-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 (node.kind === 'device_state') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-flow-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>${flowTextComparisonFields(c)}`;
|
||||
else if (node.kind === 'zone_state') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-flow-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>${flowTextComparisonFields(c)}`;
|
||||
else if (node.kind === 'house_mode') fields = `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-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-flow-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 (node.kind === 'device_state') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-flow-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>${flowTextComparisonFields(c)}`;
|
||||
else if (node.kind === 'zone_state') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-flow-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>${flowTextComparisonFields(c)}`;
|
||||
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>`;
|
||||
@@ -502,26 +502,26 @@ function renderFlowInspector() {
|
||||
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>`;
|
||||
else if (node.kind === 'zone_thermostat') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label><div class="two"><label><span>${esc(tr('schedules.profile'))}</span><select data-flow-config="preset">${['auto','comfort','sleep','away','custom'].map(v => `<option value="${v}" ${c.preset === v ? 'selected' : ''}>${esc(zonePresetLabel(v))}</option>`).join('')}</select></label><label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.5" data-flow-config="setpoint" value="${Number(c.setpoint ?? 21)}"></label></div><div class="two"><label><span>${esc(tr('common.mode'))}</span><select data-flow-config="mode"><option value="auto" ${c.mode === 'auto' ? 'selected' : ''}>Auto</option><option value="heat" ${c.mode === 'heat' ? 'selected' : ''}>${esc(tr('mode.heat'))}</option><option value="cool" ${c.mode === 'cool' ? 'selected' : ''}>${esc(tr('mode.cool'))}</option></select></label><label><span>${esc(tr('common.power'))}</span><select data-flow-config="power"><option value="" ${c.power == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c.power === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c.power === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label></div><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
|
||||
else if (node.kind === 'zone_thermostat') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label><div class="two"><label><span>${esc(tr('schedules.profile'))}</span><select data-flow-config="preset">${['auto', 'comfort', 'sleep', 'away', 'custom'].map(v => `<option value="${v}" ${c.preset === v ? 'selected' : ''}>${esc(zonePresetLabel(v))}</option>`).join('')}</select></label><label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.5" data-flow-config="setpoint" value="${Number(c.setpoint ?? 21)}"></label></div><div class="two"><label><span>${esc(tr('common.mode'))}</span><select data-flow-config="mode"><option value="auto" ${c.mode === 'auto' ? 'selected' : ''}>Auto</option><option value="heat" ${c.mode === 'heat' ? 'selected' : ''}>${esc(tr('mode.heat'))}</option><option value="cool" ${c.mode === 'cool' ? 'selected' : ''}>${esc(tr('mode.cool'))}</option></select></label><label><span>${esc(tr('common.power'))}</span><select data-flow-config="power"><option value="" ${c.power == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c.power === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c.power === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label></div><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
|
||||
else if (node.kind === 'device_action') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>${flowActionFields(c, false)}`;
|
||||
else if (node.kind === 'group_action') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-flow-config="group_id">${flowSelectOptions(app.groups, c.group_id)}</select></label>${flowActionFields(c, true)}`;
|
||||
else if (node.kind === 'ha_service_action') fields = `<div class="two"><label><span>domain</span><input data-flow-config="domain" value="${esc(c.domain || 'switch')}" placeholder="switch"></label><label><span>service</span><input data-flow-config="service" value="${esc(c.service || 'turn_on')}" placeholder="turn_on"></label></div><p class="field-note">${esc(tr('flow.serviceExample'))}</p><label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="switch.gas"></label><label><span>${esc(tr('flow.serviceData'))}</span><textarea rows="5" data-flow-config="data_json">${esc(JSON.stringify(c.data || {}, null, 2))}</textarea></label><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
|
||||
host.innerHTML = `<div class="flow-inspector-head"><div><span class="eyebrow">${esc(tr('flow.blockSettings'))}</span><h3>${esc(flowNodeTitle(meta))}</h3></div><div class="flow-inspector-head-actions"><button type="button" class="secondary flow-inspector-expand" data-action="flow-toggle-inspector" aria-label="${esc(tr('flow.expandSettings'))}">↕</button><button type="button" class="secondary flow-inspector-close" data-action="flow-clear-selection" aria-label="${esc(tr('flow.clearSelection'))}">×</button><button type="button" class="danger flow-inspector-delete" data-flow-remove="${esc(node.id)}">${esc(tr('actions.delete'))}</button></div></div>${fields}<div class="flow-inspector-meta"><small>ID</small><code>${esc(node.id)}</code></div>`;
|
||||
}
|
||||
|
||||
function flowTextComparisonFields(c, numeric = false) { const options = numeric ? flowOperatorOptions(c.operator || 'eq') : [['eq','='],['neq','≠']].map(([v,l]) => `<option value="${v}" ${v === (c.operator || 'eq') ? 'selected' : ''}>${l}</option>`).join(''); return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${options}</select></label><label><span>${esc(tr('flow.value'))}</span><input data-flow-config="value" value="${esc(c.value ?? '')}"></label></div>`; }
|
||||
function flowTextComparisonFields(c, numeric = false) { const options = numeric ? flowOperatorOptions(c.operator || 'eq') : [['eq', '='], ['neq', '≠']].map(([v, l]) => `<option value="${v}" ${v === (c.operator || 'eq') ? 'selected' : ''}>${l}</option>`).join(''); return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${options}</select></label><label><span>${esc(tr('flow.value'))}</span><input data-flow-config="value" value="${esc(c.value ?? '')}"></label></div>`; }
|
||||
function flowComparisonFields(c, temperature = true) { return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${flowOperatorOptions(c.operator || 'lt')}</select></label><label><span>${esc(temperature ? tr('automations.thresholdC') : tr('flow.value'))}</span><input type="number" step="0.1" data-flow-config="value" value="${Number(c.value ?? 0)}"></label></div>`; }
|
||||
function flowOptionalBoolField(c, key) {
|
||||
return `<label><span>${esc(key)}</span><select data-flow-config="${esc(key)}"><option value="" ${c[key] == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c[key] === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c[key] === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label>`;
|
||||
}
|
||||
function flowActionFields(c, group) {
|
||||
const modes = group ? ['auto','house','heat','cool'] : ['auto','cool','dry','fan','heat'];
|
||||
const modes = group ? ['auto', 'house', 'heat', 'cool'] : ['auto', 'cool', 'dry', 'fan', 'heat'];
|
||||
const modeOptions = modes.map(v => `<option value="${v}" ${c.mode === v ? 'selected' : ''}>${esc(v === 'auto' ? 'Auto' : v === 'house' ? tr('flow.houseMode') : (tr(`mode.${v}`) || v))}</option>`).join('');
|
||||
const base = `<div class="two"><label><span>${esc(tr('common.power'))}</span><select data-flow-config="power"><option value="" ${c.power == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c.power === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c.power === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label><label><span>${esc(tr('common.mode'))}</span><select data-flow-config="mode"><option value="">${esc(tr('actions.noChange'))}</option>${modeOptions}</select></label></div>`;
|
||||
const target = group
|
||||
? `<label><span>${esc(tr('groups.profile'))}</span><select data-flow-config="preset"><option value="">${esc(tr('actions.noChange'))}</option>${['auto','comfort','sleep','away','custom'].map(v => `<option value="${v}" ${c.preset === v ? 'selected' : ''}>${esc(zonePresetLabel(v))}</option>`).join('')}</select></label><label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.5" data-flow-config="setpoint" value="${c.setpoint ?? 21}"></label>`
|
||||
? `<label><span>${esc(tr('groups.profile'))}</span><select data-flow-config="preset"><option value="">${esc(tr('actions.noChange'))}</option>${['auto', 'comfort', 'sleep', 'away', 'custom'].map(v => `<option value="${v}" ${c.preset === v ? 'selected' : ''}>${esc(zonePresetLabel(v))}</option>`).join('')}</select></label><label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.5" data-flow-config="setpoint" value="${c.setpoint ?? 21}"></label>`
|
||||
: `<label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.5" data-flow-config="target_temperature" value="${c.target_temperature ?? ''}"></label>`;
|
||||
const deviceOptions = group ? '' : `<details class="flow-device-options"><summary>${esc(tr('flow.deviceOptions'))}</summary><div class="two"><label><span>fan_speed</span><select data-flow-config="fan_speed"><option value="" ${c.fan_speed == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option>${[0,1,2,3,4,5].map(v => `<option value="${v}" ${Number(c.fan_speed) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label>${flowOptionalBoolField(c,'swing_vertical')}${flowOptionalBoolField(c,'swing_horizontal')}${flowOptionalBoolField(c,'quiet')}${flowOptionalBoolField(c,'turbo')}${flowOptionalBoolField(c,'light')}${flowOptionalBoolField(c,'air')}${flowOptionalBoolField(c,'xfan')}${flowOptionalBoolField(c,'health')}${flowOptionalBoolField(c,'sleep')}</div><p class="field-note">${esc(tr('flow.deviceOptionsHint'))}</p></details>`;
|
||||
const deviceOptions = group ? '' : `<details class="flow-device-options"><summary>${esc(tr('flow.deviceOptions'))}</summary><div class="two"><label><span>fan_speed</span><select data-flow-config="fan_speed"><option value="" ${c.fan_speed == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option>${[0, 1, 2, 3, 4, 5].map(v => `<option value="${v}" ${Number(c.fan_speed) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label>${flowOptionalBoolField(c, 'swing_vertical')}${flowOptionalBoolField(c, 'swing_horizontal')}${flowOptionalBoolField(c, 'quiet')}${flowOptionalBoolField(c, 'turbo')}${flowOptionalBoolField(c, 'light')}${flowOptionalBoolField(c, 'air')}${flowOptionalBoolField(c, 'xfan')}${flowOptionalBoolField(c, 'health')}${flowOptionalBoolField(c, 'sleep')}</div><p class="field-note">${esc(tr('flow.deviceOptionsHint'))}</p></details>`;
|
||||
return `${base}${target}${deviceOptions}<label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
|
||||
}
|
||||
|
||||
@@ -546,7 +546,7 @@ function flowExpressionSummary(actionId) {
|
||||
|
||||
function renderFlowInterpretation() {
|
||||
const target = $('#flowInterpretation'); if (!target || !app.flowDraft) return;
|
||||
const actions = app.flowDraft.nodes.filter(node => ['action','haaction'].includes(FLOW_NODE_META[node.kind]?.category));
|
||||
const actions = app.flowDraft.nodes.filter(node => ['action', 'haaction'].includes(FLOW_NODE_META[node.kind]?.category));
|
||||
if (!actions.length) { target.textContent = tr('flow.noActionsYet'); return; }
|
||||
target.textContent = actions.map(action => `${flowExpressionSummary(action.id)} → ${flowNodeTitle(FLOW_NODE_META[action.kind])}: ${flowNodeSummary(action)}`).join(' · ');
|
||||
}
|
||||
@@ -570,7 +570,7 @@ function renderFlowRuntimeInfo() {
|
||||
const container = $('#flowRuntimeInfo');
|
||||
if (!target || !app.flowDraft) return;
|
||||
const seconds = Math.max(2, Number(app.settings?.zone_interval_seconds || 5));
|
||||
const actions = app.flowDraft.nodes.filter(node => ['action','haaction'].includes(FLOW_NODE_META[node.kind]?.category));
|
||||
const actions = app.flowDraft.nodes.filter(node => ['action', 'haaction'].includes(FLOW_NODE_META[node.kind]?.category));
|
||||
if (!actions.length) {
|
||||
target.textContent = tr('flow.runtimeNoActions', { seconds });
|
||||
if (container) container.title = tr('flow.runtimeHint');
|
||||
@@ -610,7 +610,7 @@ function addFlowNode(kind) {
|
||||
const viewportY = workspace ? (workspace.scrollTop / (app.flowZoom || 1)) + 54 : 70;
|
||||
const node = { id: newFlowId('node'), kind, x: Math.max(36, viewportX + (count % 3) * 24), y: Math.max(36, viewportY + (count % 3) * 24), config: flowDefaultConfig(kind) };
|
||||
app.flowDraft.nodes.push(node); app.flowSelectedNodeId = node.id; app.flowSelectedNodeIds = [node.id]; app.flowDirty = true; renderFlowEditor();
|
||||
requestAnimationFrame(() => $(`[data-flow-node="${CSS.escape(node.id)}"]`)?.scrollIntoView({ block:'center', inline:'center', behavior:'smooth' }));
|
||||
requestAnimationFrame(() => $(`[data-flow-node="${CSS.escape(node.id)}"]`)?.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' }));
|
||||
}
|
||||
function removeFlowNode(id) {
|
||||
if (!app.flowDraft) return;
|
||||
@@ -668,7 +668,7 @@ async function importFlowFile(file) {
|
||||
finally { const input = $('#flowImportFile'); if (input) input.value = ''; }
|
||||
}
|
||||
|
||||
const FLOW_PRESET_CATEGORY_ORDER = ['comfort','energy','safety','night','reliability','home_assistant','advanced'];
|
||||
const FLOW_PRESET_CATEGORY_ORDER = ['comfort', 'energy', 'safety', 'night', 'reliability', 'home_assistant', 'advanced'];
|
||||
const FLOW_PRESET_FAVORITES_KEY = 'gree_controller_flow_preset_favorites';
|
||||
const FLOW_PRESET_RECENT_KEY = 'gree_controller_flow_preset_recent';
|
||||
let flowPresetLoadPromise = null;
|
||||
@@ -684,7 +684,7 @@ function flowPresetStoredIds(key) {
|
||||
}
|
||||
function flowPresetFavoriteIds() { return new Set(flowPresetStoredIds(FLOW_PRESET_FAVORITES_KEY)); }
|
||||
function flowPresetRecentIds() { return flowPresetStoredIds(FLOW_PRESET_RECENT_KEY); }
|
||||
function saveFlowPresetIds(key, ids) { try { localStorage.setItem(key, JSON.stringify(ids)); } catch (_) {} }
|
||||
function saveFlowPresetIds(key, ids) { try { localStorage.setItem(key, JSON.stringify(ids)); } catch (_) { } }
|
||||
function toggleFlowPresetFavorite(id) {
|
||||
const ids = flowPresetFavoriteIds();
|
||||
if (ids.has(id)) ids.delete(id); else ids.add(id);
|
||||
@@ -757,7 +757,7 @@ function materializeFlowPreset(preset) {
|
||||
zone_id: zone1,
|
||||
preset: node.config.preset || 'comfort',
|
||||
setpoint: Number(node.config.setpoint ?? 21),
|
||||
mode: ['heat','cool'].includes(node.config.mode) ? node.config.mode : 'auto',
|
||||
mode: ['heat', 'cool'].includes(node.config.mode) ? node.config.mode : 'auto',
|
||||
cooldown_seconds: Number(node.config.cooldown_seconds || 60),
|
||||
power: node.config.power ?? null,
|
||||
};
|
||||
@@ -767,13 +767,13 @@ function materializeFlowPreset(preset) {
|
||||
});
|
||||
const edges = sourceEdges
|
||||
.filter(edge => !skipped.has(edge.from) && !skipped.has(edge.to) && idMap.has(edge.from) && idMap.has(edge.to))
|
||||
.map(edge => ({ id:newFlowId('edge'), from:idMap.get(edge.from), to:idMap.get(edge.to) }));
|
||||
.map(edge => ({ id: newFlowId('edge'), from: idMap.get(edge.from), to: idMap.get(edge.to) }));
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
function flowPresetRequirements(preset) {
|
||||
const nodes = preset?.flow?.nodes || [];
|
||||
const placeholders = { zone:new Set(), device:new Set(), group:new Set(), shared:new Set() };
|
||||
const placeholders = { zone: new Set(), device: new Set(), group: new Set(), shared: new Set() };
|
||||
const visit = value => {
|
||||
if (typeof value === 'string') {
|
||||
const match = /^\$(zone|device|group|shared)(\d+)$/.exec(value);
|
||||
@@ -784,14 +784,14 @@ function flowPresetRequirements(preset) {
|
||||
else if (value && typeof value === 'object') Object.values(value).forEach(visit);
|
||||
};
|
||||
nodes.forEach(node => visit(node.config || {}));
|
||||
const hasHa = nodes.some(node => ['ha_state','ha_numeric','ha_attribute','ha_available','ha_service_action'].includes(node.kind));
|
||||
const hasHa = nodes.some(node => ['ha_state', 'ha_numeric', 'ha_attribute', 'ha_available', 'ha_service_action'].includes(node.kind));
|
||||
const haEntities = [...new Set(nodes.map(node => node.config?.entity_id).filter(value => typeof value === 'string' && value && !value.startsWith('$')))];
|
||||
const requirements = [];
|
||||
const missing = [];
|
||||
const addCount = (kind, count, available, key) => {
|
||||
if (!count) return;
|
||||
const label = tr(key, { count });
|
||||
requirements.push({ label, ok:available >= count });
|
||||
requirements.push({ label, ok: available >= count });
|
||||
if (available < count) missing.push(label);
|
||||
};
|
||||
addCount('zone', placeholders.zone.size, (app.zones || []).length, 'flow.templateRequiresZone');
|
||||
@@ -800,10 +800,10 @@ function flowPresetRequirements(preset) {
|
||||
addCount('shared', placeholders.shared.size, (app.flowSharedInputs || []).length, 'flow.templateRequiresShared');
|
||||
if (hasHa) {
|
||||
const ready = Boolean(app.settings?.home_assistant?.url && app.settings?.home_assistant?.token_configured);
|
||||
requirements.push({ label:tr('flow.templateRequiresHa'), ok:ready });
|
||||
requirements.push({ label: tr('flow.templateRequiresHa'), ok: ready });
|
||||
if (!ready) missing.push(tr('flow.templateRequiresHa'));
|
||||
}
|
||||
if (haEntities.length) requirements.push({ label:`${tr('flow.templateRequiresEntity')}: ${haEntities.join(', ')}`, ok:true, info:true });
|
||||
if (haEntities.length) requirements.push({ label: `${tr('flow.templateRequiresEntity')}: ${haEntities.join(', ')}`, ok: true, info: true });
|
||||
return { requirements, missing };
|
||||
}
|
||||
|
||||
@@ -814,14 +814,14 @@ function flowPresetPreviewGraph(preset) {
|
||||
const xs = nodes.map(node => Number(node.x || 0)), ys = nodes.map(node => Number(node.y || 0));
|
||||
const minX = Math.min(...xs), maxX = Math.max(...xs), minY = Math.min(...ys), maxY = Math.max(...ys);
|
||||
const spanX = Math.max(1, maxX - minX), spanY = Math.max(1, maxY - minY);
|
||||
const point = node => ({ x:8 + ((Number(node.x || 0) - minX) / spanX) * 84, y:12 + ((Number(node.y || 0) - minY) / spanY) * 76 });
|
||||
const point = node => ({ x: 8 + ((Number(node.x || 0) - minX) / spanX) * 84, y: 12 + ((Number(node.y || 0) - minY) / spanY) * 76 });
|
||||
const lines = edges.map(edge => {
|
||||
const from = byId.get(edge.from), to = byId.get(edge.to); if (!from || !to) return '';
|
||||
const a = point(from), b = point(to);
|
||||
return `<line x1="${a.x.toFixed(2)}" y1="${a.y.toFixed(2)}" x2="${b.x.toFixed(2)}" y2="${b.y.toFixed(2)}"></line>`;
|
||||
}).join('');
|
||||
const blocks = nodes.map(node => {
|
||||
const p = point(node), meta = FLOW_NODE_META[node.kind] || { title:node.kind, category:'logic' };
|
||||
const p = point(node), meta = FLOW_NODE_META[node.kind] || { title: node.kind, category: 'logic' };
|
||||
return `<div class="flow-template-preview-node flow-template-preview-node-${esc(meta.category)}" style="left:${p.x.toFixed(2)}%;top:${p.y.toFixed(2)}%" title="${esc(flowNodeTitle(meta))}">${esc(flowNodeTitle(meta))}</div>`;
|
||||
}).join('');
|
||||
return `<div class="flow-template-preview-canvas"><svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">${lines}</svg>${blocks}</div>`;
|
||||
@@ -836,13 +836,13 @@ function renderFlowPresetPreview(preset) {
|
||||
const req = flowPresetRequirements(preset);
|
||||
const favorite = flowPresetFavoriteIds().has(preset.id);
|
||||
const reqMarkup = req.requirements.length
|
||||
? req.requirements.map(item => `<span class="flow-template-requirement ${item.ok ? 'ok' : 'missing'} ${item.info ? 'info' : ''}">${item.ok ? '✓' : '!' } ${esc(item.label)}</span>`).join('')
|
||||
? req.requirements.map(item => `<span class="flow-template-requirement ${item.ok ? 'ok' : 'missing'} ${item.info ? 'info' : ''}">${item.ok ? '✓' : '!'} ${esc(item.label)}</span>`).join('')
|
||||
: `<span class="flow-template-requirement ok">✓ ${esc(tr('flow.templateRequirementsReady'))}</span>`;
|
||||
host.innerHTML = `<div class="flow-template-preview-head"><div><span class="eyebrow">${esc(tr('flow.templatePreview'))}</span><h3>${esc(flowPresetText(preset.name) || preset.id)}</h3></div><button type="button" class="flow-template-favorite ${favorite ? 'active' : ''}" data-flow-template-favorite="${esc(preset.id)}" title="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}" aria-label="${esc(favorite ? tr('flow.templateFavoriteRemove') : tr('flow.templateFavoriteAdd'))}">${favorite ? '★' : '☆'}</button></div>
|
||||
<p>${esc(flowPresetText(preset.description))}</p>
|
||||
<div class="flow-template-preview-stats"><span>${esc(tr('flow.templateNodesCount', { count:preset.flow.nodes.length }))}</span><span>${esc(tr('flow.templateEdgesCount', { count:preset.flow.edges.length }))}</span></div>
|
||||
<div class="flow-template-preview-stats"><span>${esc(tr('flow.templateNodesCount', { count: preset.flow.nodes.length }))}</span><span>${esc(tr('flow.templateEdgesCount', { count: preset.flow.edges.length }))}</span></div>
|
||||
${flowPresetPreviewGraph(preset)}
|
||||
<div class="flow-template-requirements"><strong>${esc(tr('flow.templateRequirements'))}</strong><div>${reqMarkup}</div>${req.missing.length ? `<p class="field-note warning-note">${esc(tr('flow.templateRequirementsMissing', { items:req.missing.join(', ') }))}</p>` : ''}</div>
|
||||
<div class="flow-template-requirements"><strong>${esc(tr('flow.templateRequirements'))}</strong><div>${reqMarkup}</div>${req.missing.length ? `<p class="field-note warning-note">${esc(tr('flow.templateRequirementsMissing', { items: req.missing.join(', ') }))}</p>` : ''}</div>
|
||||
<div class="form-actions"><button type="button" class="primary" data-flow-template-use="${esc(preset.id)}">${esc(tr('flow.templateUse'))}</button></div>`;
|
||||
}
|
||||
|
||||
@@ -864,7 +864,7 @@ function renderFlowPresetBrowser(category = '') {
|
||||
byCategory.set('favorites', presets.filter(preset => favorites.has(preset.id)));
|
||||
byCategory.set('recent', recent.map(id => presets.find(preset => preset.id === id)).filter(Boolean));
|
||||
const normalCategories = [...new Set([...FLOW_PRESET_CATEGORY_ORDER, ...presets.map(preset => preset.category || 'advanced')])].filter(key => (byCategory.get(key) || []).length);
|
||||
const categories = ['favorites','recent', ...normalCategories];
|
||||
const categories = ['favorites', 'recent', ...normalCategories];
|
||||
if (!presets.length) { tabs.innerHTML = ''; host.innerHTML = ''; renderFlowPresetPreview(null); return; }
|
||||
flowPresetActiveCategory = categories.includes(category) ? category : (categories.includes(flowPresetActiveCategory) ? flowPresetActiveCategory : normalCategories[0]);
|
||||
tabs.innerHTML = categories.map(key => `<button type="button" class="flow-template-tab ${key === flowPresetActiveCategory ? 'active' : ''}" role="tab" aria-selected="${key === flowPresetActiveCategory}" data-flow-template-category="${esc(key)}">${esc(flowPresetCategoryLabel(key))} <span class="badge">${(byCategory.get(key) || []).length}</span></button>`).join('');
|
||||
@@ -921,17 +921,17 @@ function applyFlowTemplate(key) {
|
||||
|
||||
function localDateTimeInputValue(date = new Date()) {
|
||||
const pad = value => String(value).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
function flowSimulationOverrideNodes() {
|
||||
return (app.flowDraft?.nodes || []).filter(node => ['outdoor_temperature','device_temperature','zone_temperature','ha_state','ha_numeric','ha_attribute','ha_available','house_mode','device_state','zone_state','group_state','night_mode','shared_input'].includes(node.kind));
|
||||
return (app.flowDraft?.nodes || []).filter(node => ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_state', 'ha_numeric', 'ha_attribute', 'ha_available', 'house_mode', 'device_state', 'zone_state', 'group_state', 'night_mode', 'shared_input'].includes(node.kind));
|
||||
}
|
||||
function renderFlowSimulationOverrides() {
|
||||
const host = $('#flowSimulationOverrides'); if (!host) return;
|
||||
const nodes = flowSimulationOverrideNodes();
|
||||
host.innerHTML = nodes.length ? nodes.map(node => {
|
||||
const effectiveKind = node.kind === 'shared_input' ? (app.flowSharedInputs || []).find(item => item.id === node.config?.input_id)?.kind : node.kind;
|
||||
const numeric = ['outdoor_temperature','device_temperature','zone_temperature','ha_numeric'].includes(effectiveKind);
|
||||
const numeric = ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(effectiveKind);
|
||||
return `<label><span>${esc(flowNodeTitle(FLOW_NODE_META[node.kind]))} · ${esc(flowNodeSummary(node))}</span><input ${numeric ? 'type="number" step="0.1"' : 'type="text"'} data-flow-sim-node="${esc(node.id)}" placeholder="${esc(tr('flow.useLiveValue'))}"></label>`;
|
||||
}).join('') : `<p class="field-note">${esc(tr('flow.noSimulationOverrides'))}</p>`;
|
||||
}
|
||||
@@ -941,7 +941,7 @@ function collectFlowSimulationOverrides() {
|
||||
if (input.value.trim() === '') return;
|
||||
const node = flowNodeById(input.dataset.flowSimNode); let value = input.value.trim();
|
||||
const effectiveKind = node?.kind === 'shared_input' ? (app.flowSharedInputs || []).find(item => item.id === node.config?.input_id)?.kind : node?.kind;
|
||||
if (node && ['outdoor_temperature','device_temperature','zone_temperature','ha_numeric'].includes(effectiveKind)) value = Number(value);
|
||||
if (node && ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(effectiveKind)) value = Number(value);
|
||||
else if (/^(true|false)$/i.test(value)) value = value.toLowerCase() === 'true';
|
||||
result[input.dataset.flowSimNode] = value;
|
||||
});
|
||||
@@ -955,43 +955,43 @@ function openFlowDryRun() {
|
||||
function renderFlowDryRunResult(result) {
|
||||
const host = $('#flowTestResults');
|
||||
const actionName = id => flowNodeById(id) ? `${flowNodeTitle(FLOW_NODE_META[flowNodeById(id).kind])}: ${flowNodeSummary(flowNodeById(id))}` : id;
|
||||
host.innerHTML = `<div class="flow-test-summary"><strong>${esc(result.summary || tr('flow.dryRun'))}</strong><span>${esc(tr('flow.compiledPreview', result.compiled || { schedules:0, automations:0 }))}</span></div>${(result.actions || []).map(action => `<article class="flow-test-action ${action.would_execute ? 'pass' : 'blocked'}"><div><strong>${esc(actionName(action.node_id))}</strong><span class="badge ${action.would_execute ? 'active' : ''}">${esc(action.would_execute ? tr('flow.wouldExecute') : action.matched ? tr('flow.blockedByOwnership') : tr('flow.conditionFalse'))}</span></div>${action.blocked_reason ? `<p>${esc(tr('flow.blockReason'))}: <code>${esc(action.blocked_reason)}</code></p>` : ''}<div class="flow-trace">${(action.trace || []).map(item => `<div><span>${item.matched ? '✓' : '×'} ${esc(flowNodeTitle(FLOW_NODE_META[item.kind] || { title:item.kind }))}</span><code>${esc(JSON.stringify(item.actual))}</code></div>`).join('')}</div></article>`).join('')}`;
|
||||
host.innerHTML = `<div class="flow-test-summary"><strong>${esc(result.summary || tr('flow.dryRun'))}</strong><span>${esc(tr('flow.compiledPreview', result.compiled || { schedules: 0, automations: 0 }))}</span></div>${(result.actions || []).map(action => `<article class="flow-test-action ${action.would_execute ? 'pass' : 'blocked'}"><div><strong>${esc(actionName(action.node_id))}</strong><span class="badge ${action.would_execute ? 'active' : ''}">${esc(action.would_execute ? tr('flow.wouldExecute') : action.matched ? tr('flow.blockedByOwnership') : tr('flow.conditionFalse'))}</span></div>${action.blocked_reason ? `<p>${esc(tr('flow.blockReason'))}: <code>${esc(action.blocked_reason)}</code></p>` : ''}<div class="flow-trace">${(action.trace || []).map(item => `<div><span>${item.matched ? '✓' : '×'} ${esc(flowNodeTitle(FLOW_NODE_META[item.kind] || { title: item.kind }))}</span><code>${esc(JSON.stringify(item.actual))}</code></div>`).join('')}</div></article>`).join('')}`;
|
||||
}
|
||||
async function runFlowDryRun() {
|
||||
if (!app.flowDraft) return;
|
||||
const input = $('#flowSimulationAt').value; const at = input ? new Date(input).toISOString() : new Date().toISOString();
|
||||
$('#flowTestResults').innerHTML = `<p class="field-note">${esc(tr('flow.runningSimulation'))}</p>`;
|
||||
try {
|
||||
const result = await api('/api/flows/simulate', { method:'POST', body:{ flow:flowSourcePayload(), flow_id:app.flowDraft.id || null, at, overrides:collectFlowSimulationOverrides(), log:true } });
|
||||
const result = await api('/api/flows/simulate', { method: 'POST', body: { flow: flowSourcePayload(), flow_id: app.flowDraft.id || null, at, overrides: collectFlowSimulationOverrides(), log: true } });
|
||||
renderFlowDryRunResult(result);
|
||||
} catch (error) { $('#flowTestResults').innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.simulationFailed'))}</strong><span>${esc(error.message)}</span></div>`; }
|
||||
}
|
||||
function flowLogLevelLabel(level = '') {
|
||||
const key = { info:'flow.logLevelInfo', warn:'flow.logLevelWarn', error:'flow.logLevelError' }[String(level).toLowerCase()];
|
||||
const key = { info: 'flow.logLevelInfo', warn: 'flow.logLevelWarn', error: 'flow.logLevelError' }[String(level).toLowerCase()];
|
||||
return key ? tr(key) : level;
|
||||
}
|
||||
function flowLogKindLabel(kind = '') {
|
||||
const key = {
|
||||
'flow.created':'flow.logKindCreated', 'flow.updated':'flow.logKindUpdated', 'flow.deleted':'flow.logKindDeleted',
|
||||
'flow.imported':'flow.logKindImported', 'flow.dry_run':'flow.logKindDryRun', 'flow.condition_error':'flow.logKindConditionError',
|
||||
'flow.action_suppressed':'flow.logKindActionSuppressed', 'automation.fired':'flow.logKindActionFired',
|
||||
'automation.error':'flow.logKindActionError', 'automation.blocked_by_zone':'flow.logKindBlockedZone',
|
||||
'automation.blocked_by_manual_override':'flow.logKindBlockedManual', 'automation.blocked_by_local_thermostat':'flow.logKindBlockedThermostat',
|
||||
'automation.blocked_by_temporary_thermostat':'flow.logKindBlockedTemporary', 'automation.blocked_by_thermostat_owner':'flow.logKindBlockedThermostatOwner',
|
||||
'automation.blocked_by_fresh_ownership':'flow.logKindBlockedOwnership', 'automation.conflict':'flow.logKindConflict',
|
||||
'flow.created': 'flow.logKindCreated', 'flow.updated': 'flow.logKindUpdated', 'flow.deleted': 'flow.logKindDeleted',
|
||||
'flow.imported': 'flow.logKindImported', 'flow.dry_run': 'flow.logKindDryRun', 'flow.condition_error': 'flow.logKindConditionError',
|
||||
'flow.action_suppressed': 'flow.logKindActionSuppressed', 'automation.fired': 'flow.logKindActionFired',
|
||||
'automation.error': 'flow.logKindActionError', 'automation.blocked_by_zone': 'flow.logKindBlockedZone',
|
||||
'automation.blocked_by_manual_override': 'flow.logKindBlockedManual', 'automation.blocked_by_local_thermostat': 'flow.logKindBlockedThermostat',
|
||||
'automation.blocked_by_temporary_thermostat': 'flow.logKindBlockedTemporary', 'automation.blocked_by_thermostat_owner': 'flow.logKindBlockedThermostatOwner',
|
||||
'automation.blocked_by_fresh_ownership': 'flow.logKindBlockedOwnership', 'automation.conflict': 'flow.logKindConflict',
|
||||
}[kind];
|
||||
return key ? tr(key) : kind;
|
||||
}
|
||||
function flowLogMessage(event = {}) {
|
||||
const name = app.flowDraft?.name || tr('nav.flows');
|
||||
const key = {
|
||||
'flow.created':'flow.logMessageCreated', 'flow.updated':'flow.logMessageUpdated', 'flow.deleted':'flow.logMessageDeleted',
|
||||
'flow.imported':'flow.logMessageImported', 'flow.dry_run':'flow.logMessageDryRun', 'flow.condition_error':'flow.logMessageConditionError',
|
||||
'flow.action_suppressed':'flow.logMessageActionSuppressed', 'automation.fired':'flow.logMessageActionFired',
|
||||
'automation.error':'flow.logMessageActionError', 'automation.blocked_by_zone':'flow.logMessageBlockedZone',
|
||||
'automation.blocked_by_manual_override':'flow.logMessageBlockedManual', 'automation.blocked_by_local_thermostat':'flow.logMessageBlockedThermostat',
|
||||
'automation.blocked_by_temporary_thermostat':'flow.logMessageBlockedTemporary', 'automation.blocked_by_thermostat_owner':'flow.logMessageBlockedThermostatOwner',
|
||||
'automation.blocked_by_fresh_ownership':'flow.logMessageBlockedOwnership', 'automation.conflict':'flow.logMessageConflict',
|
||||
'flow.created': 'flow.logMessageCreated', 'flow.updated': 'flow.logMessageUpdated', 'flow.deleted': 'flow.logMessageDeleted',
|
||||
'flow.imported': 'flow.logMessageImported', 'flow.dry_run': 'flow.logMessageDryRun', 'flow.condition_error': 'flow.logMessageConditionError',
|
||||
'flow.action_suppressed': 'flow.logMessageActionSuppressed', 'automation.fired': 'flow.logMessageActionFired',
|
||||
'automation.error': 'flow.logMessageActionError', 'automation.blocked_by_zone': 'flow.logMessageBlockedZone',
|
||||
'automation.blocked_by_manual_override': 'flow.logMessageBlockedManual', 'automation.blocked_by_local_thermostat': 'flow.logMessageBlockedThermostat',
|
||||
'automation.blocked_by_temporary_thermostat': 'flow.logMessageBlockedTemporary', 'automation.blocked_by_thermostat_owner': 'flow.logMessageBlockedThermostatOwner',
|
||||
'automation.blocked_by_fresh_ownership': 'flow.logMessageBlockedOwnership', 'automation.conflict': 'flow.logMessageConflict',
|
||||
}[event.kind];
|
||||
return key ? tr(key, { name }) : (event.message || '—');
|
||||
}
|
||||
@@ -1027,10 +1027,10 @@ async function saveFlow() {
|
||||
await applySavedFlow(saved, 'flow.saved');
|
||||
} catch (error) {
|
||||
if (error.status !== 400) return toast(error.message, true);
|
||||
const saveDraft = confirm(tr('flow.saveAsDraftConfirm', { reason:error.message }));
|
||||
const saveDraft = confirm(tr('flow.saveAsDraftConfirm', { reason: error.message }));
|
||||
if (!saveDraft) return toast(error.message, true);
|
||||
try {
|
||||
const saved = await persistFlowDraft({ ...body, enabled:false, draft:true });
|
||||
const saved = await persistFlowDraft({ ...body, enabled: false, draft: true });
|
||||
await applySavedFlow(saved, 'flow.savedAsDraft');
|
||||
} catch (draftError) { toast(draftError.message, true); }
|
||||
}
|
||||
@@ -1047,9 +1047,9 @@ async function toggleFlowEnabled(id, enabled) {
|
||||
if (flow.draft) return toast(tr('flow.draftCannotEnable'), true);
|
||||
const toggle = $(`[data-action="toggle-flow-enabled"][data-id="${CSS.escape(id)}"]`); if (toggle?.disabled) return;
|
||||
if (toggle) toggle.disabled = true;
|
||||
const body = { name:flow.name, enabled, draft:false, description:flow.description || '', nodes:flow.nodes || [], edges:flow.edges || [], expected_revision:Number(flow.revision || 0) };
|
||||
const body = { name: flow.name, enabled, draft: false, description: flow.description || '', nodes: flow.nodes || [], edges: flow.edges || [], expected_revision: Number(flow.revision || 0) };
|
||||
try {
|
||||
const saved = await api(`/api/flows/${encodeURIComponent(id)}`, { method:'PUT', body });
|
||||
const saved = await api(`/api/flows/${encodeURIComponent(id)}`, { method: 'PUT', body });
|
||||
const index = app.flows.findIndex(item => item.id === id); if (index >= 0) app.flows[index] = saved;
|
||||
renderFlows(); await loadBootstrap(); toast(enabled ? tr('flow.quickEnabled') : tr('flow.quickDisabled'));
|
||||
} catch (error) { renderFlows(); toast(error.message, true); }
|
||||
@@ -1097,22 +1097,22 @@ function updateFlowConfig(input) {
|
||||
}
|
||||
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);
|
||||
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 (['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 (key === 'data_json' && node.kind === 'ha_service_action') { try { node.config.data = JSON.parse(input.value || '{}'); } catch { toast(tr('flow.invalidJson'), true); return; } }
|
||||
else if (key === 'seconds' && ['stable_for','delay'].includes(node.kind)) node.config.seconds = Math.max(1, Number(input.value || 1));
|
||||
else if (key === 'seconds' && ['stable_for', 'delay'].includes(node.kind)) node.config.seconds = Math.max(1, Number(input.value || 1));
|
||||
else if (key === 'min_seconds' && node.kind === 'state_duration') node.config.min_seconds = Math.max(0, Number(input.value || 0));
|
||||
else if (key === 'max_seconds' && node.kind === 'state_duration') node.config.max_seconds = input.value === '' ? null : Math.max(0, Number(input.value));
|
||||
else if (key === 'max_count' && node.kind === 'rate_limit') node.config.max_count = Math.max(1, Math.floor(Number(input.value || 1)));
|
||||
else if (key === 'period_seconds' && node.kind === 'rate_limit') node.config.period_seconds = Math.max(1, Math.floor(Number(input.value || 1)));
|
||||
else if (key === 'window_seconds' && ['rolling_stat','oscillates'].includes(node.kind)) node.config.window_seconds = Math.max(10, Number(input.value || 10));
|
||||
else if (key === 'window_seconds' && ['rolling_stat', 'oscillates'].includes(node.kind)) node.config.window_seconds = Math.max(10, Number(input.value || 10));
|
||||
else if (key === 'value' && node.kind === 'rolling_stat') node.config.value = Number(input.value || 0);
|
||||
else if (key === 'min_span' && node.kind === 'oscillates') node.config.min_span = Math.max(0.001, Number(input.value || 0.001));
|
||||
else if (key === 'min_direction_changes' && node.kind === 'oscillates') node.config.min_direction_changes = Math.max(1, Math.floor(Number(input.value || 1)));
|
||||
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','ha_service_action'].includes(node.kind)) node.config[key] = input.value === '' ? null : Number(input.value);
|
||||
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', 'ha_service_action'].includes(node.kind)) node.config[key] = input.value === '' ? null : Number(input.value);
|
||||
else node.config[key] = input.value;
|
||||
app.flowDirty = true; renderFlowEditor();
|
||||
}
|
||||
@@ -1134,8 +1134,8 @@ document.addEventListener('pointerdown', event => {
|
||||
if (!selected.includes(node.id)) { setFlowSelection(selected, selected[selected.length - 1] || null); event.preventDefault(); return; }
|
||||
} else if (!selected.includes(node.id)) selected = [node.id];
|
||||
app.flowSelectedNodeIds = selected; app.flowSelectedNodeId = node.id; renderFlowEditor();
|
||||
const starts = selected.map(id => flowNodeById(id)).filter(Boolean).map(item => ({ id:item.id, left:Number(item.x || 0), top:Number(item.y || 0) }));
|
||||
flowDrag = { x:event.clientX, y:event.clientY, starts };
|
||||
const starts = selected.map(id => flowNodeById(id)).filter(Boolean).map(item => ({ id: item.id, left: Number(item.x || 0), top: Number(item.y || 0) }));
|
||||
flowDrag = { x: event.clientX, y: event.clientY, starts };
|
||||
nodeEl.setPointerCapture?.(event.pointerId); event.preventDefault();
|
||||
});
|
||||
document.addEventListener('pointermove', event => {
|
||||
@@ -1208,7 +1208,7 @@ document.addEventListener('click', event => {
|
||||
requestAnimationFrame(() => {
|
||||
const target = $('#flowSharedInputsSettings');
|
||||
if (!target) return;
|
||||
target.scrollIntoView({ behavior:'smooth', block:'start' });
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
target.classList.add('is-linked-target');
|
||||
setTimeout(() => target.classList.remove('is-linked-target'), 1800);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user