feat: value source card crosslinks + gradient_map test shows input value
Lint & Test / test (push) Successful in 1m23s

Add navigateToCard crosslinks for ha_entity (→ HA source), gradient_map
(→ input value source + gradient entity), and css_extract (→ color strip).
Gradient map test now charts the input interpolation factor instead of
output luminance, making the 0–1 chart meaningful.
This commit is contained in:
2026-03-30 03:23:05 +03:00
parent f6c25cd15f
commit 4b7a8d75f4
4 changed files with 34 additions and 8 deletions
@@ -397,6 +397,8 @@ async def test_value_source_ws(
msg["color"] = [int(r), int(g), int(b)]
except NotImplementedError:
pass
if hasattr(stream, "get_input_value"):
msg["input_value"] = round(stream.get_input_value(), 4)
if hasattr(stream, "get_raw_value"):
raw = stream.get_raw_value()
if raw is not None:
@@ -939,6 +939,7 @@ class GradientMapValueStream(ValueStream):
self._gradient_store = gradient_store
self._inner_stream: Optional[ValueStream] = None
self._stops: list = []
self._input_value: float = 0.0
self._resolve_gradient()
def _resolve_gradient(self) -> None:
@@ -979,11 +980,16 @@ class GradientMapValueStream(ValueStream):
r, g, b = self.get_color()
return (0.299 * r + 0.587 * g + 0.114 * b) / 255.0
def get_input_value(self) -> float:
"""Return the last input interpolation factor (0.01.0)."""
return self._input_value
def get_color(self) -> tuple:
if self._inner_stream is None:
return (128, 128, 128)
t = max(0.0, min(1.0, self._inner_stream.get_value()))
self._input_value = t
stops = self._stops
if not stops:
return (128, 128, 128)
@@ -813,13 +813,16 @@ export function testValueSource(sourceId: any) {
_testVsWs.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
_testVsLatest = data.value;
_testVsHistory.push(data.value);
// For color sources with an input value (e.g. gradient_map),
// chart the input interpolation factor instead of luminance
const chartValue = data.input_value !== undefined ? data.input_value : data.value;
_testVsLatest = chartValue;
_testVsHistory.push(chartValue);
if (_testVsHistory.length > VS_HISTORY_SIZE) {
_testVsHistory.shift();
}
if (data.value < _testVsMinObserved) _testVsMinObserved = data.value;
if (data.value > _testVsMaxObserved) _testVsMaxObserved = data.value;
if (chartValue < _testVsMinObserved) _testVsMinObserved = chartValue;
if (chartValue > _testVsMaxObserved) _testVsMaxObserved = chartValue;
if (data.raw_value !== undefined) {
_testVsRawLatest = data.raw_value;
if (data.raw_range) _testVsRawRange = data.raw_range;
@@ -1134,8 +1137,11 @@ export function createValueSourceCard(src: ValueSource) {
const haName = haSrc ? haSrc.name : ((src as any).ha_source_id || '-');
const entityId = (src as any).entity_id || '';
const attr = (src as any).attribute;
const haBadge = haSrc
? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('value_source.ha_source'))}" onclick="event.stopPropagation(); navigateToCard('streams','home_assistant','ha-sources','data-id','${(src as any).ha_source_id}')">${ICON_HOME} ${escapeHtml(haName)}</span>`
: `<span class="stream-card-prop">${ICON_HOME} ${escapeHtml(haName)}</span>`;
propsHtml = `
<span class="stream-card-prop">${ICON_HOME} ${escapeHtml(haName)}</span>
${haBadge}
<span class="stream-card-prop">${ICON_LINK} ${escapeHtml(entityId)}${attr ? '.' + escapeHtml(attr) : ''}</span>
<span class="stream-card-prop">${ICON_MOVE_VERTICAL} ${(src as any).min_ha_value ?? 0}\u2013${(src as any).max_ha_value ?? 100}</span>
`;
@@ -1149,9 +1155,15 @@ export function createValueSourceCard(src: ValueSource) {
const gradientCss = stops.length >= 2
? `linear-gradient(to right, ${stops.map((s: any) => `rgb(${s.color.join(',')}) ${(s.position * 100).toFixed(0)}%`).join(', ')})`
: '#333';
const inputBadge = inputVs
? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('value_source.gradient_map.input'))}" onclick="event.stopPropagation(); navigateToCard('streams','value','value-sources','data-id','${(src as any).value_source_id}')">${ICON_LINK} ${escapeHtml(inputName)}</span>`
: `<span class="stream-card-prop">${ICON_LINK} ${escapeHtml(inputName)}</span>`;
const gradBadge = grad
? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('value_source.gradient_map.gradient'))}" onclick="event.stopPropagation(); navigateToCard('streams','gradients','gradients','data-id','${(src as any).gradient_id}')">${ICON_RAINBOW} ${escapeHtml(gradName)}</span>`
: `<span class="stream-card-prop">${ICON_RAINBOW} ${escapeHtml(gradName)}</span>`;
propsHtml = `
<span class="stream-card-prop">${ICON_LINK} ${escapeHtml(inputName)}</span>
<span class="stream-card-prop">${ICON_RAINBOW} ${escapeHtml(gradName)}</span>
${inputBadge}
${gradBadge}
<div style="height:8px;border-radius:4px;margin:4px 0;background:${gradientCss};"></div>
`;
} else if (src.source_type === 'css_extract') {
@@ -1160,8 +1172,11 @@ export function createValueSourceCard(src: ValueSource) {
const ledStart = (src as any).led_start ?? 0;
const ledEnd = (src as any).led_end ?? -1;
const rangeLabel = ledEnd < 0 ? `${ledStart}\u2013all` : `${ledStart}\u2013${ledEnd}`;
const cssBadge = cssSrc
? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('value_source.css_extract.source'))}" onclick="event.stopPropagation(); navigateToCard('streams','color_strip','color-strips','data-css-id','${(src as any).color_strip_source_id}')">${ICON_DROPLETS} ${escapeHtml(cssName)}</span>`
: `<span class="stream-card-prop">${ICON_DROPLETS} ${escapeHtml(cssName)}</span>`;
propsHtml = `
<span class="stream-card-prop">${ICON_DROPLETS} ${escapeHtml(cssName)}</span>
${cssBadge}
<span class="stream-card-prop">${ICON_MOVE_VERTICAL} LED ${rangeLabel}</span>
`;
}
@@ -1463,6 +1463,9 @@
"value_source.type.gradient_map.desc": "Maps numeric value through a color gradient",
"value_source.type.css_extract": "Strip Extract",
"value_source.type.css_extract.desc": "Extracts color from a color strip source",
"value_source.gradient_map.input": "Input Value Source",
"value_source.gradient_map.gradient": "Gradient",
"value_source.css_extract.source": "Color Strip Source",
"value_source.ha_source": "HA Connection:",
"value_source.ha_source.hint": "Home Assistant connection to read entities from",
"value_source.entity_id": "Entity:",