feat: BindableFloat — universal value source binding for all scalar properties
Lint & Test / test (push) Successful in 1m20s
Lint & Test / test (push) Successful in 1m20s
Introduce BindableFloat abstraction that allows any numeric property to be
either a static value or dynamically driven by a ValueSource. Backward-compatible
serialization: plain float when unbound, {value, source_id} dict when bound.
Backend:
- storage/bindable.py — BindableFloat dataclass + bfloat() helper
- 25+ scalar properties converted across all entity types
- Runtime VS acquisition in ColorStripStreamManager for CSS bindings
- All stream hot loops use self.resolve() for live values
- KeyColorsColorStripStream now inherits ColorStripStream
Frontend:
- BindableScalarWidget (slider + VS picker toggle) for all editors
- TypeScript BindableFloat type + helpers
- Graph editor edges for all bindable properties
- Audio source channel IconSelect grid
Fixes: daylight longitude, candlelight wind_strength/candle_type from_dict
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { apiKey, _automationsLoading, set_automationsLoading, automationsCacheObj, scenePresetsCache, _cachedHASources } from '../core/state.ts';
|
||||
import { getHAEntityIcon } from '../core/icons.ts';
|
||||
import { fetchWithAuth, escapeHtml } from '../core/api.ts';
|
||||
import { t } from '../core/i18n.ts';
|
||||
import { showToast, showConfirm, setTabRefreshing } from '../core/ui.ts';
|
||||
@@ -21,6 +22,33 @@ import { TreeNav } from '../core/tree-nav.ts';
|
||||
import { csScenes, createSceneCard, initScenePresetDelegation } from './scene-presets.ts';
|
||||
import type { Automation } from '../types.ts';
|
||||
|
||||
// ── HA condition entity cache ──
|
||||
let _haConditionEntities: any[] = [];
|
||||
|
||||
async function _loadHAEntitiesForCondition(haSourceId: string, container: HTMLElement): Promise<void> {
|
||||
if (!haSourceId) { _haConditionEntities = []; return; }
|
||||
try {
|
||||
const resp = await fetchWithAuth(`/home-assistant/sources/${haSourceId}/entities`);
|
||||
if (!resp.ok) { _haConditionEntities = []; return; }
|
||||
const data = await resp.json();
|
||||
_haConditionEntities = data.entities || [];
|
||||
} catch {
|
||||
_haConditionEntities = [];
|
||||
}
|
||||
// Rebuild entity select options
|
||||
const entitySelect = container.querySelector('.condition-ha-entity-id') as HTMLSelectElement;
|
||||
if (entitySelect) {
|
||||
const currentVal = entitySelect.value;
|
||||
entitySelect.innerHTML = `<option value="">—</option>` +
|
||||
_haConditionEntities.map((e: any) =>
|
||||
`<option value="${e.entity_id}" ${e.entity_id === currentVal ? 'selected' : ''}>${escapeHtml(e.friendly_name || e.entity_id)}</option>`
|
||||
).join('');
|
||||
if (currentVal && !_haConditionEntities.some((e: any) => e.entity_id === currentVal)) {
|
||||
entitySelect.innerHTML += `<option value="${escapeHtml(currentVal)}" selected>${escapeHtml(currentVal)}</option>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _automationTagsInput: any = null;
|
||||
|
||||
// ── Auto-name ──
|
||||
@@ -732,7 +760,6 @@ function addAutomationConditionRow(condition: any) {
|
||||
const entityId = data.entity_id || '';
|
||||
const haState = data.state || '';
|
||||
const matchMode = data.match_mode || 'exact';
|
||||
// Build HA source options from cached data
|
||||
const haOptions = _cachedHASources.map((s: any) =>
|
||||
`<option value="${s.id}" ${s.id === haSourceId ? 'selected' : ''}>${escapeHtml(s.name)}</option>`
|
||||
).join('');
|
||||
@@ -748,7 +775,9 @@ function addAutomationConditionRow(condition: any) {
|
||||
</div>
|
||||
<div class="condition-field">
|
||||
<label>${t('automations.condition.home_assistant.entity_id')}</label>
|
||||
<input type="text" class="condition-ha-entity-id" value="${escapeHtml(entityId)}" placeholder="binary_sensor.front_door">
|
||||
<select class="condition-ha-entity-id">
|
||||
${entityId ? `<option value="${escapeHtml(entityId)}" selected>${escapeHtml(entityId)}</option>` : '<option value="">—</option>'}
|
||||
</select>
|
||||
</div>
|
||||
<div class="condition-field">
|
||||
<label>${t('automations.condition.home_assistant.state')}</label>
|
||||
@@ -763,6 +792,45 @@ function addAutomationConditionRow(condition: any) {
|
||||
</select>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Wire HA source EntitySelect
|
||||
const haSrcSelect = container.querySelector('.condition-ha-source-id') as HTMLSelectElement;
|
||||
new EntitySelect({
|
||||
target: haSrcSelect,
|
||||
getItems: () => _cachedHASources.map((s: any) => ({
|
||||
value: s.id, label: s.name, icon: _icon(P.home),
|
||||
desc: s.connected ? t('ha_source.connected') : t('ha_source.disconnected'),
|
||||
})),
|
||||
placeholder: t('palette.search'),
|
||||
onChange: (newId: string) => _loadHAEntitiesForCondition(newId, container),
|
||||
});
|
||||
|
||||
// Wire entity EntitySelect
|
||||
const entitySelect = container.querySelector('.condition-ha-entity-id') as HTMLSelectElement;
|
||||
const entityES = new EntitySelect({
|
||||
target: entitySelect,
|
||||
getItems: () => _haConditionEntities.map((e: any) => ({
|
||||
value: e.entity_id, label: e.friendly_name || e.entity_id,
|
||||
icon: getHAEntityIcon(e), desc: e.state || '',
|
||||
})),
|
||||
placeholder: t('ha_light.mapping.search_entity'),
|
||||
});
|
||||
|
||||
// Wire match mode IconSelect
|
||||
const matchSelect = container.querySelector('.condition-ha-match-mode') as HTMLSelectElement;
|
||||
new IconSelect({
|
||||
target: matchSelect,
|
||||
items: [
|
||||
{ value: 'exact', icon: _icon(P.check), label: t('automations.condition.mqtt.match_mode.exact'), desc: t('automations.condition.ha.match_mode.exact.desc') },
|
||||
{ value: 'contains', icon: _icon(P.search), label: t('automations.condition.mqtt.match_mode.contains'), desc: t('automations.condition.ha.match_mode.contains.desc') },
|
||||
{ value: 'regex', icon: _icon(P.code), label: t('automations.condition.mqtt.match_mode.regex'), desc: t('automations.condition.ha.match_mode.regex.desc') },
|
||||
],
|
||||
columns: 1,
|
||||
});
|
||||
|
||||
// Load entities if source is already selected
|
||||
if (haSourceId) _loadHAEntitiesForCondition(haSourceId, container);
|
||||
|
||||
return;
|
||||
}
|
||||
if (type === 'webhook') {
|
||||
@@ -878,7 +946,7 @@ function getAutomationEditorConditions() {
|
||||
conditions.push({
|
||||
condition_type: 'home_assistant',
|
||||
ha_source_id: (row.querySelector('.condition-ha-source-id') as HTMLSelectElement).value,
|
||||
entity_id: (row.querySelector('.condition-ha-entity-id') as HTMLInputElement).value.trim(),
|
||||
entity_id: (row.querySelector('.condition-ha-entity-id') as HTMLSelectElement).value.trim(),
|
||||
state: (row.querySelector('.condition-ha-state') as HTMLInputElement).value,
|
||||
match_mode: (row.querySelector('.condition-ha-match-mode') as HTMLSelectElement).value || 'exact',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user