|
|
|
@@ -29,6 +29,7 @@ const FLOW_NODE_META = Object.freeze({
|
|
|
|
|
logic_not: { titleKey: 'flow.node.not', category: 'logic' },
|
|
|
|
|
zone_thermostat: { titleKey: 'flow.node.thermostat', category: 'action' },
|
|
|
|
|
device_action: { titleKey: 'flow.node.greeDevice', category: 'action' },
|
|
|
|
|
device_feature_action: { titleKey: 'flow.node.greeFeature', category: 'action' },
|
|
|
|
|
group_action: { titleKey: 'flow.node.group', category: 'action' },
|
|
|
|
|
ha_service_action: { titleKey: 'flow.node.haServiceAction', category: 'haaction' },
|
|
|
|
|
});
|
|
|
|
@@ -97,6 +98,7 @@ function flowDefaultConfig(kind) {
|
|
|
|
|
if (kind === 'shared_input') return sharedFlowReferenceDefaultConfig(app.flowSharedInputs?.[0]);
|
|
|
|
|
if (kind === 'zone_thermostat') return { zone_id: app.zones[0]?.id || '', preset: 'comfort', setpoint: 21, mode: 'auto', power: null, swing_vertical: null, swing_horizontal: null, cooldown_seconds: 60 };
|
|
|
|
|
if (kind === 'device_action') return { device_id: app.devices[0]?.id || '', power: true, mode: '', target_temperature: null, fan_speed: null, swing_vertical: null, swing_horizontal: null, quiet: null, turbo: null, light: null, air: null, xfan: null, health: null, sleep: null, cooldown_seconds: 60 };
|
|
|
|
|
if (kind === 'device_feature_action') return { device_id: app.devices[0]?.id || '', feature: 'light', value: true, cooldown_seconds: 60 };
|
|
|
|
|
if (kind === 'group_action') return { group_id: app.groups[0]?.id || '', power: true, mode: '', preset: '', setpoint: 21, cooldown_seconds: 60 };
|
|
|
|
|
if (kind === 'ha_service_action') return { domain: 'switch', service: 'turn_on', entity_id: '', data: {}, cooldown_seconds: 60 };
|
|
|
|
|
return {};
|
|
|
|
@@ -357,6 +359,7 @@ function flowNodeSummary(node) {
|
|
|
|
|
if (node.kind === 'logic_not') return tr('flow.invertCondition');
|
|
|
|
|
if (node.kind === 'zone_thermostat') { const zone = app.zones.find(z => z.id === c.zone_id); return `${zone?.name || tr('common.noZone')} · ${c.preset === 'custom' ? fmtTemp(c.setpoint) : zonePresetLabel(c.preset || 'comfort')}`; }
|
|
|
|
|
if (node.kind === 'device_action') { const d = app.devices.find(v => v.id === c.device_id); return `${d?.name || tr('common.noDevice')} · ${c.power == null ? tr('actions.noChange') : c.power ? tr('common.on') : tr('common.off')}`; }
|
|
|
|
|
if (node.kind === 'device_feature_action') { const d = app.devices.find(v => v.id === c.device_id); return `${d?.name || tr('common.noDevice')} · ${deviceCommandFieldLabel(c.feature || 'light')} → ${flowDeviceFeatureValueLabel(c.feature, c.value)}`; }
|
|
|
|
|
if (node.kind === 'group_action') { const g = app.groups.find(v => v.id === c.group_id); return `${g?.name || tr('groups.group')} · ${c.power == null ? tr('actions.noChange') : c.power ? tr('common.on') : tr('common.off')}`; }
|
|
|
|
|
if (node.kind === 'ha_service_action') return `${c.domain || 'domain'}.${c.service || 'service'} · ${c.entity_id || 'entity_id'}`;
|
|
|
|
|
return '';
|
|
|
|
@@ -550,6 +553,7 @@ function renderFlowInspector() {
|
|
|
|
|
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">${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 === 'device_feature_action') 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.deviceFeature'))}</span><select data-flow-config="feature">${flowDeviceFeatureOptions(c.feature)}</select></label>${flowDeviceFeatureValueField(c)}<p class="field-note">${esc(tr('flow.deviceFeatureHint'))}</p><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 === '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>`;
|
|
|
|
|
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'))}">${uiIcon('expand-vertical')}</button><button type="button" class="secondary flow-inspector-close" data-action="flow-clear-selection" aria-label="${esc(tr('flow.clearSelection'))}">${uiIcon('close')}</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>`;
|
|
|
|
@@ -562,6 +566,40 @@ function flowLouverComparisonFields(c, axis) {
|
|
|
|
|
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>`; }
|
|
|
|
|
const FLOW_DEVICE_FEATURES = Object.freeze(['power', 'mode', 'target_temperature', 'fan_speed', 'swing_vertical', 'swing_horizontal', 'quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep']);
|
|
|
|
|
const FLOW_DEVICE_BOOL_FEATURES = Object.freeze(['power', 'quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep']);
|
|
|
|
|
function flowDeviceFeatureDefaultValue(feature) {
|
|
|
|
|
if (FLOW_DEVICE_BOOL_FEATURES.includes(feature)) return true;
|
|
|
|
|
if (feature === 'mode') return 'auto';
|
|
|
|
|
if (feature === 'target_temperature') return 22;
|
|
|
|
|
if (feature === 'fan_speed') return 0;
|
|
|
|
|
if (['swing_vertical', 'swing_horizontal'].includes(feature)) return 1;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
function flowDeviceFeatureOptions(selected) {
|
|
|
|
|
return FLOW_DEVICE_FEATURES.map(feature => `<option value="${feature}" ${feature === selected ? 'selected' : ''}>${esc(deviceCommandFieldLabel(feature))}</option>`).join('');
|
|
|
|
|
}
|
|
|
|
|
function flowDeviceFeatureValueLabel(feature, value) {
|
|
|
|
|
if (FLOW_DEVICE_BOOL_FEATURES.includes(feature)) return value === false ? tr('common.off') : tr('common.on');
|
|
|
|
|
if (feature === 'mode') return modeLabel(value || 'auto');
|
|
|
|
|
if (feature === 'target_temperature') return fmtTemp(value);
|
|
|
|
|
if (feature === 'fan_speed') return fanLabel(Number(value));
|
|
|
|
|
if (feature === 'swing_vertical') return louverPositionLabel('vertical', value);
|
|
|
|
|
if (feature === 'swing_horizontal') return louverPositionLabel('horizontal', value);
|
|
|
|
|
return value ?? '—';
|
|
|
|
|
}
|
|
|
|
|
function flowDeviceFeatureValueField(c) {
|
|
|
|
|
const feature = c.feature || 'light';
|
|
|
|
|
const value = c.value ?? flowDeviceFeatureDefaultValue(feature);
|
|
|
|
|
if (FLOW_DEVICE_BOOL_FEATURES.includes(feature)) return `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value"><option value="true" ${value === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${value === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label>`;
|
|
|
|
|
if (feature === 'mode') return `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value">${['auto', 'cool', 'dry', 'fan', 'heat'].map(mode => `<option value="${mode}" ${value === mode ? 'selected' : ''}>${esc(modeLabel(mode))}</option>`).join('')}</select></label>`;
|
|
|
|
|
if (feature === 'target_temperature') return `<label><span>${esc(tr('flow.value'))}</span><input type="number" min="8" max="30" step="0.5" data-flow-config="value" value="${Number(value)}"></label>`;
|
|
|
|
|
if (feature === 'fan_speed') return `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value">${[0, 1, 2, 3, 4, 5].map(v => `<option value="${v}" ${Number(value) === v ? 'selected' : ''}>${esc(fanLabel(v))}</option>`).join('')}</select></label>`;
|
|
|
|
|
if (feature === 'swing_vertical') return `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value">${louverOptions('vertical', value)}</select></label>`;
|
|
|
|
|
if (feature === 'swing_horizontal') return `<label><span>${esc(tr('flow.value'))}</span><select data-flow-config="value">${louverOptions('horizontal', value)}</select></label>`;
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>`;
|
|
|
|
|
}
|
|
|
|
@@ -1169,6 +1207,12 @@ function updateFlowConfig(input) {
|
|
|
|
|
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 (node.kind === 'device_feature_action' && key === 'feature') { node.config.feature = input.value; node.config.value = flowDeviceFeatureDefaultValue(input.value); }
|
|
|
|
|
else if (node.kind === 'device_feature_action' && key === 'value') {
|
|
|
|
|
if (FLOW_DEVICE_BOOL_FEATURES.includes(node.config.feature)) node.config.value = input.value === 'true';
|
|
|
|
|
else if (['target_temperature', 'fan_speed', 'swing_vertical', 'swing_horizontal'].includes(node.config.feature)) node.config.value = Number(input.value);
|
|
|
|
|
else node.config.value = input.value;
|
|
|
|
|
}
|
|
|
|
|
else if (node.kind === 'device_state' && key === 'field') {
|
|
|
|
|
const wasLouver = ['swing_vertical', 'swing_horizontal'].includes(node.config.field);
|
|
|
|
|
node.config.field = input.value;
|
|
|
|
@@ -1189,7 +1233,7 @@ function updateFlowConfig(input) {
|
|
|
|
|
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', 'device_feature_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();
|
|
|
|
|
}
|
|
|
|
|