diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index f2d0d51..69f93dd 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -175,11 +175,11 @@ d6df434f3fbd33ba38dd244830c980e54433c6dff94dd39c716b377255bd2ebf ./web/index.ht 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md 21b662914590e720dafefe4dc3dccc91dbeb963cce9ae91f007c835d2d0c1fe5 ./web/js/bootstrap.js 72fa280996ba331a05bdb29a1f49b4e61bdd8964953c8ce1db952156620f08fe ./web/js/charts.js -7dd04683781137deedc5dfccafcb5a4d15954041f2e5207312816de216904513 ./web/js/core.js +f07600277c60c771c04b1d43c5ed08de6bf518a72660011767b71cde8ec5bde8 ./web/js/core.js dfc123885a9b6b5c1d07c12d2d2625ff15aa0844e5cd3736092ccd0a360da39c ./web/js/dashboard.js 2887856e35e086bb575e9fc0d8b0ed1160b6912297a2957535d5416bda68b4b7 ./web/js/entities.js 9a0e87911f78cd8a969f4c57ce74774f5fa829fdecd79494d9a7f9d5804f9792 ./web/js/events.js -92674d277d12aa2bca0fbd7dffcd6e76f182a2601ae60dbdd33210e906a5fee2 ./web/js/flows.js +707614dc2c48c09b483b39d67af8ec4c5c0423f4f1d59e17402bbef521fedb9d ./web/js/flows.js f06557c6d338259059b990f8f79c91d68ac8ec1f245c302468f08929b5506901 ./web/js/forms.js dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js 7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js @@ -189,6 +189,6 @@ c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/route 34513d9ec2e45c60443dffe028eea74f415b8a29016df6af3377a80a4ac0df5b ./web/js/settings-ui.js aa6a2ba22648de44547efd09f41c4335ebe38ed564f4684e942d7827df40f1a2 ./web/js/settings.js 6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest -cad5c59275ab306b0de88b76dcc7b667a3a83585bd9c64ba0221bde7744dd1a1 ./web/styles.css +91cc0f52bd276a39d11bad6b76674f41c8e88b70f699d02ca71a1c473d1ca557 ./web/styles.css ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js diff --git a/web/js/core.js b/web/js/core.js index 8da2cd5..4ec1847 100644 --- a/web/js/core.js +++ b/web/js/core.js @@ -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, }; diff --git a/web/js/flows.js b/web/js/flows.js index bf33a5b..db57166 100644 --- a/web/js/flows.js +++ b/web/js/flows.js @@ -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 = `${esc(tr('flow.sharedInputTestCurrent'))}${esc(text)}`; + }); +} + +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 `
${esc(flowNodeTitle(meta))}
-
${esc(flowNodeSummary(node))}
+
${esc(flowNodeSummary(node))}${node.kind === 'shared_input' && node.config?.input_id ? `
${esc(tr('flow.sharedInputTestCurrent'))}
` : ''}
`; }).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 }); } diff --git a/web/styles.css b/web/styles.css index ce7c0a6..eeeb8a5 100644 --- a/web/styles.css +++ b/web/styles.css @@ -6379,3 +6379,6 @@ body.flow-editor-open { overflow:hidden; } /* Keep zone hysteresis/source controls aligned without stretching selects. */ .zone-hysteresis-row { align-items:start; } +.flow-editor .flow-node-current { margin-top:6px; padding-top:6px; border-top:1px solid color-mix(in srgb, var(--border) 72%, transparent); display:flex; align-items:baseline; justify-content:space-between; gap:8px; } +.flow-editor .flow-node-current span { color:var(--muted); font-size:.68rem; white-space:nowrap; } +.flow-editor .flow-node-current strong { min-width:0; max-width:72px; color:var(--text); font-size:.76rem; font-weight:700; text-align:right; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }