v0.14.13
This commit is contained in:
+150
-15
@@ -34,7 +34,11 @@ function knownHaEntities() {
|
||||
|
||||
function renderHaEntitySuggestions() {
|
||||
const host = $('#haEntitySuggestions'); if (!host) return;
|
||||
host.innerHTML = knownHaEntities().map(entity => {
|
||||
const entities = [...new Set([
|
||||
...knownHaEntities(),
|
||||
...(app.haEntityCatalog || []).map(item => item?.entity_id).filter(Boolean),
|
||||
])].sort();
|
||||
host.innerHTML = entities.map(entity => {
|
||||
const alias = haSensorLabel(entity);
|
||||
return `<option value="${esc(entity)}" label="${esc(alias === entity ? '' : alias)}"></option>`;
|
||||
}).join('');
|
||||
@@ -116,15 +120,107 @@ function sharedFlowOptions(items, selected, label = item => item.name) {
|
||||
return items.map(item => `<option value="${esc(item.id)}" ${item.id === selected ? 'selected' : ''}>${esc(label(item))}</option>`).join('');
|
||||
}
|
||||
|
||||
|
||||
function haEntityMatchesSharedKind(entity, kind) {
|
||||
if (!entity?.entity_id) return false;
|
||||
if (kind === 'ha_numeric') { const state = String(entity.state ?? '').trim(); return state !== '' && Number.isFinite(Number(state)); }
|
||||
return true;
|
||||
}
|
||||
|
||||
function haEntityPickerResults(kind, query = '') {
|
||||
const needle = String(query || '').trim().toLocaleLowerCase();
|
||||
return (app.haEntityCatalog || [])
|
||||
.filter(entity => haEntityMatchesSharedKind(entity, kind))
|
||||
.filter(entity => {
|
||||
if (!needle) return true;
|
||||
return [entity.entity_id, entity.name, entity.state, entity.unit, entity.device_class]
|
||||
.some(value => String(value || '').toLocaleLowerCase().includes(needle));
|
||||
})
|
||||
.slice(0, 12);
|
||||
}
|
||||
|
||||
function renderHaEntityPickerResults(input, kind) {
|
||||
const picker = input?.closest?.('[data-ha-entity-picker]');
|
||||
const host = picker?.querySelector?.('[data-ha-entity-results]');
|
||||
const note = picker?.querySelector?.('[data-ha-entity-note]');
|
||||
if (!host || !note) return;
|
||||
|
||||
if (app.haEntityCatalogLoading) {
|
||||
note.textContent = tr('flow.haEntitySearchLoading');
|
||||
host.hidden = true;
|
||||
return;
|
||||
}
|
||||
if (!app.haEntityCatalogConfigured) {
|
||||
note.textContent = tr('flow.haEntitySearchConnect');
|
||||
host.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const items = haEntityPickerResults(kind, input.value);
|
||||
note.textContent = tr('flow.haEntitySearchCount', { count: app.haEntityCatalog.length });
|
||||
host.innerHTML = items.length ? items.map(entity => {
|
||||
const title = entity.name || entity.entity_id;
|
||||
const state = `${entity.state || '—'}${entity.unit ? ` ${entity.unit}` : ''}`;
|
||||
return `<button type="button" class="ha-entity-option" data-ha-entity-value="${esc(entity.entity_id)}"><span><strong>${esc(title)}</strong><code>${esc(entity.entity_id)}</code></span><small>${esc(state)}</small></button>`;
|
||||
}).join('') : `<div class="ha-entity-empty">${esc(tr('flow.haEntitySearchEmpty'))}</div>`;
|
||||
host.hidden = false;
|
||||
}
|
||||
|
||||
async function loadHaEntityCatalog(force = false) {
|
||||
const fresh = app.haEntityCatalogLoadedAt && Date.now() - app.haEntityCatalogLoadedAt < 60000;
|
||||
if (!force && fresh) return app.haEntityCatalog;
|
||||
if (app.haEntityCatalogLoading) return app.haEntityCatalog;
|
||||
app.haEntityCatalogLoading = true;
|
||||
const activeInput = $('#flowSharedInputFields [data-ha-entity-search]');
|
||||
if (activeInput) renderHaEntityPickerResults(activeInput, $('#flowSharedInputKind')?.value || 'ha_state');
|
||||
try {
|
||||
const response = await api('/api/integrations/home-assistant/entities');
|
||||
app.haEntityCatalog = Array.isArray(response.entities) ? response.entities : [];
|
||||
app.haEntityCatalogConfigured = response.configured === true;
|
||||
app.haEntityCatalogLoadedAt = Date.now();
|
||||
renderHaEntitySuggestions();
|
||||
} catch (_) {
|
||||
app.haEntityCatalog = [];
|
||||
app.haEntityCatalogConfigured = false;
|
||||
app.haEntityCatalogLoadedAt = Date.now();
|
||||
} finally {
|
||||
app.haEntityCatalogLoading = false;
|
||||
const input = $('#flowSharedInputFields [data-ha-entity-search]');
|
||||
if (input) renderHaEntityPickerResults(input, $('#flowSharedInputKind')?.value || 'ha_state');
|
||||
}
|
||||
return app.haEntityCatalog;
|
||||
}
|
||||
|
||||
function haEntityPickerMarkup(kind, value, placeholder) {
|
||||
return `<div class="ha-entity-picker" data-ha-entity-picker><label><span>entity_id</span><input type="search" autocomplete="off" data-shared-config="entity_id" data-ha-entity-search value="${esc(value || '')}" placeholder="${esc(placeholder)}"></label><small class="field-note" data-ha-entity-note>${esc(tr('flow.haEntitySearchHint'))}</small><div class="ha-entity-results" data-ha-entity-results hidden></div></div>`;
|
||||
}
|
||||
|
||||
function bindHaEntityPicker(kind) {
|
||||
const input = $('#flowSharedInputFields [data-ha-entity-search]');
|
||||
const results = $('#flowSharedInputFields [data-ha-entity-results]');
|
||||
if (!input || !results) return;
|
||||
const refresh = () => renderHaEntityPickerResults(input, kind);
|
||||
input.addEventListener('input', refresh);
|
||||
input.addEventListener('focus', refresh);
|
||||
results.addEventListener('click', event => {
|
||||
const option = event.target.closest?.('[data-ha-entity-value]');
|
||||
if (!option) return;
|
||||
input.value = option.dataset.haEntityValue || '';
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
results.hidden = true;
|
||||
});
|
||||
loadHaEntityCatalog().then(refresh);
|
||||
}
|
||||
|
||||
function renderFlowSharedInputFields(kind, config = {}) {
|
||||
const host = $('#flowSharedInputFields'); if (!host) return;
|
||||
const c = config || {};
|
||||
let fields = '';
|
||||
if (kind === 'constant') fields = `<label><span>${esc(tr('flow.value'))}</span><select data-shared-config="value"><option value="true" ${c.value !== false ? 'selected' : ''}>${esc(tr('common.yes'))}</option><option value="false" ${c.value === false ? 'selected' : ''}>${esc(tr('common.no'))}</option></select></label>`;
|
||||
else if (kind === 'ha_state') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label>`;
|
||||
else if (kind === 'ha_numeric') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="sensor.energy_price"></label>`;
|
||||
else if (kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-shared-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label>`;
|
||||
else if (kind === 'ha_available') fields = `<label><span>entity_id</span><input data-shared-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 (kind === 'ha_state') fields = haEntityPickerMarkup(kind, c.entity_id, 'binary_sensor.window');
|
||||
else if (kind === 'ha_numeric') fields = haEntityPickerMarkup(kind, c.entity_id, 'sensor.energy_price');
|
||||
else if (kind === 'ha_attribute') fields = `${haEntityPickerMarkup(kind, c.entity_id, 'climate.living_room')}<label><span>${esc(tr('flow.attribute'))}</span><input data-shared-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label>`;
|
||||
else if (kind === 'ha_available') fields = `${haEntityPickerMarkup(kind, c.entity_id, 'binary_sensor.window')}<p class="field-note">${esc(tr('flow.haAvailableHint'))}</p>`;
|
||||
else if (kind === 'outdoor_temperature') fields = `<p class="field-note">${esc(tr('flow.sharedInputSourceOnlyHint'))}</p>`;
|
||||
else if (kind === 'device_temperature') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label>`;
|
||||
else if (kind === 'zone_temperature') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label>`;
|
||||
@@ -134,6 +230,7 @@ function renderFlowSharedInputFields(kind, config = {}) {
|
||||
else if (kind === 'group_state') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-shared-config="group_id">${sharedFlowOptions(app.groups, c.group_id)}</select></label><input type="hidden" data-shared-config="field" value="power_enabled">`;
|
||||
else if (kind === 'night_mode') fields = `<p class="field-note">${esc(tr('flow.nightModeHint'))}</p>`;
|
||||
host.innerHTML = fields;
|
||||
if (isHaSharedInputKind(kind)) bindHaEntityPicker(kind);
|
||||
const testPanel = $('#flowSharedInputTestPanel'), testResult = $('#flowSharedInputTestResult');
|
||||
if (testPanel) testPanel.hidden = !isHaSharedInputKind(kind);
|
||||
if (testResult) { testResult.hidden = true; testResult.innerHTML = ''; }
|
||||
@@ -337,22 +434,60 @@ function renderNightSettings() {
|
||||
markFormClean(form);
|
||||
}
|
||||
|
||||
function renderHomeAssistantAuthState() {
|
||||
const form = $('#homeAssistantForm');
|
||||
if (!form || !app.settings?.home_assistant) return;
|
||||
const ha = app.settings.home_assistant;
|
||||
const supervisorDetected = ha.supervisor_detected === true;
|
||||
const manualFallback = supervisorDetected && (ha.manual_auth_override === true || app.haManualFallbackVisible === true);
|
||||
form.dataset.haManualOverride = manualFallback ? 'true' : 'false';
|
||||
|
||||
$$('[data-ha-manual-field]', form).forEach(node => { node.hidden = supervisorDetected && !manualFallback; });
|
||||
form.ha_url.required = manualFallback;
|
||||
form.ha_token.required = manualFallback && !ha.manual_token_configured;
|
||||
const useSupervisor = $('#haUseSupervisor');
|
||||
if (useSupervisor) useSupervisor.hidden = !supervisorDetected || !manualFallback;
|
||||
|
||||
const status = $('#haSupervisorStatus');
|
||||
if (!status) return;
|
||||
status.hidden = !supervisorDetected;
|
||||
status.classList.remove('success', 'warning', 'error');
|
||||
if (!supervisorDetected) return;
|
||||
|
||||
const testState = app.haSupervisorTestState;
|
||||
if (manualFallback) {
|
||||
status.classList.add(testState?.ok === false ? 'error' : 'warning');
|
||||
status.innerHTML = `<strong>${esc(testState?.ok === false ? tr('settings.haSupervisorTestFailedTitle') : tr('settings.haManualFallbackTitle'))}</strong><span>${esc(testState?.ok === false ? tr('settings.haSupervisorTestFailedHint') : tr('settings.haManualFallbackHint'))}</span>`;
|
||||
} else if (testState?.ok === true) {
|
||||
status.classList.add('success');
|
||||
status.innerHTML = `<strong>${esc(tr('settings.haSupervisorVerifiedTitle'))}</strong><span>${esc(tr('settings.haSupervisorVerifiedHint'))}</span>`;
|
||||
} else if (ha.supervisor_token_detected) {
|
||||
status.classList.add('success');
|
||||
status.innerHTML = `<strong>${esc(tr('settings.haSupervisorAutoTitle'))}</strong><span>${esc(tr('settings.haSupervisorAutoHint'))}</span>`;
|
||||
} else {
|
||||
status.classList.add('warning');
|
||||
status.innerHTML = `<strong>${esc(tr('settings.haSupervisorMissingTitle'))}</strong><span>${esc(tr('settings.haSupervisorMissingHint'))}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderHomeAssistantSettings() {
|
||||
if (!app.settings) return;
|
||||
const form = $('#homeAssistantForm');
|
||||
if (!form) return;
|
||||
const supervisorManaged = app.settings.home_assistant?.auth_mode === 'supervisor';
|
||||
form.ha_url.value = app.settings.home_assistant?.url || '';
|
||||
form.ha_url.readOnly = supervisorManaged;
|
||||
const ha = app.settings.home_assistant || {};
|
||||
const supervisorDetected = ha.supervisor_detected === true;
|
||||
form.ha_url.value = supervisorDetected ? (ha.manual_url || '') : (ha.url || '');
|
||||
form.ha_url.readOnly = false;
|
||||
form.ha_token.value = '';
|
||||
form.ha_token.readOnly = supervisorManaged;
|
||||
form.ha_token.placeholder = supervisorManaged
|
||||
? tr('settings.haSupervisorToken')
|
||||
: (app.settings.home_assistant?.token_configured ? tr('settings.haTokenSaved') : tr('settings.haLongLivedToken'));
|
||||
form.ha_outdoor_entity_id.value = app.settings.home_assistant?.outdoor_entity_id || '';
|
||||
form.ha_sensor_stale_after_minutes.value = String(Math.max(1, Math.round(Number(app.settings.home_assistant?.sensor_stale_after_seconds || 300) / 60)));
|
||||
form.ha_allow_invalid_tls.checked = !!app.settings.home_assistant?.allow_invalid_tls;
|
||||
form.ha_token.readOnly = false;
|
||||
form.ha_token.placeholder = (supervisorDetected ? ha.manual_token_configured : ha.token_configured)
|
||||
? tr('settings.haTokenSaved')
|
||||
: tr('settings.haLongLivedToken');
|
||||
form.ha_outdoor_entity_id.value = ha.outdoor_entity_id || '';
|
||||
form.ha_sensor_stale_after_minutes.value = String(Math.max(1, Math.round(Number(ha.sensor_stale_after_seconds || 300) / 60)));
|
||||
form.ha_allow_invalid_tls.checked = !!ha.allow_invalid_tls;
|
||||
form.outdoor_assist_enabled.checked = !!app.settings.outdoor_assist_enabled;
|
||||
renderHomeAssistantAuthState();
|
||||
renderSensorAliases();
|
||||
renderFlowSharedInputs();
|
||||
renderAccessTokens();
|
||||
|
||||
Reference in New Issue
Block a user