feat(value-sources): extend storage + schema + UI alongside new kinds
Storage model + Pydantic schema + route gain fields for the value-source kinds introduced by the per-type factory refactor. Frontend editor adds inputs for them and the modal template grows new field rows.
This commit is contained in:
@@ -23,6 +23,7 @@ from ledgrab.api.schemas.value_sources import (
|
|||||||
DaylightValueSourceResponse,
|
DaylightValueSourceResponse,
|
||||||
GradientMapValueSourceResponse,
|
GradientMapValueSourceResponse,
|
||||||
HAEntityValueSourceResponse,
|
HAEntityValueSourceResponse,
|
||||||
|
HTTPValueSourceResponse,
|
||||||
StaticColorValueSourceResponse,
|
StaticColorValueSourceResponse,
|
||||||
StaticValueSourceResponse,
|
StaticValueSourceResponse,
|
||||||
SystemMetricsValueSourceResponse,
|
SystemMetricsValueSourceResponse,
|
||||||
@@ -41,6 +42,7 @@ from ledgrab.storage.value_source import (
|
|||||||
DaylightValueSource,
|
DaylightValueSource,
|
||||||
GradientMapValueSource,
|
GradientMapValueSource,
|
||||||
HAEntityValueSource,
|
HAEntityValueSource,
|
||||||
|
HTTPValueSource,
|
||||||
StaticColorValueSource,
|
StaticColorValueSource,
|
||||||
StaticValueSource,
|
StaticValueSource,
|
||||||
SystemMetricsValueSource,
|
SystemMetricsValueSource,
|
||||||
@@ -213,6 +215,22 @@ _RESPONSE_MAP = {
|
|||||||
poll_interval=s.poll_interval,
|
poll_interval=s.poll_interval,
|
||||||
smoothing=s.smoothing,
|
smoothing=s.smoothing,
|
||||||
),
|
),
|
||||||
|
HTTPValueSource: lambda s: HTTPValueSourceResponse(
|
||||||
|
id=s.id,
|
||||||
|
name=s.name,
|
||||||
|
description=s.description,
|
||||||
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
|
created_at=s.created_at,
|
||||||
|
updated_at=s.updated_at,
|
||||||
|
http_endpoint_id=s.http_endpoint_id,
|
||||||
|
json_path=s.json_path,
|
||||||
|
interval_s=s.interval_s,
|
||||||
|
min_value=s.min_value,
|
||||||
|
max_value=s.max_value,
|
||||||
|
smoothing=s.smoothing,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,17 @@ class SystemMetricsValueSourceResponse(_ValueSourceResponseBase):
|
|||||||
smoothing: float = Field(description="EMA smoothing factor (0.0-1.0)")
|
smoothing: float = Field(description="EMA smoothing factor (0.0-1.0)")
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPValueSourceResponse(_ValueSourceResponseBase):
|
||||||
|
source_type: Literal["http"] = "http"
|
||||||
|
return_type: Literal["float"] = "float"
|
||||||
|
http_endpoint_id: str = Field(description="HTTP endpoint ID")
|
||||||
|
json_path: str = Field(description="Dot-path into the response body")
|
||||||
|
interval_s: int = Field(description="Polling cadence (seconds)")
|
||||||
|
min_value: float = Field(description="Raw value mapped to output 0.0")
|
||||||
|
max_value: float = Field(description="Raw value mapped to output 1.0")
|
||||||
|
smoothing: float = Field(description="EMA smoothing factor (0.0-1.0)")
|
||||||
|
|
||||||
|
|
||||||
ValueSourceResponse = Annotated[
|
ValueSourceResponse = Annotated[
|
||||||
Union[
|
Union[
|
||||||
Annotated[StaticValueSourceResponse, Tag("static")],
|
Annotated[StaticValueSourceResponse, Tag("static")],
|
||||||
@@ -166,6 +177,7 @@ ValueSourceResponse = Annotated[
|
|||||||
Annotated[GradientMapValueSourceResponse, Tag("gradient_map")],
|
Annotated[GradientMapValueSourceResponse, Tag("gradient_map")],
|
||||||
Annotated[CSSExtractValueSourceResponse, Tag("css_extract")],
|
Annotated[CSSExtractValueSourceResponse, Tag("css_extract")],
|
||||||
Annotated[SystemMetricsValueSourceResponse, Tag("system_metrics")],
|
Annotated[SystemMetricsValueSourceResponse, Tag("system_metrics")],
|
||||||
|
Annotated[HTTPValueSourceResponse, Tag("http")],
|
||||||
],
|
],
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
@@ -310,6 +322,16 @@ class SystemMetricsValueSourceCreate(_ValueSourceCreateBase):
|
|||||||
smoothing: float = Field(0.0, description="EMA smoothing (0.0-1.0)", ge=0.0, le=1.0)
|
smoothing: float = Field(0.0, description="EMA smoothing (0.0-1.0)", ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPValueSourceCreate(_ValueSourceCreateBase):
|
||||||
|
source_type: Literal["http"] = "http"
|
||||||
|
http_endpoint_id: str = Field(description="HTTP endpoint ID")
|
||||||
|
json_path: str = Field("", description="Dot-path into the response (empty = raw body text)")
|
||||||
|
interval_s: int = Field(60, description="Polling cadence (seconds)", ge=1)
|
||||||
|
min_value: float = Field(0.0, description="Raw value mapped to output 0.0")
|
||||||
|
max_value: float = Field(100.0, description="Raw value mapped to output 1.0")
|
||||||
|
smoothing: float = Field(0.0, description="EMA smoothing (0.0-1.0)", ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
|
||||||
ValueSourceCreate = Annotated[
|
ValueSourceCreate = Annotated[
|
||||||
Union[
|
Union[
|
||||||
Annotated[StaticValueSourceCreate, Tag("static")],
|
Annotated[StaticValueSourceCreate, Tag("static")],
|
||||||
@@ -325,6 +347,7 @@ ValueSourceCreate = Annotated[
|
|||||||
Annotated[GradientMapValueSourceCreate, Tag("gradient_map")],
|
Annotated[GradientMapValueSourceCreate, Tag("gradient_map")],
|
||||||
Annotated[CSSExtractValueSourceCreate, Tag("css_extract")],
|
Annotated[CSSExtractValueSourceCreate, Tag("css_extract")],
|
||||||
Annotated[SystemMetricsValueSourceCreate, Tag("system_metrics")],
|
Annotated[SystemMetricsValueSourceCreate, Tag("system_metrics")],
|
||||||
|
Annotated[HTTPValueSourceCreate, Tag("http")],
|
||||||
],
|
],
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
@@ -463,6 +486,16 @@ class SystemMetricsValueSourceUpdate(_ValueSourceUpdateBase):
|
|||||||
smoothing: Optional[float] = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
smoothing: Optional[float] = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
|
source_type: Literal["http"] = "http"
|
||||||
|
http_endpoint_id: Optional[str] = Field(None, description="HTTP endpoint ID")
|
||||||
|
json_path: Optional[str] = Field(None, description="Dot-path into the response")
|
||||||
|
interval_s: Optional[int] = Field(None, description="Polling cadence (seconds)", ge=1)
|
||||||
|
min_value: Optional[float] = Field(None, description="Raw value mapped to 0.0")
|
||||||
|
max_value: Optional[float] = Field(None, description="Raw value mapped to 1.0")
|
||||||
|
smoothing: Optional[float] = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
|
||||||
ValueSourceUpdate = Annotated[
|
ValueSourceUpdate = Annotated[
|
||||||
Union[
|
Union[
|
||||||
Annotated[StaticValueSourceUpdate, Tag("static")],
|
Annotated[StaticValueSourceUpdate, Tag("static")],
|
||||||
@@ -478,6 +511,7 @@ ValueSourceUpdate = Annotated[
|
|||||||
Annotated[GradientMapValueSourceUpdate, Tag("gradient_map")],
|
Annotated[GradientMapValueSourceUpdate, Tag("gradient_map")],
|
||||||
Annotated[CSSExtractValueSourceUpdate, Tag("css_extract")],
|
Annotated[CSSExtractValueSourceUpdate, Tag("css_extract")],
|
||||||
Annotated[SystemMetricsValueSourceUpdate, Tag("system_metrics")],
|
Annotated[SystemMetricsValueSourceUpdate, Tag("system_metrics")],
|
||||||
|
Annotated[HTTPValueSourceUpdate, Tag("http")],
|
||||||
],
|
],
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
_cachedHASources, _cachedColorStripSources, gradientsCache, GradientEntity,
|
_cachedHASources, _cachedColorStripSources, gradientsCache, GradientEntity,
|
||||||
_cachedGameIntegrations, _cachedGameAdapters, gameIntegrationsCache, gameAdaptersCache,
|
_cachedGameIntegrations, _cachedGameAdapters, gameIntegrationsCache, gameAdaptersCache,
|
||||||
_cachedSyncClocks, syncClocksCache,
|
_cachedSyncClocks, syncClocksCache,
|
||||||
|
_cachedHTTPEndpoints, httpEndpointsCache,
|
||||||
getHAEntityFriendlyName, setHAEntityNames,
|
getHAEntityFriendlyName, setHAEntityNames,
|
||||||
} from '../core/state.ts';
|
} from '../core/state.ts';
|
||||||
import { API_BASE, fetchWithAuth, escapeHtml } from '../core/api.ts';
|
import { API_BASE, fetchWithAuth, escapeHtml } from '../core/api.ts';
|
||||||
@@ -74,6 +75,7 @@ let _vsGradientEasingIconSelect: IconSelect | null = null;
|
|||||||
let _vsBehaviorIconSelect: IconSelect | null = null;
|
let _vsBehaviorIconSelect: IconSelect | null = null;
|
||||||
let _vsMetricIconSelect: IconSelect | null = null;
|
let _vsMetricIconSelect: IconSelect | null = null;
|
||||||
let _vsAnimColorClockEntitySelect: EntitySelect | null = null;
|
let _vsAnimColorClockEntitySelect: EntitySelect | null = null;
|
||||||
|
let _vsHTTPEndpointEntitySelect: EntitySelect | null = null;
|
||||||
let _vsTagsInput: TagInput | null = null;
|
let _vsTagsInput: TagInput | null = null;
|
||||||
|
|
||||||
class ValueSourceModal extends Modal {
|
class ValueSourceModal extends Modal {
|
||||||
@@ -92,6 +94,7 @@ class ValueSourceModal extends Modal {
|
|||||||
if (_vsMetricIconSelect) { _vsMetricIconSelect.destroy(); _vsMetricIconSelect = null; }
|
if (_vsMetricIconSelect) { _vsMetricIconSelect.destroy(); _vsMetricIconSelect = null; }
|
||||||
if (_vsAnimColorClockEntitySelect) { _vsAnimColorClockEntitySelect.destroy(); _vsAnimColorClockEntitySelect = null; }
|
if (_vsAnimColorClockEntitySelect) { _vsAnimColorClockEntitySelect.destroy(); _vsAnimColorClockEntitySelect = null; }
|
||||||
if (_vsGameIntegrationEntitySelect) { _vsGameIntegrationEntitySelect.destroy(); _vsGameIntegrationEntitySelect = null; }
|
if (_vsGameIntegrationEntitySelect) { _vsGameIntegrationEntitySelect.destroy(); _vsGameIntegrationEntitySelect = null; }
|
||||||
|
if (_vsHTTPEndpointEntitySelect) { _vsHTTPEndpointEntitySelect.destroy(); _vsHTTPEndpointEntitySelect = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshotValues() {
|
snapshotValues() {
|
||||||
@@ -136,6 +139,13 @@ class ValueSourceModal extends Modal {
|
|||||||
sensorLabel: (document.getElementById('value-source-sensor-label') as HTMLInputElement).value,
|
sensorLabel: (document.getElementById('value-source-sensor-label') as HTMLInputElement).value,
|
||||||
pollInterval: (document.getElementById('value-source-poll-interval') as HTMLInputElement).value,
|
pollInterval: (document.getElementById('value-source-poll-interval') as HTMLInputElement).value,
|
||||||
sysmetricSmoothing: (document.getElementById('value-source-sysmetric-smoothing') as HTMLInputElement).value,
|
sysmetricSmoothing: (document.getElementById('value-source-sysmetric-smoothing') as HTMLInputElement).value,
|
||||||
|
// HTTP value source
|
||||||
|
httpEndpoint: (document.getElementById('value-source-http-endpoint') as HTMLSelectElement | null)?.value || '',
|
||||||
|
httpJsonPath: (document.getElementById('value-source-http-json-path') as HTMLInputElement | null)?.value || '',
|
||||||
|
httpInterval: (document.getElementById('value-source-http-interval') as HTMLInputElement | null)?.value || '',
|
||||||
|
httpMin: (document.getElementById('value-source-http-min') as HTMLInputElement | null)?.value || '',
|
||||||
|
httpMax: (document.getElementById('value-source-http-max') as HTMLInputElement | null)?.value || '',
|
||||||
|
httpSmoothing: (document.getElementById('value-source-http-smoothing') as HTMLInputElement | null)?.value || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,13 +178,17 @@ function _autoGenerateVSName() {
|
|||||||
} else if (type === 'game_event') {
|
} else if (type === 'game_event') {
|
||||||
const eventType = (document.getElementById('value-source-game-event-type') as HTMLSelectElement)?.value;
|
const eventType = (document.getElementById('value-source-game-event-type') as HTMLSelectElement)?.value;
|
||||||
if (eventType) detail = eventType;
|
if (eventType) detail = eventType;
|
||||||
|
} else if (type === 'http') {
|
||||||
|
const sel = document.getElementById('value-source-http-endpoint') as HTMLSelectElement | null;
|
||||||
|
const name = sel?.selectedOptions[0]?.textContent?.trim();
|
||||||
|
if (name) detail = name;
|
||||||
}
|
}
|
||||||
(document.getElementById('value-source-name') as HTMLInputElement).value = detail ? `${typeLabel} · ${detail}` : typeLabel;
|
(document.getElementById('value-source-name') as HTMLInputElement).value = detail ? `${typeLabel} · ${detail}` : typeLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Icon-grid type selector ──────────────────────────────────── */
|
/* ── Icon-grid type selector ──────────────────────────────────── */
|
||||||
|
|
||||||
const VS_FLOAT_TYPE_KEYS = ['static', 'animated', 'audio', 'adaptive_time', 'adaptive_scene', 'daylight', 'ha_entity', 'system_metrics', 'game_event'];
|
const VS_FLOAT_TYPE_KEYS = ['static', 'animated', 'audio', 'adaptive_time', 'adaptive_scene', 'daylight', 'ha_entity', 'system_metrics', 'game_event', 'http'];
|
||||||
const VS_COLOR_TYPE_KEYS = ['static_color', 'animated_color', 'adaptive_time_color', 'gradient_map', 'css_extract'];
|
const VS_COLOR_TYPE_KEYS = ['static_color', 'animated_color', 'adaptive_time_color', 'gradient_map', 'css_extract'];
|
||||||
const VS_TYPE_KEYS = [...VS_FLOAT_TYPE_KEYS, ...VS_COLOR_TYPE_KEYS];
|
const VS_TYPE_KEYS = [...VS_FLOAT_TYPE_KEYS, ...VS_COLOR_TYPE_KEYS];
|
||||||
|
|
||||||
@@ -446,6 +460,31 @@ function _ensureVSTypeIconSelect() {
|
|||||||
_vsTypeIconSelect = new IconSelect({ target: sel, items: _buildVSTypeItems(), columns: 2 } as any);
|
_vsTypeIconSelect = new IconSelect({ target: sel, items: _buildVSTypeItems(), columns: 2 } as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── HTTP endpoint picker (EntitySelect over httpEndpointsCache) ── */
|
||||||
|
|
||||||
|
function _populateVSHTTPEndpointDropdown(selectedId: string = '') {
|
||||||
|
const sel = document.getElementById('value-source-http-endpoint') as HTMLSelectElement;
|
||||||
|
if (!sel) return;
|
||||||
|
const endpoints = _cachedHTTPEndpoints || [];
|
||||||
|
const prev = selectedId || sel.value;
|
||||||
|
sel.innerHTML = `<option value="">—</option>` +
|
||||||
|
endpoints.map(e => `<option value="${e.id}"${e.id === prev ? ' selected' : ''}>${escapeHtml(e.name)}</option>`).join('');
|
||||||
|
sel.value = prev || '';
|
||||||
|
|
||||||
|
if (_vsHTTPEndpointEntitySelect) _vsHTTPEndpointEntitySelect.destroy();
|
||||||
|
_vsHTTPEndpointEntitySelect = new EntitySelect({
|
||||||
|
target: sel,
|
||||||
|
getItems: () => (_cachedHTTPEndpoints || []).map(e => ({
|
||||||
|
value: e.id,
|
||||||
|
label: e.name,
|
||||||
|
icon: `<svg class="icon" viewBox="0 0 24 24">${P.globe}</svg>`,
|
||||||
|
desc: `${e.method} ${e.url}`,
|
||||||
|
})),
|
||||||
|
placeholder: t('palette.search'),
|
||||||
|
onChange: () => _autoGenerateVSName(),
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Modal ─────────────────────────────────────────────────────
|
// ── Modal ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function showValueSourceModal(editData: any, presetType: any = null) {
|
export async function showValueSourceModal(editData: any, presetType: any = null) {
|
||||||
@@ -586,6 +625,14 @@ export async function showValueSourceModal(editData: any, presetType: any = null
|
|||||||
_setSlider('value-source-ge-smoothing', editData.smoothing ?? 0);
|
_setSlider('value-source-ge-smoothing', editData.smoothing ?? 0);
|
||||||
_setSlider('value-source-ge-default', editData.default_value ?? 0.5);
|
_setSlider('value-source-ge-default', editData.default_value ?? 0.5);
|
||||||
_setSlider('value-source-ge-timeout', editData.timeout ?? 5.0);
|
_setSlider('value-source-ge-timeout', editData.timeout ?? 5.0);
|
||||||
|
} else if (editData.source_type === 'http') {
|
||||||
|
await httpEndpointsCache.fetch();
|
||||||
|
_populateVSHTTPEndpointDropdown(editData.http_endpoint_id || '');
|
||||||
|
(document.getElementById('value-source-http-json-path') as HTMLInputElement).value = editData.json_path || '';
|
||||||
|
(document.getElementById('value-source-http-interval') as HTMLInputElement).value = String(editData.interval_s ?? 60);
|
||||||
|
(document.getElementById('value-source-http-min') as HTMLInputElement).value = String(editData.min_value ?? 0);
|
||||||
|
(document.getElementById('value-source-http-max') as HTMLInputElement).value = String(editData.max_value ?? 100);
|
||||||
|
_setSlider('value-source-http-smoothing', editData.smoothing ?? 0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(document.getElementById('value-source-name') as HTMLInputElement).value = '';
|
(document.getElementById('value-source-name') as HTMLInputElement).value = '';
|
||||||
@@ -650,6 +697,16 @@ export async function showValueSourceModal(editData: any, presetType: any = null
|
|||||||
(document.getElementById('value-source-sensor-label') as HTMLInputElement).value = '';
|
(document.getElementById('value-source-sensor-label') as HTMLInputElement).value = '';
|
||||||
_setSlider('value-source-poll-interval', 1.0);
|
_setSlider('value-source-poll-interval', 1.0);
|
||||||
_setSlider('value-source-sysmetric-smoothing', 0);
|
_setSlider('value-source-sysmetric-smoothing', 0);
|
||||||
|
// HTTP value source defaults
|
||||||
|
const httpJsonPath = document.getElementById('value-source-http-json-path') as HTMLInputElement | null;
|
||||||
|
if (httpJsonPath) httpJsonPath.value = '';
|
||||||
|
const httpInterval = document.getElementById('value-source-http-interval') as HTMLInputElement | null;
|
||||||
|
if (httpInterval) httpInterval.value = '60';
|
||||||
|
const httpMin = document.getElementById('value-source-http-min') as HTMLInputElement | null;
|
||||||
|
if (httpMin) httpMin.value = '0';
|
||||||
|
const httpMax = document.getElementById('value-source-http-max') as HTMLInputElement | null;
|
||||||
|
if (httpMax) httpMax.value = '100';
|
||||||
|
_setSlider('value-source-http-smoothing', 0);
|
||||||
_autoGenerateVSName();
|
_autoGenerateVSName();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,6 +756,13 @@ export function onValueSourceTypeChange() {
|
|||||||
if (type === 'game_event') {
|
if (type === 'game_event') {
|
||||||
_populateVSGameIntegrationDropdown('');
|
_populateVSGameIntegrationDropdown('');
|
||||||
}
|
}
|
||||||
|
const httpSec = document.getElementById('value-source-http-section') as HTMLElement | null;
|
||||||
|
if (httpSec) httpSec.style.display = type === 'http' ? '' : 'none';
|
||||||
|
if (type === 'http') {
|
||||||
|
// Refresh endpoint list lazily — value-source modal can be opened
|
||||||
|
// before the integrations tab has been visited.
|
||||||
|
httpEndpointsCache.fetch().then(() => _populateVSHTTPEndpointDropdown(''));
|
||||||
|
}
|
||||||
(document.getElementById('value-source-adaptive-range-section') as HTMLElement).style.display =
|
(document.getElementById('value-source-adaptive-range-section') as HTMLElement).style.display =
|
||||||
(type === 'adaptive_time' || type === 'adaptive_scene' || type === 'daylight') ? '' : 'none';
|
(type === 'adaptive_time' || type === 'adaptive_scene' || type === 'daylight') ? '' : 'none';
|
||||||
|
|
||||||
@@ -755,6 +819,8 @@ function _syncDaylightVSSpeedVisibility() {
|
|||||||
|
|
||||||
export async function saveValueSource() {
|
export async function saveValueSource() {
|
||||||
const id = (document.getElementById('value-source-id') as HTMLInputElement).value;
|
const id = (document.getElementById('value-source-id') as HTMLInputElement).value;
|
||||||
|
if (valueSourceModal.closeIfPristine(id)) return;
|
||||||
|
|
||||||
const name = (document.getElementById('value-source-name') as HTMLInputElement).value.trim();
|
const name = (document.getElementById('value-source-name') as HTMLInputElement).value.trim();
|
||||||
const sourceType = (document.getElementById('value-source-type') as HTMLSelectElement).value;
|
const sourceType = (document.getElementById('value-source-type') as HTMLSelectElement).value;
|
||||||
const description = (document.getElementById('value-source-description') as HTMLInputElement).value.trim() || null;
|
const description = (document.getElementById('value-source-description') as HTMLInputElement).value.trim() || null;
|
||||||
@@ -879,6 +945,23 @@ export async function saveValueSource() {
|
|||||||
errorEl.style.display = '';
|
errorEl.style.display = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (sourceType === 'http') {
|
||||||
|
payload.http_endpoint_id = (document.getElementById('value-source-http-endpoint') as HTMLSelectElement).value;
|
||||||
|
payload.json_path = (document.getElementById('value-source-http-json-path') as HTMLInputElement).value.trim();
|
||||||
|
payload.interval_s = parseInt((document.getElementById('value-source-http-interval') as HTMLInputElement).value, 10) || 60;
|
||||||
|
payload.min_value = parseFloat((document.getElementById('value-source-http-min') as HTMLInputElement).value) || 0;
|
||||||
|
payload.max_value = parseFloat((document.getElementById('value-source-http-max') as HTMLInputElement).value) || 100;
|
||||||
|
payload.smoothing = parseFloat((document.getElementById('value-source-http-smoothing') as HTMLInputElement).value) || 0;
|
||||||
|
if (!payload.http_endpoint_id) {
|
||||||
|
errorEl.textContent = t('value_source.http.endpoint_required');
|
||||||
|
errorEl.style.display = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (payload.interval_s < 1) {
|
||||||
|
errorEl.textContent = t('value_source.http.interval_invalid');
|
||||||
|
errorEl.style.display = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -594,6 +594,55 @@ class SystemMetricsValueSource(ValueSource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HTTPValueSource(ValueSource):
|
||||||
|
"""Value source that periodically polls an HTTP endpoint and extracts
|
||||||
|
a value from the response.
|
||||||
|
|
||||||
|
The endpoint owns "where + how to reach it" (URL, auth, headers);
|
||||||
|
this source owns "what to extract + how often + how to normalize".
|
||||||
|
Exposes two accessors at runtime:
|
||||||
|
|
||||||
|
- ``get_value()`` returns a normalized float in [0, 1] suitable for
|
||||||
|
driving brightness/color modulators. Non-numeric responses or
|
||||||
|
missing paths yield 0.0.
|
||||||
|
- ``get_raw_value()`` returns the raw extracted value (str / int /
|
||||||
|
float / bool / None) for consumers that need the un-normalized
|
||||||
|
data — automation rules compare against this.
|
||||||
|
"""
|
||||||
|
|
||||||
|
http_endpoint_id: str = "" # references an HTTPEndpoint
|
||||||
|
json_path: str = "" # dot-path; empty = use raw body text
|
||||||
|
interval_s: int = 60 # polling cadence
|
||||||
|
min_value: float = 0.0 # raw value → 0.0
|
||||||
|
max_value: float = 100.0 # raw value → 1.0
|
||||||
|
smoothing: float = 0.0 # EMA smoothing on the normalized output
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
d = super().to_dict()
|
||||||
|
d["http_endpoint_id"] = self.http_endpoint_id
|
||||||
|
d["json_path"] = self.json_path
|
||||||
|
d["interval_s"] = self.interval_s
|
||||||
|
d["min_value"] = self.min_value
|
||||||
|
d["max_value"] = self.max_value
|
||||||
|
d["smoothing"] = self.smoothing
|
||||||
|
return d
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict) -> "HTTPValueSource":
|
||||||
|
common = _parse_common_fields(data)
|
||||||
|
return cls(
|
||||||
|
**common,
|
||||||
|
source_type="http",
|
||||||
|
http_endpoint_id=data.get("http_endpoint_id") or "",
|
||||||
|
json_path=data.get("json_path") or "",
|
||||||
|
interval_s=int(data.get("interval_s") or 60),
|
||||||
|
min_value=float(data.get("min_value") or 0.0),
|
||||||
|
max_value=float(data.get("max_value") if data.get("max_value") is not None else 100.0),
|
||||||
|
smoothing=float(data.get("smoothing") or 0.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -- Source type registry --
|
# -- Source type registry --
|
||||||
# Maps source_type string to its subclass for factory dispatch.
|
# Maps source_type string to its subclass for factory dispatch.
|
||||||
_VALUE_SOURCE_MAP: Dict[str, Type[ValueSource]] = {
|
_VALUE_SOURCE_MAP: Dict[str, Type[ValueSource]] = {
|
||||||
@@ -611,4 +660,5 @@ _VALUE_SOURCE_MAP: Dict[str, Type[ValueSource]] = {
|
|||||||
"css_extract": CSSExtractValueSource,
|
"css_extract": CSSExtractValueSource,
|
||||||
"system_metrics": SystemMetricsValueSource,
|
"system_metrics": SystemMetricsValueSource,
|
||||||
"game_event": GameEventValueSource,
|
"game_event": GameEventValueSource,
|
||||||
|
"http": HTTPValueSource,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
<option value="gradient_map" data-i18n="value_source.type.gradient_map">Gradient Map</option>
|
<option value="gradient_map" data-i18n="value_source.type.gradient_map">Gradient Map</option>
|
||||||
<option value="css_extract" data-i18n="value_source.type.css_extract">Strip Extract</option>
|
<option value="css_extract" data-i18n="value_source.type.css_extract">Strip Extract</option>
|
||||||
<option value="system_metrics" data-i18n="value_source.type.system_metrics">System Metrics</option>
|
<option value="system_metrics" data-i18n="value_source.type.system_metrics">System Metrics</option>
|
||||||
|
<option value="http" data-i18n="value_source.type.http">HTTP Poll</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -637,6 +638,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- HTTP value source fields -->
|
||||||
|
<div id="value-source-http-section" style="display:none">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="value-source-http-endpoint" data-i18n="value_source.http.endpoint">HTTP Endpoint:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="value_source.http.endpoint.hint">Pick a saved endpoint from the HTTP integrations tab.</small>
|
||||||
|
<select id="value-source-http-endpoint">
|
||||||
|
<option value="">—</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="value-source-http-json-path" data-i18n="value_source.http.json_path">JSON Path:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="value_source.http.json_path.hint">Empty = use raw response body. Use dotted/indexed path, e.g. MediaContainer.Metadata[0].title</small>
|
||||||
|
<input type="text" id="value-source-http-json-path" placeholder="MediaContainer.Metadata[0].title">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="value-source-http-interval" data-i18n="value_source.http.interval">Interval (s):</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="value_source.http.interval.hint">Polling cadence in seconds. Multiple value sources can share one endpoint at different intervals.</small>
|
||||||
|
<input type="number" id="value-source-http-interval" min="1" step="1" value="60">
|
||||||
|
</div>
|
||||||
|
<details class="form-collapse">
|
||||||
|
<summary data-i18n="value_source.http.modulator.summary">Modulator mapping (optional)</summary>
|
||||||
|
<div class="form-collapse-body">
|
||||||
|
<small class="input-hint" data-i18n="value_source.http.modulator.hint">Only used when this source drives brightness or color. Automation rules read the raw extracted value and ignore these settings.</small>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="value-source-http-min" data-i18n="value_source.http.min_value">Min Value:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="value_source.http.min_value.hint">Raw extracted value that maps to output 0.0 (for normalisation).</small>
|
||||||
|
<input type="number" id="value-source-http-min" step="any" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="value-source-http-max" data-i18n="value_source.http.max_value">Max Value:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="value_source.http.max_value.hint">Raw extracted value that maps to output 1.0 (for normalisation).</small>
|
||||||
|
<input type="number" id="value-source-http-max" step="any" value="100">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="value-source-http-smoothing"><span data-i18n="value_source.smoothing">Smoothing:</span> <span id="value-source-http-smoothing-display">0</span></label>
|
||||||
|
<input type="range" id="value-source-http-smoothing" min="0" max="0.99" step="0.01" value="0"
|
||||||
|
oninput="document.getElementById('value-source-http-smoothing-display').textContent = this.value">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Shared adaptive output range (shown for adaptive and daylight types) -->
|
<!-- Shared adaptive output range (shown for adaptive and daylight types) -->
|
||||||
<div id="value-source-adaptive-range-section" style="display:none">
|
<div id="value-source-adaptive-range-section" style="display:none">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user