This commit is contained in:
Mateusz Gruszczyński
2026-09-02 10:27:49 +02:00
parent e50c94b6a3
commit 5e5bd4ad3a
4 changed files with 136 additions and 6 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ const app = {
historyTab: 'overview', historyData: { zones: [], devices: [], sensors: [] }, historyCounts: {},
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyLoading: false,
customChartSeries: [], savedCharts: [], chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {},
controlPlan: null, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, debugFilter: 'all', sensorAliases: {}, flowSharedInputs: [],
controlPlan: null, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, debugFilter: 'all', sensorAliases: {}, flowSharedInputs: [], flowSharedInputValueCache: {}, flowSharedInputValueRequests: {}, flowSharedInputValueTimer: null,
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main', settingsTab: 'app', systemSnapshotAt: Date.now(),
flowDraft: null, flowSelectedNodeId: null, flowSelectedNodeIds: [], flowConnectFrom: null, flowDirty: false,
};
+129 -2
View File
@@ -103,17 +103,144 @@ function openFlowEditor(id = '', { push = true } = {}) {
$('#flowEditor').hidden = false;
document.body.classList.add('flow-editor-open');
renderFlowEditor();
startFlowSharedInputValueRefresh();
if (push) updateBrowserUrl(`/flows/${flow?.id || 'new'}`);
}
function closeFlowEditor({ push = true, force = false } = {}) {
if (!force && app.flowDirty && !confirm(tr('confirm.discardChanges'))) return false;
stopFlowSharedInputValueRefresh();
$('#flowEditor').hidden = true; document.body.classList.remove('flow-editor-open');
app.flowDraft = null; app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowConnectFrom = null; app.flowDirty = false;
if (push) updateBrowserUrl('/flows');
return true;
}
function flowSharedInputValueSignature(item) {
return `${item?.kind || ''}:${JSON.stringify(item?.config || {})}`;
}
function flowSharedInputLocalObservation(item) {
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 === '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 };
}
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 };
}
if (item.kind === 'house_mode') {
const value = app.settings?.house_mode;
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 };
}
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 };
}
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 };
}
if (item.kind === 'night_mode') {
const value = app.controlPlan?.night_mode_active;
return { hasValue:typeof value === 'boolean', value };
}
if (item.kind === 'constant') return { hasValue:true, value:c.value };
return null;
}
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 };
}
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 (item.kind === 'ha_numeric') {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return '—';
const unit = String(observation.unit || '').trim();
return `${Number.isInteger(numeric) ? numeric : Number(numeric.toFixed(2))}${unit ? ` ${unit}` : ''}`;
}
if (item.kind === 'house_mode') return houseModeLabel(String(value));
if (typeof value === 'boolean') return value ? tr('common.yes') : tr('common.no');
if (value === null || value === undefined || value === '') return '—';
return String(value);
}
function renderFlowSharedInputCurrentValues() {
if (!app.flowDraft) return;
$$('[data-flow-shared-current]').forEach(host => {
const item = (app.flowSharedInputs || []).find(value => value.id === host.dataset.flowSharedCurrent);
const text = item ? flowSharedInputCurrentText(item) : '—';
host.title = text;
host.innerHTML = `<span>${esc(tr('flow.sharedInputTestCurrent'))}</span><strong>${esc(text)}</strong>`;
});
}
async function loadFlowSharedInputHaValue(item) {
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;
app.flowSharedInputValueRequests[item.id] = signature;
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 } });
let value = result.state, hasValue = true;
if (item.kind === 'ha_available') value = result.available === true;
else if (item.kind === 'ha_attribute') {
value = result.attributes?.[item.config?.attribute];
hasValue = value !== undefined;
} else if (item.kind === 'ha_numeric') {
value = Number(result.state);
hasValue = Number.isFinite(value);
}
app.flowSharedInputValueCache[item.id] = {
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 };
} finally {
if (app.flowSharedInputValueRequests?.[item.id] === signature) delete app.flowSharedInputValueRequests[item.id];
renderFlowSharedInputCurrentValues();
}
}
function refreshFlowSharedInputCurrentValues() {
if (!app.flowDraft) return;
renderFlowSharedInputCurrentValues();
const ids = new Set((app.flowDraft.nodes || []).filter(node => node.kind === 'shared_input').map(node => node.config?.input_id).filter(Boolean));
ids.forEach(id => {
const item = (app.flowSharedInputs || []).find(value => value.id === id);
if (item) loadFlowSharedInputHaValue(item);
});
}
function startFlowSharedInputValueRefresh() {
stopFlowSharedInputValueRefresh();
refreshFlowSharedInputCurrentValues();
app.flowSharedInputValueTimer = setInterval(refreshFlowSharedInputCurrentValues, 5000);
}
function stopFlowSharedInputValueRefresh() {
if (app.flowSharedInputValueTimer) clearInterval(app.flowSharedInputValueTimer);
app.flowSharedInputValueTimer = null;
}
function flowNodeSummary(node) {
const c = node.config || {};
if (node.kind === 'weekday') return (c.days || []).map(day => tr(`day.${day}`)).join(', ') || '—';
@@ -163,13 +290,13 @@ function renderFlowEditor() {
return `<article class="flow-node flow-node-${esc(meta.category)} ${(app.flowSelectedNodeIds || []).includes(node.id) ? 'selected' : ''}" data-flow-node="${esc(node.id)}" style="left:${Number(node.x || 0)}px;top:${Number(node.y || 0)}px">
<button class="flow-port flow-port-in" type="button" data-flow-input="${esc(node.id)}" title="${esc(tr('flow.connectHere'))}"></button>
<div class="flow-node-head"><span>${esc(flowNodeTitle(meta))}</span><button type="button" data-flow-remove="${esc(node.id)}" aria-label="${esc(tr('actions.delete'))}">×</button></div>
<div class="flow-node-body">${esc(flowNodeSummary(node))}</div>
<div class="flow-node-body">${esc(flowNodeSummary(node))}${node.kind === 'shared_input' && node.config?.input_id ? `<div class="flow-node-current" data-flow-shared-current="${esc(node.config.input_id)}"><span>${esc(tr('flow.sharedInputTestCurrent'))}</span><strong>—</strong></div>` : ''}</div>
<button class="flow-port flow-port-out ${app.flowConnectFrom === node.id ? 'armed' : ''}" type="button" data-flow-output="${esc(node.id)}" title="${esc(tr('flow.startConnection'))}"></button>
</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 }) : '';
renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation();
renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation(); refreshFlowSharedInputCurrentValues();
const status = $('#flowCompileStatus');
status.textContent = tr('flow.compileCount', { schedules: draft.compiled_schedule_ids?.length || 0, automations: draft.compiled_automation_ids?.length || 0 });
}