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,
|
||||
GradientMapValueSourceResponse,
|
||||
HAEntityValueSourceResponse,
|
||||
HTTPValueSourceResponse,
|
||||
StaticColorValueSourceResponse,
|
||||
StaticValueSourceResponse,
|
||||
SystemMetricsValueSourceResponse,
|
||||
@@ -41,6 +42,7 @@ from ledgrab.storage.value_source import (
|
||||
DaylightValueSource,
|
||||
GradientMapValueSource,
|
||||
HAEntityValueSource,
|
||||
HTTPValueSource,
|
||||
StaticColorValueSource,
|
||||
StaticValueSource,
|
||||
SystemMetricsValueSource,
|
||||
@@ -213,6 +215,22 @@ _RESPONSE_MAP = {
|
||||
poll_interval=s.poll_interval,
|
||||
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)")
|
||||
|
||||
|
||||
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[
|
||||
Union[
|
||||
Annotated[StaticValueSourceResponse, Tag("static")],
|
||||
@@ -166,6 +177,7 @@ ValueSourceResponse = Annotated[
|
||||
Annotated[GradientMapValueSourceResponse, Tag("gradient_map")],
|
||||
Annotated[CSSExtractValueSourceResponse, Tag("css_extract")],
|
||||
Annotated[SystemMetricsValueSourceResponse, Tag("system_metrics")],
|
||||
Annotated[HTTPValueSourceResponse, Tag("http")],
|
||||
],
|
||||
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)
|
||||
|
||||
|
||||
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[
|
||||
Union[
|
||||
Annotated[StaticValueSourceCreate, Tag("static")],
|
||||
@@ -325,6 +347,7 @@ ValueSourceCreate = Annotated[
|
||||
Annotated[GradientMapValueSourceCreate, Tag("gradient_map")],
|
||||
Annotated[CSSExtractValueSourceCreate, Tag("css_extract")],
|
||||
Annotated[SystemMetricsValueSourceCreate, Tag("system_metrics")],
|
||||
Annotated[HTTPValueSourceCreate, Tag("http")],
|
||||
],
|
||||
Discriminator("source_type"),
|
||||
]
|
||||
@@ -463,6 +486,16 @@ class SystemMetricsValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
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[
|
||||
Union[
|
||||
Annotated[StaticValueSourceUpdate, Tag("static")],
|
||||
@@ -478,6 +511,7 @@ ValueSourceUpdate = Annotated[
|
||||
Annotated[GradientMapValueSourceUpdate, Tag("gradient_map")],
|
||||
Annotated[CSSExtractValueSourceUpdate, Tag("css_extract")],
|
||||
Annotated[SystemMetricsValueSourceUpdate, Tag("system_metrics")],
|
||||
Annotated[HTTPValueSourceUpdate, Tag("http")],
|
||||
],
|
||||
Discriminator("source_type"),
|
||||
]
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
_cachedHASources, _cachedColorStripSources, gradientsCache, GradientEntity,
|
||||
_cachedGameIntegrations, _cachedGameAdapters, gameIntegrationsCache, gameAdaptersCache,
|
||||
_cachedSyncClocks, syncClocksCache,
|
||||
_cachedHTTPEndpoints, httpEndpointsCache,
|
||||
getHAEntityFriendlyName, setHAEntityNames,
|
||||
} from '../core/state.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 _vsMetricIconSelect: IconSelect | null = null;
|
||||
let _vsAnimColorClockEntitySelect: EntitySelect | null = null;
|
||||
let _vsHTTPEndpointEntitySelect: EntitySelect | null = null;
|
||||
let _vsTagsInput: TagInput | null = null;
|
||||
|
||||
class ValueSourceModal extends Modal {
|
||||
@@ -92,6 +94,7 @@ class ValueSourceModal extends Modal {
|
||||
if (_vsMetricIconSelect) { _vsMetricIconSelect.destroy(); _vsMetricIconSelect = null; }
|
||||
if (_vsAnimColorClockEntitySelect) { _vsAnimColorClockEntitySelect.destroy(); _vsAnimColorClockEntitySelect = null; }
|
||||
if (_vsGameIntegrationEntitySelect) { _vsGameIntegrationEntitySelect.destroy(); _vsGameIntegrationEntitySelect = null; }
|
||||
if (_vsHTTPEndpointEntitySelect) { _vsHTTPEndpointEntitySelect.destroy(); _vsHTTPEndpointEntitySelect = null; }
|
||||
}
|
||||
|
||||
snapshotValues() {
|
||||
@@ -136,6 +139,13 @@ class ValueSourceModal extends Modal {
|
||||
sensorLabel: (document.getElementById('value-source-sensor-label') as HTMLInputElement).value,
|
||||
pollInterval: (document.getElementById('value-source-poll-interval') 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') {
|
||||
const eventType = (document.getElementById('value-source-game-event-type') as HTMLSelectElement)?.value;
|
||||
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;
|
||||
}
|
||||
|
||||
/* ── 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_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);
|
||||
}
|
||||
|
||||
/* ── 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 ─────────────────────────────────────────────────────
|
||||
|
||||
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-default', editData.default_value ?? 0.5);
|
||||
_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 {
|
||||
(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 = '';
|
||||
_setSlider('value-source-poll-interval', 1.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();
|
||||
}
|
||||
|
||||
@@ -699,6 +756,13 @@ export function onValueSourceTypeChange() {
|
||||
if (type === 'game_event') {
|
||||
_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 =
|
||||
(type === 'adaptive_time' || type === 'adaptive_scene' || type === 'daylight') ? '' : 'none';
|
||||
|
||||
@@ -755,6 +819,8 @@ function _syncDaylightVSSpeedVisibility() {
|
||||
|
||||
export async function saveValueSource() {
|
||||
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 sourceType = (document.getElementById('value-source-type') as HTMLSelectElement).value;
|
||||
const description = (document.getElementById('value-source-description') as HTMLInputElement).value.trim() || null;
|
||||
@@ -879,6 +945,23 @@ export async function saveValueSource() {
|
||||
errorEl.style.display = '';
|
||||
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 {
|
||||
|
||||
@@ -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 --
|
||||
# Maps source_type string to its subclass for factory dispatch.
|
||||
_VALUE_SOURCE_MAP: Dict[str, Type[ValueSource]] = {
|
||||
@@ -611,4 +660,5 @@ _VALUE_SOURCE_MAP: Dict[str, Type[ValueSource]] = {
|
||||
"css_extract": CSSExtractValueSource,
|
||||
"system_metrics": SystemMetricsValueSource,
|
||||
"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="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="http" data-i18n="value_source.type.http">HTTP Poll</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -637,6 +638,63 @@
|
||||
</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) -->
|
||||
<div id="value-source-adaptive-range-section" style="display:none">
|
||||
<div class="form-group">
|
||||
|
||||
Reference in New Issue
Block a user