feat: HA value source test — raw value axis + behavior IconSelect
Lint & Test / test (push) Successful in 1m9s
Lint & Test / test (push) Successful in 1m9s
- WS sends raw_value and raw_range for HA entity streams - Canvas draws right-side Y-axis labels: configured min/max range and current raw HA value (green, positioned at correct Y) - Replace plain <select> for scene behavior with IconSelect (moon/sun)
This commit is contained in:
@@ -88,6 +88,7 @@
|
||||
align-items: center;
|
||||
padding: 10px 0 0;
|
||||
font-family: monospace;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.vs-test-stat {
|
||||
|
||||
@@ -47,6 +47,7 @@ let _vsGradientInputEntitySelect: EntitySelect | null = null;
|
||||
let _vsGradientEntitySelect: EntitySelect | null = null;
|
||||
let _vsCSSSourceEntitySelect: EntitySelect | null = null;
|
||||
let _vsGradientEasingIconSelect: IconSelect | null = null;
|
||||
let _vsBehaviorIconSelect: IconSelect | null = null;
|
||||
let _vsTagsInput: TagInput | null = null;
|
||||
|
||||
class ValueSourceModal extends Modal {
|
||||
@@ -61,6 +62,7 @@ class ValueSourceModal extends Modal {
|
||||
if (_vsGradientEntitySelect) { _vsGradientEntitySelect.destroy(); _vsGradientEntitySelect = null; }
|
||||
if (_vsCSSSourceEntitySelect) { _vsCSSSourceEntitySelect.destroy(); _vsCSSSourceEntitySelect = null; }
|
||||
if (_vsGradientEasingIconSelect) { _vsGradientEasingIconSelect.destroy(); _vsGradientEasingIconSelect = null; }
|
||||
if (_vsBehaviorIconSelect) { _vsBehaviorIconSelect.destroy(); _vsBehaviorIconSelect = null; }
|
||||
}
|
||||
|
||||
snapshotValues() {
|
||||
@@ -289,6 +291,17 @@ function _ensureAudioModeIconSelect() {
|
||||
_audioModeIconSelect = new IconSelect({ target: sel, items, columns: 3 } as any);
|
||||
}
|
||||
|
||||
function _ensureBehaviorIconSelect() {
|
||||
const sel = document.getElementById('value-source-scene-behavior') as HTMLSelectElement | null;
|
||||
if (!sel) return;
|
||||
const items = [
|
||||
{ value: 'complement', icon: _icon(P.moon), label: t('value_source.scene_behavior.complement'), desc: t('value_source.scene_behavior.hint') },
|
||||
{ value: 'match', icon: _icon(P.sun), label: t('value_source.scene_behavior.match') },
|
||||
];
|
||||
if (_vsBehaviorIconSelect) { _vsBehaviorIconSelect.updateItems(items); return; }
|
||||
_vsBehaviorIconSelect = new IconSelect({ target: sel, items, columns: 2 } as any);
|
||||
}
|
||||
|
||||
function _ensureVSTypeIconSelect() {
|
||||
const sel = document.getElementById('value-source-type');
|
||||
if (!sel) return;
|
||||
@@ -497,6 +510,7 @@ export function onValueSourceTypeChange() {
|
||||
if (type === 'audio') _ensureAudioModeIconSelect();
|
||||
(document.getElementById('value-source-adaptive-time-section') as HTMLElement).style.display = type === 'adaptive_time' ? '' : 'none';
|
||||
(document.getElementById('value-source-adaptive-scene-section') as HTMLElement).style.display = type === 'adaptive_scene' ? '' : 'none';
|
||||
if (type === 'adaptive_scene') _ensureBehaviorIconSelect();
|
||||
(document.getElementById('value-source-daylight-section') as HTMLElement).style.display = type === 'daylight' ? '' : 'none';
|
||||
(document.getElementById('value-source-static-color-section') as HTMLElement).style.display = type === 'static_color' ? '' : 'none';
|
||||
(document.getElementById('value-source-animated-color-section') as HTMLElement).style.display = type === 'animated_color' ? '' : 'none';
|
||||
@@ -744,6 +758,8 @@ let _testVsLatest: any = null;
|
||||
let _testVsHistory: number[] = [];
|
||||
let _testVsMinObserved = Infinity;
|
||||
let _testVsMaxObserved = -Infinity;
|
||||
let _testVsRawLatest: number | null = null;
|
||||
let _testVsRawRange: [number, number] | null = null;
|
||||
|
||||
const testVsModal = new Modal('test-value-source-modal', { backdrop: true, lock: true });
|
||||
|
||||
@@ -759,6 +775,8 @@ export function testValueSource(sourceId: any) {
|
||||
_testVsHistory = [];
|
||||
_testVsMinObserved = Infinity;
|
||||
_testVsMaxObserved = -Infinity;
|
||||
_testVsRawLatest = null;
|
||||
_testVsRawRange = null;
|
||||
|
||||
const currentEl = document.getElementById('vs-test-current');
|
||||
const minEl = document.getElementById('vs-test-min');
|
||||
@@ -794,6 +812,10 @@ export function testValueSource(sourceId: any) {
|
||||
}
|
||||
if (data.value < _testVsMinObserved) _testVsMinObserved = data.value;
|
||||
if (data.value > _testVsMaxObserved) _testVsMaxObserved = data.value;
|
||||
if (data.raw_value !== undefined) {
|
||||
_testVsRawLatest = data.raw_value;
|
||||
if (data.raw_range) _testVsRawRange = data.raw_range;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Value source test WS parse error:', e);
|
||||
return;
|
||||
@@ -929,6 +951,46 @@ function _renderVsChart() {
|
||||
if (_testVsMaxObserved !== -Infinity && mxEl) {
|
||||
mxEl.textContent = (_testVsMaxObserved * 100).toFixed(1) + '%';
|
||||
}
|
||||
|
||||
// Draw right-side Y-axis labels for raw HA values
|
||||
if (_testVsRawRange) {
|
||||
const [rawMin, rawMax] = _testVsRawRange;
|
||||
const rawMid = (rawMin + rawMax) / 2;
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
||||
ctx.font = '10px monospace';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(_fmtRaw(rawMax), w - 4, 12);
|
||||
ctx.fillText(_fmtRaw(rawMid), w - 4, h / 2 - 2);
|
||||
ctx.fillText(_fmtRaw(rawMin), w - 4, h - 4);
|
||||
|
||||
// Draw current raw value marker on right edge
|
||||
if (_testVsRawLatest !== null) {
|
||||
const rawRange = rawMax - rawMin;
|
||||
const frac = rawRange !== 0 ? (_testVsRawLatest - rawMin) / rawRange : 0.5;
|
||||
const clampedFrac = Math.max(0, Math.min(1, frac));
|
||||
const yRaw = h - clampedFrac * h;
|
||||
|
||||
ctx.fillStyle = '#4caf50';
|
||||
ctx.font = 'bold 10px monospace';
|
||||
ctx.textAlign = 'right';
|
||||
// Offset label to avoid overlap with top/bottom labels
|
||||
const labelY = Math.max(20, Math.min(h - 12, yRaw + 4));
|
||||
ctx.fillText(_fmtRaw(_testVsRawLatest), w - 4, labelY);
|
||||
|
||||
// Small tick mark
|
||||
ctx.strokeStyle = '#4caf50';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(w - 2, yRaw);
|
||||
ctx.lineTo(w, yRaw);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _fmtRaw(v: number): string {
|
||||
return Number.isInteger(v) ? String(v) : v.toFixed(1);
|
||||
}
|
||||
|
||||
// ── Card rendering (used by streams.js) ───────────────────────
|
||||
|
||||
@@ -366,7 +366,7 @@
|
||||
"tour.targets": "Targets — add WLED devices, configure LED targets with capture settings and calibration.",
|
||||
"tour.sources": "Sources — manage capture templates, picture sources, audio sources, and color strips.",
|
||||
"tour.graph": "Graph — visual overview of all entities and their connections. Drag ports to connect, right-click edges to disconnect.",
|
||||
"tour.automations": "Automations — automate scene switching with time, audio, or value conditions.",
|
||||
"tour.automations": "Automations — automate scene switching with time, audio, or value rules.",
|
||||
"tour.settings": "Settings — backup and restore configuration, manage auto-backups.",
|
||||
"tour.api": "API Docs — interactive REST API documentation powered by Swagger.",
|
||||
"tour.search": "Search — quickly find and navigate to any entity with Ctrl+K.",
|
||||
@@ -389,11 +389,11 @@
|
||||
"tour.src.processed": "Processed — apply post-processing effects like blur, brightness, or color correction.",
|
||||
"tour.src.color_strip": "Color Strips — define how screen regions map to LED segments.",
|
||||
"tour.src.audio": "Audio — analyze microphone or system audio for reactive LED effects.",
|
||||
"tour.src.value": "Value — numeric data sources used as conditions in automations.",
|
||||
"tour.src.value": "Value — numeric data sources used as rules in automations.",
|
||||
"tour.src.sync": "Sync Clocks — shared timers that synchronize animations across multiple sources.",
|
||||
"tour.auto.list": "Automations — automate scene activation based on time, audio, or value conditions.",
|
||||
"tour.auto.add": "Click + to create a new automation with conditions and a scene to activate.",
|
||||
"tour.auto.card": "Each card shows automation status, conditions, and quick controls to edit or toggle.",
|
||||
"tour.auto.list": "Automations — automate scene activation based on time, audio, or value rules.",
|
||||
"tour.auto.add": "Click + to create a new automation with rules and a scene to activate.",
|
||||
"tour.auto.card": "Each card shows automation status, rules, and quick controls to edit or toggle.",
|
||||
"tour.auto.scenes_list": "Scenes — saved system states that automations can activate or you can apply manually.",
|
||||
"tour.auto.scenes_add": "Click + to capture the current system state as a new scene preset.",
|
||||
"tour.auto.scenes_card": "Each scene card shows target/device counts. Click to edit, recapture, or activate.",
|
||||
@@ -451,6 +451,8 @@
|
||||
"palette.search": "Search…",
|
||||
"section.filter.placeholder": "Filter...",
|
||||
"section.filter.reset": "Clear filter",
|
||||
"section.hide": "Hide",
|
||||
"section.show_hidden": "Show hidden cards",
|
||||
"tags.label": "Tags",
|
||||
"tags.hint": "Assign tags for grouping and filtering cards",
|
||||
"tags.placeholder": "Add tag...",
|
||||
@@ -739,71 +741,68 @@
|
||||
"automations.name.placeholder": "My Automation",
|
||||
"automations.enabled": "Enabled:",
|
||||
"automations.enabled.hint": "Disabled automations won't activate even when conditions are met",
|
||||
"automations.condition_logic": "Condition Logic:",
|
||||
"automations.condition_logic.hint": "How multiple conditions are combined: ANY (OR) or ALL (AND)",
|
||||
"automations.condition_logic.or": "Any condition (OR)",
|
||||
"automations.condition_logic.and": "All conditions (AND)",
|
||||
"automations.condition_logic.or.desc": "Triggers when any condition matches",
|
||||
"automations.condition_logic.and.desc": "Triggers only when all match",
|
||||
"automations.conditions": "Conditions:",
|
||||
"automations.conditions.hint": "Rules that determine when this automation activates",
|
||||
"automations.conditions.add": "Add Condition",
|
||||
"automations.conditions.empty": "No conditions — automation is always active when enabled",
|
||||
"automations.condition.always": "Always",
|
||||
"automations.condition.always.desc": "Always active",
|
||||
"automations.condition.always.hint": "Automation activates immediately when enabled and stays active.",
|
||||
"automations.condition.startup": "Startup",
|
||||
"automations.condition.startup.desc": "On server start",
|
||||
"automations.condition.startup.hint": "Activates when the server starts and stays active while enabled.",
|
||||
"automations.condition.application": "Application",
|
||||
"automations.condition.application.desc": "App running/focused",
|
||||
"automations.condition.application.apps": "Applications:",
|
||||
"automations.condition.application.apps.hint": "Process names, one per line (e.g. firefox.exe)",
|
||||
"automations.condition.application.browse": "Browse",
|
||||
"automations.condition.application.search": "Filter processes...",
|
||||
"automations.condition.application.no_processes": "No processes found",
|
||||
"automations.condition.application.match_type": "Match Type:",
|
||||
"automations.condition.application.match_type.hint": "How to detect the application",
|
||||
"automations.condition.application.match_type.running": "Running",
|
||||
"automations.condition.application.match_type.running.desc": "Process is active",
|
||||
"automations.condition.application.match_type.topmost": "Topmost",
|
||||
"automations.condition.application.match_type.topmost.desc": "Foreground window",
|
||||
"automations.condition.application.match_type.topmost_fullscreen": "Topmost + FS",
|
||||
"automations.condition.application.match_type.topmost_fullscreen.desc": "Foreground + fullscreen",
|
||||
"automations.condition.application.match_type.fullscreen": "Fullscreen",
|
||||
"automations.condition.application.match_type.fullscreen.desc": "Any fullscreen app",
|
||||
"automations.condition.time_of_day": "Time of Day",
|
||||
"automations.condition.time_of_day.desc": "Time range",
|
||||
"automations.condition.time_of_day.start_time": "Start Time:",
|
||||
"automations.condition.time_of_day.end_time": "End Time:",
|
||||
"automations.condition.time_of_day.overnight_hint": "For overnight ranges (e.g. 22:00–06:00), set start time after end time.",
|
||||
"automations.condition.system_idle": "System Idle",
|
||||
"automations.condition.system_idle.desc": "User idle/active",
|
||||
"automations.condition.system_idle.idle_minutes": "Idle Timeout (minutes):",
|
||||
"automations.condition.system_idle.mode": "Trigger Mode:",
|
||||
"automations.condition.system_idle.when_idle": "When idle",
|
||||
"automations.condition.system_idle.when_active": "When active",
|
||||
"automations.condition.display_state": "Display State",
|
||||
"automations.condition.display_state.desc": "Monitor on/off",
|
||||
"automations.condition.display_state.state": "Monitor State:",
|
||||
"automations.condition.display_state.on": "On",
|
||||
"automations.condition.display_state.off": "Off (sleeping)",
|
||||
"automations.condition.mqtt": "MQTT",
|
||||
"automations.condition.mqtt.desc": "MQTT message",
|
||||
"automations.condition.mqtt.topic": "Topic:",
|
||||
"automations.condition.mqtt.payload": "Payload:",
|
||||
"automations.condition.mqtt.match_mode": "Match Mode:",
|
||||
"automations.condition.mqtt.match_mode.exact": "Exact",
|
||||
"automations.condition.mqtt.match_mode.contains": "Contains",
|
||||
"automations.condition.mqtt.match_mode.regex": "Regex",
|
||||
"automations.condition.mqtt.hint": "Activate when an MQTT topic receives a matching payload",
|
||||
"automations.condition.webhook": "Webhook",
|
||||
"automations.condition.webhook.desc": "HTTP callback",
|
||||
"automations.condition.webhook.hint": "Activate via an HTTP call from external services (Home Assistant, IFTTT, curl, etc.)",
|
||||
"automations.condition.webhook.url": "Webhook URL:",
|
||||
"automations.condition.webhook.copy": "Copy",
|
||||
"automations.condition.webhook.copied": "Copied!",
|
||||
"automations.condition.webhook.save_first": "Save the automation first to generate a webhook URL",
|
||||
"automations.rule_logic": "Rule Logic:",
|
||||
"automations.rule_logic.hint": "How multiple rules are combined: ANY (OR) or ALL (AND)",
|
||||
"automations.rule_logic.or": "Any rule (OR)",
|
||||
"automations.rule_logic.and": "All rules (AND)",
|
||||
"automations.rule_logic.or.desc": "Triggers when any rule matches",
|
||||
"automations.rule_logic.and.desc": "Triggers only when all match",
|
||||
"automations.rules": "Rules:",
|
||||
"automations.rules.hint": "Rules that determine when this automation activates",
|
||||
"automations.rules.add": "Add Rule",
|
||||
"automations.rules.empty": "No rules — automation is always active when enabled",
|
||||
"automations.rule.startup": "Startup",
|
||||
"automations.rule.startup.desc": "On server start",
|
||||
"automations.rule.startup.hint": "Activates when the server starts and stays active while enabled.",
|
||||
"automations.rule.application": "Application",
|
||||
"automations.rule.application.desc": "App running/focused",
|
||||
"automations.rule.application.apps": "Applications:",
|
||||
"automations.rule.application.apps.hint": "Process names, one per line (e.g. firefox.exe)",
|
||||
"automations.rule.application.browse": "Browse",
|
||||
"automations.rule.application.search": "Filter processes...",
|
||||
"automations.rule.application.no_processes": "No processes found",
|
||||
"automations.rule.application.match_type": "Match Type:",
|
||||
"automations.rule.application.match_type.hint": "How to detect the application",
|
||||
"automations.rule.application.match_type.running": "Running",
|
||||
"automations.rule.application.match_type.running.desc": "Process is active",
|
||||
"automations.rule.application.match_type.topmost": "Topmost",
|
||||
"automations.rule.application.match_type.topmost.desc": "Foreground window",
|
||||
"automations.rule.application.match_type.topmost_fullscreen": "Topmost + FS",
|
||||
"automations.rule.application.match_type.topmost_fullscreen.desc": "Foreground + fullscreen",
|
||||
"automations.rule.application.match_type.fullscreen": "Fullscreen",
|
||||
"automations.rule.application.match_type.fullscreen.desc": "Any fullscreen app",
|
||||
"automations.rule.time_of_day": "Time of Day",
|
||||
"automations.rule.time_of_day.desc": "Time range",
|
||||
"automations.rule.time_of_day.start_time": "Start Time:",
|
||||
"automations.rule.time_of_day.end_time": "End Time:",
|
||||
"automations.rule.time_of_day.overnight_hint": "For overnight ranges (e.g. 22:00–06:00), set start time after end time.",
|
||||
"automations.rule.system_idle": "System Idle",
|
||||
"automations.rule.system_idle.desc": "User idle/active",
|
||||
"automations.rule.system_idle.idle_minutes": "Idle Timeout (minutes):",
|
||||
"automations.rule.system_idle.mode": "Trigger Mode:",
|
||||
"automations.rule.system_idle.when_idle": "When idle",
|
||||
"automations.rule.system_idle.when_active": "When active",
|
||||
"automations.rule.display_state": "Display State",
|
||||
"automations.rule.display_state.desc": "Monitor on/off",
|
||||
"automations.rule.display_state.state": "Monitor State:",
|
||||
"automations.rule.display_state.on": "On",
|
||||
"automations.rule.display_state.off": "Off (sleeping)",
|
||||
"automations.rule.mqtt": "MQTT",
|
||||
"automations.rule.mqtt.desc": "MQTT message",
|
||||
"automations.rule.mqtt.topic": "Topic:",
|
||||
"automations.rule.mqtt.payload": "Payload:",
|
||||
"automations.rule.mqtt.match_mode": "Match Mode:",
|
||||
"automations.rule.mqtt.match_mode.exact": "Exact",
|
||||
"automations.rule.mqtt.match_mode.contains": "Contains",
|
||||
"automations.rule.mqtt.match_mode.regex": "Regex",
|
||||
"automations.rule.mqtt.hint": "Activate when an MQTT topic receives a matching payload",
|
||||
"automations.rule.webhook": "Webhook",
|
||||
"automations.rule.webhook.desc": "HTTP callback",
|
||||
"automations.rule.webhook.hint": "Activate via an HTTP call from external services (Home Assistant, IFTTT, curl, etc.)",
|
||||
"automations.rule.webhook.url": "Webhook URL:",
|
||||
"automations.rule.webhook.copy": "Copy",
|
||||
"automations.rule.webhook.copied": "Copied!",
|
||||
"automations.rule.webhook.save_first": "Save the automation first to generate a webhook URL",
|
||||
"automations.scene": "Scene:",
|
||||
"automations.scene.hint": "Scene preset to activate when conditions are met",
|
||||
"automations.scene.search_placeholder": "Search scenes...",
|
||||
@@ -1881,16 +1880,16 @@
|
||||
"ha_light.mapping.select_entity": "Select a light entity...",
|
||||
"ha_light.mapping.search_entity": "Search light entities...",
|
||||
"section.empty.ha_light_targets": "No HA light targets yet. Click + to add one.",
|
||||
"automations.condition.home_assistant": "Home Assistant",
|
||||
"automations.condition.home_assistant.desc": "HA entity state",
|
||||
"automations.condition.home_assistant.ha_source": "HA Source:",
|
||||
"automations.condition.home_assistant.entity_id": "Entity ID:",
|
||||
"automations.condition.home_assistant.state": "State:",
|
||||
"automations.condition.home_assistant.match_mode": "Match Mode:",
|
||||
"automations.condition.home_assistant.hint": "Activate when a Home Assistant entity matches the specified state",
|
||||
"automations.condition.ha.match_mode.exact.desc": "State must match exactly",
|
||||
"automations.condition.ha.match_mode.contains.desc": "State must contain the text",
|
||||
"automations.condition.ha.match_mode.regex.desc": "State must match the regex pattern",
|
||||
"automations.rule.home_assistant": "Home Assistant",
|
||||
"automations.rule.home_assistant.desc": "HA entity state",
|
||||
"automations.rule.home_assistant.ha_source": "HA Source:",
|
||||
"automations.rule.home_assistant.entity_id": "Entity ID:",
|
||||
"automations.rule.home_assistant.state": "State:",
|
||||
"automations.rule.home_assistant.match_mode": "Match Mode:",
|
||||
"automations.rule.home_assistant.hint": "Activate when a Home Assistant entity matches the specified state",
|
||||
"automations.rule.ha.match_mode.exact.desc": "State must match exactly",
|
||||
"automations.rule.ha.match_mode.contains.desc": "State must contain the text",
|
||||
"automations.rule.ha.match_mode.regex.desc": "State must match the regex pattern",
|
||||
"color_strip.clock": "Sync Clock:",
|
||||
"color_strip.clock.hint": "Link to a sync clock to synchronize animation timing across sources. Speed is controlled on the clock.",
|
||||
"graph.title": "Graph",
|
||||
|
||||
Reference in New Issue
Block a user