v0.15.0
This commit is contained in:
Vendored
+1
@@ -66,6 +66,7 @@ async function loadBootstrap(showMessage = false) {
|
||||
app.loading = true;
|
||||
try {
|
||||
const data = await api('/api/bootstrap');
|
||||
app.authTokenSubmitted = false;
|
||||
const hasControlPlan = applyBootstrapSnapshot(data);
|
||||
renderAll();
|
||||
if (!hasControlPlan) scheduleControlPlanLoad(0);
|
||||
|
||||
+47
-2
@@ -26,7 +26,7 @@ const preferredTheme = ['system', 'light', 'dark'].includes(getCookie('gree_cont
|
||||
|
||||
const app = {
|
||||
devices: [], zones: [], groups: [], deviceGroups: [], deviceGroupEnergy: {}, schedules: [], automations: [], flows: [], accessTokens: [], settings: null, system: {}, outdoorTemperature: null,
|
||||
token: localStorage.getItem('gree_controller_token') || '', ws: null, wsTimer: null,
|
||||
token: localStorage.getItem('gree_controller_token') || '', authTokenSubmitted: false, ws: null, wsTimer: null,
|
||||
currentView: 'dashboard', loading: false, bootstrapReloadPending: false, language: preferredLanguage, theme: preferredTheme, connectionStatus: 'connecting',
|
||||
languages: [], translations: {}, locales: {}, defaultLanguage: DEFAULT_LANGUAGE,
|
||||
historyTab: 'overview', historyData: { zones: [], devices: [], sensors: [] }, historyCounts: {},
|
||||
@@ -128,6 +128,50 @@ const deviceCommandFieldLabel = key => {
|
||||
}[key];
|
||||
return translationKey ? tr(translationKey) : key;
|
||||
};
|
||||
const VERTICAL_LOUVER_OPTIONS = Object.freeze([
|
||||
[0, 'louver.off'],
|
||||
[1, 'louver.fullRangeAuto'],
|
||||
[2, 'louver.vertical.fixedTop'],
|
||||
[3, 'louver.vertical.fixedUpperMiddle'],
|
||||
[4, 'louver.vertical.fixedMiddle'],
|
||||
[5, 'louver.vertical.fixedLowerMiddle'],
|
||||
[6, 'louver.vertical.fixedBottom'],
|
||||
[7, 'louver.vertical.swingTop'],
|
||||
[8, 'louver.vertical.swingUpperMiddle'],
|
||||
[9, 'louver.vertical.swingMiddle'],
|
||||
[10, 'louver.vertical.swingLowerMiddle'],
|
||||
[11, 'louver.vertical.swingBottom'],
|
||||
]);
|
||||
const HORIZONTAL_LOUVER_OPTIONS = Object.freeze([
|
||||
[0, 'louver.off'],
|
||||
[1, 'louver.fullRangeAuto'],
|
||||
[2, 'louver.horizontal.fixedLeft'],
|
||||
[3, 'louver.horizontal.fixedLeftMiddle'],
|
||||
[4, 'louver.horizontal.fixedMiddle'],
|
||||
[5, 'louver.horizontal.fixedRightMiddle'],
|
||||
[6, 'louver.horizontal.fixedRight'],
|
||||
]);
|
||||
const normalizedLouverPosition = value => {
|
||||
if (value === null || value === undefined || value === '') return null;
|
||||
if (value === true || String(value).toLowerCase() === 'true') return 1;
|
||||
if (value === false || String(value).toLowerCase() === 'false') return 0;
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : null;
|
||||
};
|
||||
const louverPositionLabel = (axis, value) => {
|
||||
const options = axis === 'horizontal' ? HORIZONTAL_LOUVER_OPTIONS : VERTICAL_LOUVER_OPTIONS;
|
||||
const normalized = normalizedLouverPosition(value);
|
||||
const item = options.find(([raw]) => raw === normalized);
|
||||
return item ? tr(item[1]) : String(value ?? '');
|
||||
};
|
||||
const louverOptions = (axis, selected, { includeNoChange = false } = {}) => {
|
||||
const options = axis === 'horizontal' ? HORIZONTAL_LOUVER_OPTIONS : VERTICAL_LOUVER_OPTIONS;
|
||||
const selectedValue = normalizedLouverPosition(selected);
|
||||
const render = items => items.map(([value, key]) => `<option value="${value}" ${selectedValue === value ? 'selected' : ''}>${esc(tr(key))}</option>`).join('');
|
||||
const noChange = includeNoChange ? `<option value="" ${selectedValue === null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option>` : '';
|
||||
if (axis !== 'vertical') return `${noChange}${render(options)}`;
|
||||
return `${noChange}${render(options.slice(0, 7))}<optgroup label="${esc(tr('louver.advancedSwingRanges'))}">${render(options.slice(7))}</optgroup>`;
|
||||
};
|
||||
const dateTime = value => value ? new Intl.DateTimeFormat(locale(), { dateStyle: 'short', timeStyle: 'short' }).format(new Date(value)) : '—';
|
||||
const localResumeSeconds = value => {
|
||||
const timestamp = value ? new Date(value).getTime() : NaN;
|
||||
@@ -413,9 +457,10 @@ async function api(path, options = {}) {
|
||||
}
|
||||
const response = await fetch(withBase(path), { ...options, headers, body });
|
||||
if (response.status === 401) {
|
||||
const invalidToken = Boolean(app.token);
|
||||
const invalidToken = app.authTokenSubmitted && Boolean(app.token);
|
||||
const message = invalidToken ? tr('auth.invalid') : tr('auth.required');
|
||||
showTokenDialog(invalidToken ? message : '');
|
||||
app.authTokenSubmitted = false;
|
||||
const error = new Error(message);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
|
||||
@@ -130,6 +130,16 @@ function powerIconMarkup() {
|
||||
return uiIcon('power', 'power-icon');
|
||||
}
|
||||
|
||||
function deviceLouverControls(device, { disabled = false } = {}) {
|
||||
if (!device) return '';
|
||||
const caps = device.capabilities || {};
|
||||
const disabledAttr = disabled ? ' disabled' : '';
|
||||
const controls = [];
|
||||
if (caps.vertical_swing !== false) controls.push(`<label class="louver-control"><span>${esc(tr('devices.swingVertical'))}</span><select data-louver-field="swing_vertical" data-device="${esc(device.id)}"${disabledAttr}>${louverOptions('vertical', device.swing_vertical)}</select></label>`);
|
||||
if (caps.horizontal_swing !== false) controls.push(`<label class="louver-control"><span>${esc(tr('devices.swingHorizontal'))}</span><select data-louver-field="swing_horizontal" data-device="${esc(device.id)}"${disabledAttr}>${louverOptions('horizontal', device.swing_horizontal)}</select></label>`);
|
||||
return controls.length ? `<div class="louver-controls">${controls.join('')}</div>` : '';
|
||||
}
|
||||
|
||||
function manualLocalDeviceCard(device) {
|
||||
const caps = device.capabilities || {};
|
||||
const modes = ['auto', 'cool', 'dry', 'fan', 'heat'];
|
||||
@@ -153,9 +163,8 @@ function manualLocalDeviceCard(device) {
|
||||
${compressorQueuePanel(managedZone)}
|
||||
<div class="mode-row quick-control-row quick-control-row-5">${modes.map(mode => `<button class="${device.mode === mode ? 'active' : ''}" data-action="mode" data-value="${mode}" data-device="${esc(device.id)}">${esc(modeLabel(mode))}</button>`).join('')}</div>
|
||||
<div class="fan-row quick-control-row quick-control-row-4">${fans.map(fan => `<button class="${Number(device.fan_speed) === fan ? 'active' : ''}" data-action="fan" data-value="${fan}" data-device="${esc(device.id)}">${esc(fanLabel(fan))}</button>`).join('')}</div>
|
||||
<div class="device-toggles quick-control-row quick-control-row-4">
|
||||
${caps.vertical_swing === false ? '' : `<button class="${device.swing_vertical ? 'active' : ''}" data-action="toggle" data-field="swing_vertical" data-device="${esc(device.id)}">${esc(tr('devices.swingVertical'))}</button>`}
|
||||
${caps.horizontal_swing === false ? '' : `<button class="${device.swing_horizontal ? 'active' : ''}" data-action="toggle" data-field="swing_horizontal" data-device="${esc(device.id)}">${esc(tr('devices.swingHorizontal'))}</button>`}
|
||||
${deviceLouverControls(device)}
|
||||
<div class="device-toggles quick-control-row quick-control-row-2">
|
||||
${device.supports_quiet === false ? '' : `<button class="${device.quiet ? 'active' : ''}" data-action="toggle" data-field="quiet" data-device="${esc(device.id)}">${esc(tr('devices.quiet'))}</button>`}
|
||||
${device.supports_turbo === false ? '' : `<button class="${device.turbo ? 'active' : ''}" data-action="toggle" data-field="turbo" data-device="${esc(device.id)}">${esc(tr('devices.turbo'))}</button>`}
|
||||
</div>
|
||||
@@ -192,9 +201,8 @@ function manualCloudDeviceCard(device) {
|
||||
${compressorQueuePanel(managedZone)}
|
||||
${modes.length ? `<div class="mode-row quick-control-row quick-control-row-5">${modes.map(mode => `<button class="${device.mode === mode ? 'active' : ''}" data-action="mode" data-value="${mode}" data-device="${esc(device.id)}">${esc(modeLabel(mode))}</button>`).join('')}</div>` : ''}
|
||||
${fans.length ? `<div class="fan-row quick-control-row quick-control-row-${Math.min(6, fans.length)}">${fans.map(fan => `<button class="${Number(device.fan_speed) === fan ? 'active' : ''}" data-action="fan" data-value="${fan}" data-device="${esc(device.id)}">${esc(fanLabel(fan))}</button>`).join('')}</div>` : ''}
|
||||
<div class="device-toggles quick-control-row quick-control-row-4">
|
||||
${caps.vertical_swing === false ? '' : `<button class="${device.swing_vertical ? 'active' : ''}" data-action="toggle" data-field="swing_vertical" data-device="${esc(device.id)}">${esc(tr('devices.swingVertical'))}</button>`}
|
||||
${caps.horizontal_swing === false ? '' : `<button class="${device.swing_horizontal ? 'active' : ''}" data-action="toggle" data-field="swing_horizontal" data-device="${esc(device.id)}">${esc(tr('devices.swingHorizontal'))}</button>`}
|
||||
${deviceLouverControls(device)}
|
||||
<div class="device-toggles quick-control-row quick-control-row-2">
|
||||
${device.supports_quiet === false ? '' : `<button class="${device.quiet ? 'active' : ''}" data-action="toggle" data-field="quiet" data-device="${esc(device.id)}">${esc(tr('devices.quiet'))}</button>`}
|
||||
${device.supports_turbo === false ? '' : `<button class="${device.turbo ? 'active' : ''}" data-action="toggle" data-field="turbo" data-device="${esc(device.id)}">${esc(tr('devices.turbo'))}</button>`}
|
||||
</div>
|
||||
@@ -564,7 +572,7 @@ function zoneCard(zone, detailed = true) {
|
||||
${['auto', 'comfort', 'sleep', 'away'].map(preset => `<button class="${manual === preset ? 'active' : ''}" data-action="zone-preset" data-id="${esc(zone.id)}" data-value="${preset}">${esc(preset === 'sleep' ? tr('zones.sleepNow') : zonePresetLabel(preset))}</button>`).join('')}
|
||||
</div>
|
||||
<div class="mode-row zone-mode-row quick-control-row quick-control-row-3"><button class="${mode === 'house' && globalModeAvailable ? 'active' : ''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="house"${globalModeDisabled}>${esc(tr('zones.followHouseShort'))}</button><button class="${mode === 'heat' ? 'active' : ''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="heat">${esc(modeLabel('heat'))}</button><button class="${mode === 'cool' ? 'active' : ''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="cool">${esc(modeLabel('cool'))}</button></div>
|
||||
${device ? `<div class="device-toggles thermostat-swing-row quick-control-row quick-control-row-2">${device.capabilities?.vertical_swing === false ? '' : `<button class="${device.swing_vertical ? 'active' : ''}" data-action="zone-swing" data-field="swing_vertical" data-device="${esc(device.id)}"${deviceUnavailable ? ' disabled' : ''}>${esc(tr('devices.swingVertical'))}</button>`}${device.capabilities?.horizontal_swing === false ? '' : `<button class="${device.swing_horizontal ? 'active' : ''}" data-action="zone-swing" data-field="swing_horizontal" data-device="${esc(device.id)}"${deviceUnavailable ? ' disabled' : ''}>${esc(tr('devices.swingHorizontal'))}</button>`}</div>` : ''}
|
||||
${device ? deviceLouverControls(device, { disabled: deviceUnavailable }) : ''}
|
||||
<div class="zone-state-line"><span class="zone-runtime-status" title="${esc(runtimeStatus)}">${esc(runtimeStatus)}${controlGroup ? ` <b class="group-control-tag">${esc(controlGroup.name)}</b>` : ''}</span><span>${esc(override)}</span></div>
|
||||
<div class="zone-state-line control-owner-line"><span><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}</span><span>${esc(zoneControlOwnerMeta(zone))}</span></div>
|
||||
${compressorQueuePanel(zone)}
|
||||
|
||||
@@ -8,6 +8,15 @@ document.addEventListener('keydown', event => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('change', async event => {
|
||||
const select = event.target.closest?.('[data-louver-field][data-device]');
|
||||
if (!select) return;
|
||||
const value = Number(select.value);
|
||||
if (!Number.isInteger(value)) return;
|
||||
await sendDeviceCommand(select.dataset.device, { [select.dataset.louverField]: value });
|
||||
});
|
||||
|
||||
document.addEventListener('click', async event => {
|
||||
const inlineTarget = event.target.closest('[data-temperature-kind]');
|
||||
if (inlineTarget && !event.target.closest('button')) { beginInlineTemperatureEdit(inlineTarget); return; }
|
||||
@@ -194,7 +203,6 @@ document.addEventListener('click', async event => {
|
||||
if (action === 'cancel-compressor-task') return cancelCompressorTask(button.dataset.id);
|
||||
if (action === 'cancel-all-compressor-tasks') { if (!confirm(tr('zones.queueCancelAllConfirm', { count: app.zones.filter(zone => !!zone.compressor_pending_action).length }))) return; return cancelAllCompressorTasks(); }
|
||||
if (action === 'zone-device-power') return sendZoneLocalPower(button.dataset.id, button.dataset.value === 'true');
|
||||
if (action === 'zone-swing' && device) return sendDeviceCommand(device.id, current => ({ [button.dataset.field]: !current?.[button.dataset.field] }));
|
||||
if (action === 'zone-open-temporary') return populateTemporaryThermostat(button.dataset.id);
|
||||
if (action === 'zone-temperature') { const zone = app.zones.find(v => v.id === button.dataset.id); if (zone) { const base = Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint); queueZoneTemperature(zone, base + Number(button.dataset.delta)); } return; }
|
||||
if (action === 'zone-mode') return sendZoneControl(button.dataset.id, { mode: button.dataset.value });
|
||||
@@ -349,6 +357,7 @@ $('#tokenForm').addEventListener('submit', async event => {
|
||||
setFormBusy(form, true, 'auth.connecting');
|
||||
app.token = new FormData(form).get('token').trim();
|
||||
localStorage.setItem('gree_controller_token', app.token);
|
||||
app.authTokenSubmitted = true;
|
||||
if (app.ws) app.ws.close();
|
||||
try { await loadBootstrap(); }
|
||||
finally { setFormBusy(form, false); }
|
||||
@@ -634,7 +643,7 @@ $('#scheduleForm').addEventListener('submit', async event => {
|
||||
$('#automationForm').addEventListener('submit', async event => {
|
||||
event.preventDefault(); const form = event.currentTarget, raw = Object.fromEntries(new FormData(form)); const id = raw.id;
|
||||
const groupTarget = raw.action_target_kind === 'group';
|
||||
const action = {}; if (raw.action_power !== '') action.power = raw.action_power === 'true'; if (raw.action_mode) action.mode = raw.action_mode; if (!groupTarget && raw.action_target_temperature !== '') action.target_temperature = parseDecimal(raw.action_target_temperature); if (!groupTarget && raw.action_swing_vertical !== '') action.swing_vertical = raw.action_swing_vertical === 'true'; if (!groupTarget && raw.action_swing_horizontal !== '') action.swing_horizontal = raw.action_swing_horizontal === 'true';
|
||||
const action = {}; if (raw.action_power !== '') action.power = raw.action_power === 'true'; if (raw.action_mode) action.mode = raw.action_mode; if (!groupTarget && raw.action_target_temperature !== '') action.target_temperature = parseDecimal(raw.action_target_temperature); if (!groupTarget && raw.action_swing_vertical !== '') action.swing_vertical = Number(raw.action_swing_vertical); if (!groupTarget && raw.action_swing_horizontal !== '') action.swing_horizontal = Number(raw.action_swing_horizontal);
|
||||
const body = { name: raw.name, enabled: form.enabled.checked, trigger_kind: raw.trigger_kind, trigger_device_id: raw.trigger_device_id || null, threshold: raw.threshold === '' ? null : parseDecimal(raw.threshold), at_time: raw.at_time || null, action_device_id: groupTarget ? '' : raw.action_device_id, action_group_id: groupTarget ? (raw.action_group_id || null) : null, action_preset: groupTarget ? (raw.action_preset || null) : null, action, cooldown_seconds: Number(raw.cooldown_seconds) };
|
||||
await runFormTask(form, async () => {
|
||||
await api(id ? `/api/automations/${encodeURIComponent(id)}` : '/api/automations', { method: id ? 'PUT' : 'POST', body }); form.closest('dialog').close(); form.reset(); await loadBootstrap(); toast(tr('common.saved'));
|
||||
|
||||
+59
-10
@@ -42,6 +42,13 @@ function sharedFlowInputRequiresComparison(kind) {
|
||||
return ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_state', 'ha_numeric', 'ha_attribute', 'house_mode', 'device_state', 'zone_state', 'group_state'].includes(kind);
|
||||
}
|
||||
|
||||
function sharedFlowInputLouverAxis(item) {
|
||||
if (item?.kind !== 'device_state') return null;
|
||||
if (item.config?.field === 'swing_vertical') return 'vertical';
|
||||
if (item.config?.field === 'swing_horizontal') return 'horizontal';
|
||||
return null;
|
||||
}
|
||||
|
||||
function sharedFlowReferenceComparisonDefaults(item) {
|
||||
const kind = item?.kind || '';
|
||||
if (['outdoor_temperature', 'device_temperature', 'zone_temperature'].includes(kind)) return { operator: 'lt', value: 20 };
|
||||
@@ -49,6 +56,7 @@ function sharedFlowReferenceComparisonDefaults(item) {
|
||||
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 (sharedFlowInputLouverAxis(item)) return { operator: 'eq', value: 0 };
|
||||
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' };
|
||||
@@ -225,6 +233,8 @@ function flowSharedInputObservation(item) {
|
||||
function flowSharedInputCurrentText(item, observation = flowSharedInputObservation(item)) {
|
||||
if (!observation?.hasValue) return '—';
|
||||
const value = observation.value;
|
||||
const louverAxis = sharedFlowInputLouverAxis(item);
|
||||
if (louverAxis) return louverPositionLabel(louverAxis, value);
|
||||
if (['outdoor_temperature', 'device_temperature', 'zone_temperature'].includes(item.kind)) return fmtTemp(value);
|
||||
if (item.kind === 'ha_numeric') {
|
||||
const numeric = Number(value);
|
||||
@@ -321,7 +331,11 @@ function flowNodeSummary(node) {
|
||||
if (node.kind === 'ha_attribute') return `${c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id'}.${c.attribute || 'attribute'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
|
||||
if (node.kind === 'ha_available') return `${c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id'} · ${tr('flow.available')}`;
|
||||
if (node.kind === 'house_mode') return `${tr('flow.houseMode')} ${flowOperatorLabel(c.operator)} ${c.value || '—'}`;
|
||||
if (node.kind === 'device_state') return `${app.devices.find(d => d.id === c.device_id)?.name || tr('common.noDevice')} · ${c.field || 'state'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
|
||||
if (node.kind === 'device_state') {
|
||||
const axis = c.field === 'swing_vertical' ? 'vertical' : c.field === 'swing_horizontal' ? 'horizontal' : null;
|
||||
const value = axis ? louverPositionLabel(axis, c.value) : (c.value ?? '—');
|
||||
return `${app.devices.find(d => d.id === c.device_id)?.name || tr('common.noDevice')} · ${deviceCommandFieldLabel(c.field || 'state')} ${flowOperatorLabel(c.operator)} ${value}`;
|
||||
}
|
||||
if (node.kind === 'zone_state') return `${app.zones.find(z => z.id === c.zone_id)?.name || tr('common.noZone')} · ${c.field || 'state'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
|
||||
if (node.kind === 'group_state') return `${app.groups.find(g => g.id === c.group_id)?.name || tr('groups.group')} · ${c.field || 'power_enabled'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
|
||||
if (node.kind === 'night_mode') return tr('flow.nightModeActive');
|
||||
@@ -330,8 +344,10 @@ function flowNodeSummary(node) {
|
||||
const item = (app.flowSharedInputs || []).find(value => value.id === c.input_id);
|
||||
if (!item) return tr('flow.sharedInputMissing');
|
||||
if (sharedFlowInputRequiresComparison(item.kind)) {
|
||||
const axis = sharedFlowInputLouverAxis(item);
|
||||
const value = axis ? louverPositionLabel(axis, c.value) : (c.value ?? '—');
|
||||
return c.operator
|
||||
? `${item.name} · ${sharedFlowInputSourceSummary(item)} · ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`
|
||||
? `${item.name} · ${sharedFlowInputSourceSummary(item)} · ${flowOperatorLabel(c.operator)} ${value}`
|
||||
: `${item.name} · ${tr('flow.selectOperator')}`;
|
||||
}
|
||||
return `${item.name} · ${sharedFlowInputSourceSummary(item)}`;
|
||||
@@ -481,12 +497,14 @@ function sharedFlowReferenceComparisonFields(item, config) {
|
||||
if (!item || !sharedFlowInputRequiresComparison(item.kind)) return '';
|
||||
const defaults = sharedFlowReferenceComparisonDefaults(item);
|
||||
const selected = config.operator || '';
|
||||
const louverAxis = sharedFlowInputLouverAxis(item);
|
||||
const numeric = ['outdoor_temperature', 'device_temperature', 'zone_temperature', 'ha_numeric'].includes(item.kind);
|
||||
const fullOperators = numeric || item.kind === 'ha_attribute';
|
||||
const operatorOptions = `${!selected ? `<option value="" selected disabled>${esc(tr('flow.selectOperator'))}</option>` : ''}${fullOperators ? flowOperatorOptions(selected) : [['eq', '='], ['neq', '≠']].map(([value, label]) => `<option value="${value}" ${value === selected ? 'selected' : ''}>${label}</option>`).join('')}`;
|
||||
const value = config.value ?? defaults.value ?? '';
|
||||
let valueField = `<input data-flow-config="value" value="${esc(value)}">`;
|
||||
if (numeric) valueField = `<input type="number" step="0.1" data-flow-config="value" value="${Number(value ?? 0)}">`;
|
||||
if (louverAxis) valueField = `<select data-flow-config="value">${louverOptions(louverAxis, value)}</select>`;
|
||||
else if (numeric) valueField = `<input type="number" step="0.1" data-flow-config="value" value="${Number(value ?? 0)}">`;
|
||||
else if (item.kind === 'house_mode') valueField = `<select data-flow-config="value">${['cool', 'heat', 'off'].map(v => `<option value="${v}" ${String(value) === v ? 'selected' : ''}>${esc(v === 'off' ? tr('common.off') : tr(`mode.${v}`))}</option>`).join('')}</select>`;
|
||||
return `<p class="field-note">${esc(tr('flow.sharedInputFlowComparisonHint'))}</p><div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${operatorOptions}</select></label><label><span>${esc(tr('flow.value'))}</span>${valueField}</label></div>`;
|
||||
}
|
||||
@@ -516,7 +534,7 @@ function renderFlowInspector() {
|
||||
else if (node.kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" 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" list="haEntitySuggestions" 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(deviceCommandFieldLabel(v))}</option>`).join('')}</select></label>${flowTextComparisonFields(c)}`;
|
||||
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(deviceCommandFieldLabel(v))}</option>`).join('')}</select></label>${['swing_vertical', 'swing_horizontal'].includes(c.field) ? flowLouverComparisonFields(c, c.field === 'swing_horizontal' ? 'horizontal' : 'vertical') : 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>`;
|
||||
@@ -530,7 +548,7 @@ 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.1" 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><details class="flow-device-options"><summary>${esc(tr('flow.thermostatDeviceOptions'))}</summary><div class="two">${flowOptionalBoolField(c, 'swing_vertical')}${flowOptionalBoolField(c, 'swing_horizontal')}</div><p class="field-note">${esc(tr('flow.thermostatDeviceOptionsHint'))}</p></details><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.1" 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><details class="flow-device-options"><summary>${esc(tr('flow.thermostatDeviceOptions'))}</summary><div class="two">${flowOptionalLouverField(c, 'swing_vertical', 'vertical')}${flowOptionalLouverField(c, 'swing_horizontal', 'horizontal')}</div><p class="field-note">${esc(tr('flow.thermostatDeviceOptionsHint'))}</p></details><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" list="haEntitySuggestions" 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>`;
|
||||
@@ -538,10 +556,18 @@ function renderFlowInspector() {
|
||||
}
|
||||
|
||||
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 flowLouverComparisonFields(c, axis) {
|
||||
const options = [['eq', '='], ['neq', '≠']].map(([v, l]) => `<option value="${v}" ${v === (c.operator || 'eq') ? 'selected' : ''}>${l}</option>`).join('');
|
||||
const value = Number.isInteger(Number(c.value)) ? Number(c.value) : 0;
|
||||
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><select data-flow-config="value">${louverOptions(axis, value)}</select></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(deviceCommandFieldLabel(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 flowOptionalLouverField(c, key, axis) {
|
||||
return `<label><span>${esc(deviceCommandFieldLabel(key))}</span><select data-flow-config="${esc(key)}">${louverOptions(axis, c[key], { includeNoChange: true })}</select></label>`;
|
||||
}
|
||||
function flowActionFields(c, group) {
|
||||
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('');
|
||||
@@ -549,7 +575,7 @@ function flowActionFields(c, group) {
|
||||
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.1" 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>${esc(deviceCommandFieldLabel('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>${esc(deviceCommandFieldLabel('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>${flowOptionalLouverField(c, 'swing_vertical', 'vertical')}${flowOptionalLouverField(c, 'swing_horizontal', '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>`;
|
||||
}
|
||||
|
||||
@@ -954,13 +980,28 @@ function localDateTimeInputValue(date = new Date()) {
|
||||
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));
|
||||
}
|
||||
function flowSimulationLouverAxis(node) {
|
||||
if (node?.kind === 'device_state') {
|
||||
if (node.config?.field === 'swing_vertical') return 'vertical';
|
||||
if (node.config?.field === 'swing_horizontal') return 'horizontal';
|
||||
}
|
||||
if (node?.kind === 'shared_input') {
|
||||
const item = (app.flowSharedInputs || []).find(value => value.id === node.config?.input_id);
|
||||
return sharedFlowInputLouverAxis(item);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
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 louverAxis = flowSimulationLouverAxis(node);
|
||||
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>`;
|
||||
const control = louverAxis
|
||||
? `<select data-flow-sim-node="${esc(node.id)}"><option value="">${esc(tr('flow.useLiveValue'))}</option>${louverOptions(louverAxis, null)}</select>`
|
||||
: `<input ${numeric ? 'type="number" step="0.1"' : 'type="text"'} data-flow-sim-node="${esc(node.id)}" placeholder="${esc(tr('flow.useLiveValue'))}">`;
|
||||
return `<label><span>${esc(flowNodeTitle(FLOW_NODE_META[node.kind]))} · ${esc(flowNodeSummary(node))}</span>${control}</label>`;
|
||||
}).join('') : `<p class="field-note">${esc(tr('flow.noSimulationOverrides'))}</p>`;
|
||||
}
|
||||
function collectFlowSimulationOverrides() {
|
||||
@@ -969,7 +1010,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 && (flowSimulationLouverAxis(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;
|
||||
});
|
||||
@@ -1125,10 +1166,18 @@ 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 = Boolean(sharedFlowInputLouverAxis(item)) || ['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 (node.kind === 'device_state' && key === 'field') {
|
||||
const wasLouver = ['swing_vertical', 'swing_horizontal'].includes(node.config.field);
|
||||
node.config.field = input.value;
|
||||
if (['swing_vertical', 'swing_horizontal'].includes(input.value)) node.config.value = 0;
|
||||
else if (wasLouver) node.config.value = '';
|
||||
}
|
||||
else if (node.kind === 'device_state' && key === 'value' && ['swing_vertical', 'swing_horizontal'].includes(node.config.field)) node.config.value = Number(input.value);
|
||||
else if (['swing_vertical', 'swing_horizontal'].includes(key)) node.config[key] = input.value === '' ? null : Number(input.value);
|
||||
else if (['power', '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));
|
||||
|
||||
@@ -87,7 +87,7 @@ function sharedFlowInputSourceSummary(item) {
|
||||
if (item.kind === 'ha_state' || item.kind === 'ha_numeric' || item.kind === 'ha_available') return c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id';
|
||||
if (item.kind === 'ha_attribute') return `${c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id'}.${c.attribute || 'attribute'}`;
|
||||
if (item.kind === 'house_mode') return tr('flow.houseMode');
|
||||
if (item.kind === 'device_state') return `${app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice')} · ${c.field || 'state'}`;
|
||||
if (item.kind === 'device_state') return `${app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice')} · ${deviceCommandFieldLabel(c.field || 'state')}`;
|
||||
if (item.kind === 'zone_state') return `${app.zones.find(value => value.id === c.zone_id)?.name || tr('common.noZone')} · ${c.field || 'state'}`;
|
||||
if (item.kind === 'group_state') return `${app.groups.find(value => value.id === c.group_id)?.name || tr('groups.group')} · ${c.field || 'power_enabled'}`;
|
||||
return flowNodeSummary({ kind: item.kind, config: c });
|
||||
|
||||
Reference in New Issue
Block a user