v0.10.0
This commit is contained in:
+1
-1
@@ -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
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user