Split adaptive value source into explicit adaptive_time and adaptive_scene types

Replace single "adaptive" type with adaptive_mode sub-selector by two
distinct source types in the dropdown. Removes the adaptive_mode field
entirely — the source_type itself carries the mode. Clearer UX with
"Adaptive (Time of Day)" and "Adaptive (Scene)" as separate options.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 15:23:50 +03:00
parent d339dd3f90
commit 1e4a7a067f
10 changed files with 142 additions and 170 deletions

View File

@@ -43,7 +43,6 @@ def _to_response(source: ValueSource) -> ValueSourceResponse:
mode=d.get("mode"), mode=d.get("mode"),
sensitivity=d.get("sensitivity"), sensitivity=d.get("sensitivity"),
smoothing=d.get("smoothing"), smoothing=d.get("smoothing"),
adaptive_mode=d.get("adaptive_mode"),
schedule=d.get("schedule"), schedule=d.get("schedule"),
picture_source_id=d.get("picture_source_id"), picture_source_id=d.get("picture_source_id"),
scene_behavior=d.get("scene_behavior"), scene_behavior=d.get("scene_behavior"),
@@ -56,7 +55,7 @@ def _to_response(source: ValueSource) -> ValueSourceResponse:
@router.get("/api/v1/value-sources", response_model=ValueSourceListResponse, tags=["Value Sources"]) @router.get("/api/v1/value-sources", response_model=ValueSourceListResponse, tags=["Value Sources"])
async def list_value_sources( async def list_value_sources(
_auth: AuthRequired, _auth: AuthRequired,
source_type: Optional[str] = Query(None, description="Filter by source_type: static, animated, or audio"), source_type: Optional[str] = Query(None, description="Filter by source_type: static, animated, audio, adaptive_time, or adaptive_scene"),
store: ValueSourceStore = Depends(get_value_source_store), store: ValueSourceStore = Depends(get_value_source_store),
): ):
"""List all value sources, optionally filtered by type.""" """List all value sources, optionally filtered by type."""
@@ -90,7 +89,6 @@ async def create_value_source(
sensitivity=data.sensitivity, sensitivity=data.sensitivity,
smoothing=data.smoothing, smoothing=data.smoothing,
description=data.description, description=data.description,
adaptive_mode=data.adaptive_mode,
schedule=data.schedule, schedule=data.schedule,
picture_source_id=data.picture_source_id, picture_source_id=data.picture_source_id,
scene_behavior=data.scene_behavior, scene_behavior=data.scene_behavior,
@@ -137,7 +135,6 @@ async def update_value_source(
sensitivity=data.sensitivity, sensitivity=data.sensitivity,
smoothing=data.smoothing, smoothing=data.smoothing,
description=data.description, description=data.description,
adaptive_mode=data.adaptive_mode,
schedule=data.schedule, schedule=data.schedule,
picture_source_id=data.picture_source_id, picture_source_id=data.picture_source_id,
scene_behavior=data.scene_behavior, scene_behavior=data.scene_behavior,

View File

@@ -10,7 +10,7 @@ class ValueSourceCreate(BaseModel):
"""Request to create a value source.""" """Request to create a value source."""
name: str = Field(description="Source name", min_length=1, max_length=100) name: str = Field(description="Source name", min_length=1, max_length=100)
source_type: Literal["static", "animated", "audio", "adaptive"] = Field(description="Source type") source_type: Literal["static", "animated", "audio", "adaptive_time", "adaptive_scene"] = Field(description="Source type")
# static fields # static fields
value: Optional[float] = Field(None, description="Constant value (0.0-1.0)", ge=0.0, le=1.0) value: Optional[float] = Field(None, description="Constant value (0.0-1.0)", ge=0.0, le=1.0)
# animated fields # animated fields
@@ -24,7 +24,6 @@ class ValueSourceCreate(BaseModel):
sensitivity: Optional[float] = Field(None, description="Gain multiplier (0.1-5.0)", ge=0.1, le=5.0) sensitivity: Optional[float] = Field(None, description="Gain multiplier (0.1-5.0)", ge=0.1, le=5.0)
smoothing: Optional[float] = Field(None, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0) smoothing: Optional[float] = Field(None, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0)
# adaptive fields # adaptive fields
adaptive_mode: Optional[str] = Field(None, description="Adaptive mode: time_of_day|scene")
schedule: Optional[list] = Field(None, description="Time-of-day schedule: [{time: 'HH:MM', value: 0.0-1.0}]") schedule: Optional[list] = Field(None, description="Time-of-day schedule: [{time: 'HH:MM', value: 0.0-1.0}]")
picture_source_id: Optional[str] = Field(None, description="Picture source ID for scene mode") picture_source_id: Optional[str] = Field(None, description="Picture source ID for scene mode")
scene_behavior: Optional[str] = Field(None, description="Scene behavior: complement|match") scene_behavior: Optional[str] = Field(None, description="Scene behavior: complement|match")
@@ -48,7 +47,6 @@ class ValueSourceUpdate(BaseModel):
sensitivity: Optional[float] = Field(None, description="Gain multiplier (0.1-5.0)", ge=0.1, le=5.0) sensitivity: Optional[float] = Field(None, description="Gain multiplier (0.1-5.0)", ge=0.1, le=5.0)
smoothing: Optional[float] = Field(None, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0) smoothing: Optional[float] = Field(None, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0)
# adaptive fields # adaptive fields
adaptive_mode: Optional[str] = Field(None, description="Adaptive mode: time_of_day|scene")
schedule: Optional[list] = Field(None, description="Time-of-day schedule") schedule: Optional[list] = Field(None, description="Time-of-day schedule")
picture_source_id: Optional[str] = Field(None, description="Picture source ID for scene mode") picture_source_id: Optional[str] = Field(None, description="Picture source ID for scene mode")
scene_behavior: Optional[str] = Field(None, description="Scene behavior: complement|match") scene_behavior: Optional[str] = Field(None, description="Scene behavior: complement|match")
@@ -60,7 +58,7 @@ class ValueSourceResponse(BaseModel):
id: str = Field(description="Source ID") id: str = Field(description="Source ID")
name: str = Field(description="Source name") name: str = Field(description="Source name")
source_type: str = Field(description="Source type: static, animated, audio, or adaptive") source_type: str = Field(description="Source type: static, animated, audio, adaptive_time, or adaptive_scene")
value: Optional[float] = Field(None, description="Static value") value: Optional[float] = Field(None, description="Static value")
waveform: Optional[str] = Field(None, description="Waveform type") waveform: Optional[str] = Field(None, description="Waveform type")
speed: Optional[float] = Field(None, description="Cycles per minute") speed: Optional[float] = Field(None, description="Cycles per minute")
@@ -70,7 +68,6 @@ class ValueSourceResponse(BaseModel):
mode: Optional[str] = Field(None, description="Audio mode") mode: Optional[str] = Field(None, description="Audio mode")
sensitivity: Optional[float] = Field(None, description="Gain multiplier") sensitivity: Optional[float] = Field(None, description="Gain multiplier")
smoothing: Optional[float] = Field(None, description="Temporal smoothing") smoothing: Optional[float] = Field(None, description="Temporal smoothing")
adaptive_mode: Optional[str] = Field(None, description="Adaptive mode")
schedule: Optional[list] = Field(None, description="Time-of-day schedule") schedule: Optional[list] = Field(None, description="Time-of-day schedule")
picture_source_id: Optional[str] = Field(None, description="Picture source ID") picture_source_id: Optional[str] = Field(None, description="Picture source ID")
scene_behavior: Optional[str] = Field(None, description="Scene behavior") scene_behavior: Optional[str] = Field(None, description="Scene behavior")

View File

@@ -7,8 +7,8 @@ on demand via ``get_value()``. Five concrete types:
AnimatedValueStream — evaluates a periodic waveform (sine/triangle/square/sawtooth) AnimatedValueStream — evaluates a periodic waveform (sine/triangle/square/sawtooth)
AudioValueStream — polls audio analysis for RMS/peak/beat, applies AudioValueStream — polls audio analysis for RMS/peak/beat, applies
sensitivity and temporal smoothing sensitivity and temporal smoothing
TimeOfDayValueStream — interpolates brightness along a 24h schedule TimeOfDayValueStream — interpolates brightness along a 24h schedule (adaptive_time)
SceneValueStream — derives brightness from a picture source's frame luminance SceneValueStream — derives brightness from a picture source's frame luminance (adaptive_scene)
ValueStreams are cheap (trivial math or single poll), so they compute inline ValueStreams are cheap (trivial math or single poll), so they compute inline
in the caller's processing loop — no background threads required. in the caller's processing loop — no background threads required.
@@ -362,7 +362,7 @@ class TimeOfDayValueStream(ValueStream):
def update_source(self, source: "ValueSource") -> None: def update_source(self, source: "ValueSource") -> None:
from wled_controller.storage.value_source import AdaptiveValueSource from wled_controller.storage.value_source import AdaptiveValueSource
if isinstance(source, AdaptiveValueSource) and source.adaptive_mode == "time_of_day": if isinstance(source, AdaptiveValueSource) and source.source_type == "adaptive_time":
self._parse_schedule(source.schedule) self._parse_schedule(source.schedule)
self._min = source.min_value self._min = source.min_value
self._max = source.max_value self._max = source.max_value
@@ -463,7 +463,7 @@ class SceneValueStream(ValueStream):
def update_source(self, source: "ValueSource") -> None: def update_source(self, source: "ValueSource") -> None:
from wled_controller.storage.value_source import AdaptiveValueSource from wled_controller.storage.value_source import AdaptiveValueSource
if not isinstance(source, AdaptiveValueSource) or source.adaptive_mode != "scene": if not isinstance(source, AdaptiveValueSource) or source.source_type != "adaptive_scene":
return return
self._behavior = source.scene_behavior self._behavior = source.scene_behavior
@@ -603,7 +603,7 @@ class ValueStreamManager:
) )
if isinstance(source, AdaptiveValueSource): if isinstance(source, AdaptiveValueSource):
if source.adaptive_mode == "scene": if source.source_type == "adaptive_scene":
return SceneValueStream( return SceneValueStream(
picture_source_id=source.picture_source_id, picture_source_id=source.picture_source_id,
scene_behavior=source.scene_behavior, scene_behavior=source.scene_behavior,
@@ -613,7 +613,6 @@ class ValueStreamManager:
max_value=source.max_value, max_value=source.max_value,
live_stream_manager=self._live_stream_manager, live_stream_manager=self._live_stream_manager,
) )
# Default: time_of_day
return TimeOfDayValueStream( return TimeOfDayValueStream(
schedule=source.schedule, schedule=source.schedule,
min_value=source.min_value, min_value=source.min_value,

View File

@@ -112,7 +112,7 @@ import {
import { import {
showValueSourceModal, closeValueSourceModal, saveValueSource, showValueSourceModal, closeValueSourceModal, saveValueSource,
editValueSource, deleteValueSource, onValueSourceTypeChange, editValueSource, deleteValueSource, onValueSourceTypeChange,
onAdaptiveModeChange, addSchedulePoint, addSchedulePoint,
} from './features/value-sources.js'; } from './features/value-sources.js';
// Layer 5: calibration // Layer 5: calibration
@@ -331,7 +331,6 @@ Object.assign(window, {
editValueSource, editValueSource,
deleteValueSource, deleteValueSource,
onValueSourceTypeChange, onValueSourceTypeChange,
onAdaptiveModeChange,
addSchedulePoint, addSchedulePoint,
// calibration // calibration

View File

@@ -1,9 +1,10 @@
/** /**
* Value Sources — CRUD for scalar value sources (static, animated, audio, adaptive). * Value Sources — CRUD for scalar value sources (static, animated, audio, adaptive_time, adaptive_scene).
* *
* Value sources produce a float 0.0-1.0 used for dynamic brightness control * Value sources produce a float 0.0-1.0 used for dynamic brightness control
* on LED targets. Four subtypes: static (constant), animated (waveform), * on LED targets. Five subtypes: static (constant), animated (waveform),
* audio (audio-reactive), adaptive (time-of-day schedule or scene brightness). * audio (audio-reactive), adaptive_time (time-of-day schedule),
* adaptive_scene (scene brightness analysis).
* *
* Card rendering is handled by streams.js (Value tab). * Card rendering is handled by streams.js (Value tab).
* This module manages the editor modal and API operations. * This module manages the editor modal and API operations.
@@ -49,10 +50,11 @@ export async function showValueSourceModal(editData) {
document.getElementById('value-source-mode').value = editData.mode || 'rms'; document.getElementById('value-source-mode').value = editData.mode || 'rms';
_setSlider('value-source-sensitivity', editData.sensitivity ?? 1.0); _setSlider('value-source-sensitivity', editData.sensitivity ?? 1.0);
_setSlider('value-source-smoothing', editData.smoothing ?? 0.3); _setSlider('value-source-smoothing', editData.smoothing ?? 0.3);
} else if (editData.source_type === 'adaptive') { } else if (editData.source_type === 'adaptive_time') {
document.getElementById('value-source-adaptive-mode').value = editData.adaptive_mode || 'time_of_day';
onAdaptiveModeChange();
_populateScheduleUI(editData.schedule); _populateScheduleUI(editData.schedule);
_setSlider('value-source-adaptive-min-value', editData.min_value ?? 0);
_setSlider('value-source-adaptive-max-value', editData.max_value ?? 1);
} else if (editData.source_type === 'adaptive_scene') {
_populatePictureSourceDropdown(editData.picture_source_id || ''); _populatePictureSourceDropdown(editData.picture_source_id || '');
document.getElementById('value-source-scene-behavior').value = editData.scene_behavior || 'complement'; document.getElementById('value-source-scene-behavior').value = editData.scene_behavior || 'complement';
_setSlider('value-source-scene-sensitivity', editData.sensitivity ?? 1.0); _setSlider('value-source-scene-sensitivity', editData.sensitivity ?? 1.0);
@@ -75,7 +77,6 @@ export async function showValueSourceModal(editData) {
_setSlider('value-source-sensitivity', 1.0); _setSlider('value-source-sensitivity', 1.0);
_setSlider('value-source-smoothing', 0.3); _setSlider('value-source-smoothing', 0.3);
// Adaptive defaults // Adaptive defaults
document.getElementById('value-source-adaptive-mode').value = 'time_of_day';
_populateScheduleUI([]); _populateScheduleUI([]);
_populatePictureSourceDropdown(''); _populatePictureSourceDropdown('');
document.getElementById('value-source-scene-behavior').value = 'complement'; document.getElementById('value-source-scene-behavior').value = 'complement';
@@ -97,7 +98,10 @@ export function onValueSourceTypeChange() {
document.getElementById('value-source-static-section').style.display = type === 'static' ? '' : 'none'; document.getElementById('value-source-static-section').style.display = type === 'static' ? '' : 'none';
document.getElementById('value-source-animated-section').style.display = type === 'animated' ? '' : 'none'; document.getElementById('value-source-animated-section').style.display = type === 'animated' ? '' : 'none';
document.getElementById('value-source-audio-section').style.display = type === 'audio' ? '' : 'none'; document.getElementById('value-source-audio-section').style.display = type === 'audio' ? '' : 'none';
document.getElementById('value-source-adaptive-section').style.display = type === 'adaptive' ? '' : 'none'; document.getElementById('value-source-adaptive-time-section').style.display = type === 'adaptive_time' ? '' : 'none';
document.getElementById('value-source-adaptive-scene-section').style.display = type === 'adaptive_scene' ? '' : 'none';
document.getElementById('value-source-adaptive-range-section').style.display =
(type === 'adaptive_time' || type === 'adaptive_scene') ? '' : 'none';
// Populate audio dropdown when switching to audio type // Populate audio dropdown when switching to audio type
if (type === 'audio') { if (type === 'audio') {
@@ -107,19 +111,12 @@ export function onValueSourceTypeChange() {
} }
} }
// Initialize adaptive sub-sections // Populate picture source dropdown when switching to scene type
if (type === 'adaptive') { if (type === 'adaptive_scene') {
onAdaptiveModeChange();
_populatePictureSourceDropdown(''); _populatePictureSourceDropdown('');
} }
} }
export function onAdaptiveModeChange() {
const mode = document.getElementById('value-source-adaptive-mode').value;
document.getElementById('value-source-tod-section').style.display = mode === 'time_of_day' ? '' : 'none';
document.getElementById('value-source-scene-section').style.display = mode === 'scene' ? '' : 'none';
}
// ── Save ────────────────────────────────────────────────────── // ── Save ──────────────────────────────────────────────────────
export async function saveValueSource() { export async function saveValueSource() {
@@ -149,23 +146,22 @@ export async function saveValueSource() {
payload.mode = document.getElementById('value-source-mode').value; payload.mode = document.getElementById('value-source-mode').value;
payload.sensitivity = parseFloat(document.getElementById('value-source-sensitivity').value); payload.sensitivity = parseFloat(document.getElementById('value-source-sensitivity').value);
payload.smoothing = parseFloat(document.getElementById('value-source-smoothing').value); payload.smoothing = parseFloat(document.getElementById('value-source-smoothing').value);
} else if (sourceType === 'adaptive') { } else if (sourceType === 'adaptive_time') {
payload.adaptive_mode = document.getElementById('value-source-adaptive-mode').value;
payload.min_value = parseFloat(document.getElementById('value-source-adaptive-min-value').value);
payload.max_value = parseFloat(document.getElementById('value-source-adaptive-max-value').value);
if (payload.adaptive_mode === 'time_of_day') {
payload.schedule = _getScheduleFromUI(); payload.schedule = _getScheduleFromUI();
if (payload.schedule.length < 2) { if (payload.schedule.length < 2) {
errorEl.textContent = t('value_source.error.schedule_min'); errorEl.textContent = t('value_source.error.schedule_min');
errorEl.style.display = ''; errorEl.style.display = '';
return; return;
} }
} else if (payload.adaptive_mode === 'scene') { payload.min_value = parseFloat(document.getElementById('value-source-adaptive-min-value').value);
payload.max_value = parseFloat(document.getElementById('value-source-adaptive-max-value').value);
} else if (sourceType === 'adaptive_scene') {
payload.picture_source_id = document.getElementById('value-source-picture-source').value; payload.picture_source_id = document.getElementById('value-source-picture-source').value;
payload.scene_behavior = document.getElementById('value-source-scene-behavior').value; payload.scene_behavior = document.getElementById('value-source-scene-behavior').value;
payload.sensitivity = parseFloat(document.getElementById('value-source-scene-sensitivity').value); payload.sensitivity = parseFloat(document.getElementById('value-source-scene-sensitivity').value);
payload.smoothing = parseFloat(document.getElementById('value-source-scene-smoothing').value); payload.smoothing = parseFloat(document.getElementById('value-source-scene-smoothing').value);
} payload.min_value = parseFloat(document.getElementById('value-source-adaptive-min-value').value);
payload.max_value = parseFloat(document.getElementById('value-source-adaptive-max-value').value);
} }
try { try {
@@ -224,7 +220,7 @@ export async function deleteValueSource(sourceId) {
// ── Card rendering (used by streams.js) ─────────────────────── // ── Card rendering (used by streams.js) ───────────────────────
export function createValueSourceCard(src) { export function createValueSourceCard(src) {
const typeIcons = { static: '📊', animated: '🔄', audio: '🎵', adaptive: '🌤️' }; const typeIcons = { static: '📊', animated: '🔄', audio: '🎵', adaptive_time: '🕐', adaptive_scene: '🌤️' };
const icon = typeIcons[src.source_type] || '🎚️'; const icon = typeIcons[src.source_type] || '🎚️';
let propsHtml = ''; let propsHtml = '';
@@ -245,23 +241,19 @@ export function createValueSourceCard(src) {
<span class="stream-card-prop" title="${escapeHtml(t('value_source.audio_source'))}">${escapeHtml(audioName)}</span> <span class="stream-card-prop" title="${escapeHtml(t('value_source.audio_source'))}">${escapeHtml(audioName)}</span>
<span class="stream-card-prop">${modeLabel.toUpperCase()}</span> <span class="stream-card-prop">${modeLabel.toUpperCase()}</span>
`; `;
} else if (src.source_type === 'adaptive') { } else if (src.source_type === 'adaptive_time') {
if (src.adaptive_mode === 'scene') {
const ps = _cachedStreams.find(s => s.id === src.picture_source_id);
const psName = ps ? ps.name : (src.picture_source_id || '-');
propsHtml = `
<span class="stream-card-prop">${t('value_source.adaptive_mode.scene')}</span>
<span class="stream-card-prop">${escapeHtml(psName)}</span>
<span class="stream-card-prop">${src.scene_behavior || 'complement'}</span>
`;
} else {
const pts = (src.schedule || []).length; const pts = (src.schedule || []).length;
propsHtml = ` propsHtml = `
<span class="stream-card-prop">${t('value_source.adaptive_mode.time_of_day')}</span>
<span class="stream-card-prop">${pts} ${t('value_source.schedule.points')}</span> <span class="stream-card-prop">${pts} ${t('value_source.schedule.points')}</span>
<span class="stream-card-prop">${src.min_value ?? 0}${src.max_value ?? 1}</span> <span class="stream-card-prop">${src.min_value ?? 0}${src.max_value ?? 1}</span>
`; `;
} } else if (src.source_type === 'adaptive_scene') {
const ps = _cachedStreams.find(s => s.id === src.picture_source_id);
const psName = ps ? ps.name : (src.picture_source_id || '-');
propsHtml = `
<span class="stream-card-prop">${escapeHtml(psName)}</span>
<span class="stream-card-prop">${src.scene_behavior || 'complement'}</span>
`;
} }
return ` return `

View File

@@ -774,11 +774,12 @@
"value_source.name.placeholder": "Brightness Pulse", "value_source.name.placeholder": "Brightness Pulse",
"value_source.name.hint": "A descriptive name for this value source", "value_source.name.hint": "A descriptive name for this value source",
"value_source.type": "Type:", "value_source.type": "Type:",
"value_source.type.hint": "Static outputs a constant value. Animated cycles through a waveform. Audio reacts to sound input. Adaptive adjusts based on time of day or scene brightness.", "value_source.type.hint": "Static outputs a constant value. Animated cycles through a waveform. Audio reacts to sound input. Adaptive types adjust brightness automatically based on time of day or scene content.",
"value_source.type.static": "Static", "value_source.type.static": "Static",
"value_source.type.animated": "Animated", "value_source.type.animated": "Animated",
"value_source.type.audio": "Audio", "value_source.type.audio": "Audio",
"value_source.type.adaptive": "Adaptive", "value_source.type.adaptive_time": "Adaptive (Time of Day)",
"value_source.type.adaptive_scene": "Adaptive (Scene)",
"value_source.value": "Value:", "value_source.value": "Value:",
"value_source.value.hint": "Constant output value (0.0 = off, 1.0 = full brightness)", "value_source.value.hint": "Constant output value (0.0 = off, 1.0 = full brightness)",
"value_source.waveform": "Waveform:", "value_source.waveform": "Waveform:",
@@ -804,10 +805,6 @@
"value_source.sensitivity.hint": "Gain multiplier for the audio signal (higher = more reactive)", "value_source.sensitivity.hint": "Gain multiplier for the audio signal (higher = more reactive)",
"value_source.smoothing": "Smoothing:", "value_source.smoothing": "Smoothing:",
"value_source.smoothing.hint": "Temporal smoothing (0 = instant response, 1 = very smooth/slow)", "value_source.smoothing.hint": "Temporal smoothing (0 = instant response, 1 = very smooth/slow)",
"value_source.adaptive_mode": "Adaptive Mode:",
"value_source.adaptive_mode.hint": "Time of Day adjusts brightness on a daily schedule. Scene analyzes picture brightness in real time.",
"value_source.adaptive_mode.time_of_day": "Time of Day",
"value_source.adaptive_mode.scene": "Scene Brightness",
"value_source.schedule": "Schedule:", "value_source.schedule": "Schedule:",
"value_source.schedule.hint": "Define at least 2 time points. Brightness interpolates linearly between them, wrapping at midnight.", "value_source.schedule.hint": "Define at least 2 time points. Brightness interpolates linearly between them, wrapping at midnight.",
"value_source.schedule.add": "+ Add Point", "value_source.schedule.add": "+ Add Point",

View File

@@ -774,11 +774,12 @@
"value_source.name.placeholder": "Пульс яркости", "value_source.name.placeholder": "Пульс яркости",
"value_source.name.hint": "Описательное имя для этого источника значений", "value_source.name.hint": "Описательное имя для этого источника значений",
"value_source.type": "Тип:", "value_source.type": "Тип:",
"value_source.type.hint": "Статический выдаёт постоянное значение. Анимированный циклически меняет форму волны. Аудио реагирует на звук. Адаптивный подстраивается под время суток или яркость сцены.", "value_source.type.hint": "Статический выдаёт постоянное значение. Анимированный циклически меняет форму волны. Аудио реагирует на звук. Адаптивные типы автоматически подстраивают яркость по времени суток или содержимому сцены.",
"value_source.type.static": "Статический", "value_source.type.static": "Статический",
"value_source.type.animated": "Анимированный", "value_source.type.animated": "Анимированный",
"value_source.type.audio": "Аудио", "value_source.type.audio": "Аудио",
"value_source.type.adaptive": "Адаптивный", "value_source.type.adaptive_time": "Адаптивный (Время суток)",
"value_source.type.adaptive_scene": "Адаптивный (Сцена)",
"value_source.value": "Значение:", "value_source.value": "Значение:",
"value_source.value.hint": "Постоянное выходное значение (0.0 = выкл, 1.0 = полная яркость)", "value_source.value.hint": "Постоянное выходное значение (0.0 = выкл, 1.0 = полная яркость)",
"value_source.waveform": "Форма волны:", "value_source.waveform": "Форма волны:",
@@ -804,10 +805,6 @@
"value_source.sensitivity.hint": "Множитель усиления аудиосигнала (выше = более реактивный)", "value_source.sensitivity.hint": "Множитель усиления аудиосигнала (выше = более реактивный)",
"value_source.smoothing": "Сглаживание:", "value_source.smoothing": "Сглаживание:",
"value_source.smoothing.hint": "Временное сглаживание (0 = мгновенный отклик, 1 = очень плавный/медленный)", "value_source.smoothing.hint": "Временное сглаживание (0 = мгновенный отклик, 1 = очень плавный/медленный)",
"value_source.adaptive_mode": "Адаптивный режим:",
"value_source.adaptive_mode.hint": "Время суток регулирует яркость по дневному расписанию. Сцена анализирует яркость изображения в реальном времени.",
"value_source.adaptive_mode.time_of_day": "Время суток",
"value_source.adaptive_mode.scene": "Яркость сцены",
"value_source.schedule": "Расписание:", "value_source.schedule": "Расписание:",
"value_source.schedule.hint": "Определите минимум 2 временные точки. Яркость линейно интерполируется между ними, с переходом через полночь.", "value_source.schedule.hint": "Определите минимум 2 временные точки. Яркость линейно интерполируется между ними, с переходом через полночь.",
"value_source.schedule.add": "+ Добавить точку", "value_source.schedule.add": "+ Добавить точку",

View File

@@ -1,11 +1,13 @@
"""Value source data model with inheritance-based source types. """Value source data model with inheritance-based source types.
A ValueSource produces a scalar float (0.01.0) that can drive target A ValueSource produces a scalar float (0.01.0) that can drive target
parameters like brightness. Four types: parameters like brightness. Five types:
StaticValueSource — constant float value StaticValueSource — constant float value
AnimatedValueSource — periodic waveform (sine, triangle, square, sawtooth) AnimatedValueSource — periodic waveform (sine, triangle, square, sawtooth)
AudioValueSource — audio-reactive scalar (RMS, peak, beat detection) AudioValueSource — audio-reactive scalar (RMS, peak, beat detection)
AdaptiveValueSource — adapts to external conditions (time of day, scene brightness) AdaptiveValueSource — adapts to external conditions:
adaptive_time — interpolates brightness along a 24-hour schedule
adaptive_scene — derives brightness from a picture source's frame luminance
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -19,7 +21,7 @@ class ValueSource:
id: str id: str
name: str name: str
source_type: str # "static" | "animated" | "audio" source_type: str # "static" | "animated" | "audio" | "adaptive_time" | "adaptive_scene"
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
description: Optional[str] = None description: Optional[str] = None
@@ -43,7 +45,6 @@ class ValueSource:
"mode": None, "mode": None,
"sensitivity": None, "sensitivity": None,
"smoothing": None, "smoothing": None,
"adaptive_mode": None,
"schedule": None, "schedule": None,
"picture_source_id": None, "picture_source_id": None,
"scene_behavior": None, "scene_behavior": None,
@@ -92,12 +93,19 @@ class ValueSource:
smoothing=float(data.get("smoothing") or 0.3), smoothing=float(data.get("smoothing") or 0.3),
) )
if source_type == "adaptive": if source_type == "adaptive_time":
return AdaptiveValueSource( return AdaptiveValueSource(
id=sid, name=name, source_type="adaptive", id=sid, name=name, source_type="adaptive_time",
created_at=created_at, updated_at=updated_at, description=description, created_at=created_at, updated_at=updated_at, description=description,
adaptive_mode=data.get("adaptive_mode") or "time_of_day",
schedule=data.get("schedule") or [], schedule=data.get("schedule") or [],
min_value=float(data.get("min_value") or 0.0),
max_value=float(data["max_value"]) if data.get("max_value") is not None else 1.0,
)
if source_type == "adaptive_scene":
return AdaptiveValueSource(
id=sid, name=name, source_type="adaptive_scene",
created_at=created_at, updated_at=updated_at, description=description,
picture_source_id=data.get("picture_source_id") or "", picture_source_id=data.get("picture_source_id") or "",
scene_behavior=data.get("scene_behavior") or "complement", scene_behavior=data.get("scene_behavior") or "complement",
sensitivity=float(data.get("sensitivity") or 1.0), sensitivity=float(data.get("sensitivity") or 1.0),
@@ -177,12 +185,11 @@ class AudioValueSource(ValueSource):
class AdaptiveValueSource(ValueSource): class AdaptiveValueSource(ValueSource):
"""Value source that adapts to external conditions. """Value source that adapts to external conditions.
Two sub-modes: source_type determines the sub-mode:
time_of_day — interpolates brightness along a 24-hour schedule adaptive_time — interpolates brightness along a 24-hour schedule
scene — derives brightness from a picture source's frame luminance adaptive_scene — derives brightness from a picture source's frame luminance
""" """
adaptive_mode: str = "time_of_day" # "time_of_day" | "scene"
schedule: List[dict] = field(default_factory=list) # [{time: "HH:MM", value: 0.0-1.0}] schedule: List[dict] = field(default_factory=list) # [{time: "HH:MM", value: 0.0-1.0}]
picture_source_id: str = "" # for scene mode picture_source_id: str = "" # for scene mode
scene_behavior: str = "complement" # "complement" | "match" scene_behavior: str = "complement" # "complement" | "match"
@@ -193,7 +200,6 @@ class AdaptiveValueSource(ValueSource):
def to_dict(self) -> dict: def to_dict(self) -> dict:
d = super().to_dict() d = super().to_dict()
d["adaptive_mode"] = self.adaptive_mode
d["schedule"] = self.schedule d["schedule"] = self.schedule
d["picture_source_id"] = self.picture_source_id d["picture_source_id"] = self.picture_source_id
d["scene_behavior"] = self.scene_behavior d["scene_behavior"] = self.scene_behavior

View File

@@ -102,7 +102,6 @@ class ValueSourceStore:
sensitivity: Optional[float] = None, sensitivity: Optional[float] = None,
smoothing: Optional[float] = None, smoothing: Optional[float] = None,
description: Optional[str] = None, description: Optional[str] = None,
adaptive_mode: Optional[str] = None,
schedule: Optional[list] = None, schedule: Optional[list] = None,
picture_source_id: Optional[str] = None, picture_source_id: Optional[str] = None,
scene_behavior: Optional[str] = None, scene_behavior: Optional[str] = None,
@@ -110,7 +109,7 @@ class ValueSourceStore:
if not name or not name.strip(): if not name or not name.strip():
raise ValueError("Name is required") raise ValueError("Name is required")
if source_type not in ("static", "animated", "audio", "adaptive"): if source_type not in ("static", "animated", "audio", "adaptive_time", "adaptive_scene"):
raise ValueError(f"Invalid source type: {source_type}") raise ValueError(f"Invalid source type: {source_type}")
for source in self._sources.values(): for source in self._sources.values():
@@ -144,16 +143,21 @@ class ValueSourceStore:
sensitivity=sensitivity if sensitivity is not None else 1.0, sensitivity=sensitivity if sensitivity is not None else 1.0,
smoothing=smoothing if smoothing is not None else 0.3, smoothing=smoothing if smoothing is not None else 0.3,
) )
elif source_type == "adaptive": elif source_type == "adaptive_time":
am = adaptive_mode or "time_of_day"
schedule_data = schedule or [] schedule_data = schedule or []
if am == "time_of_day" and len(schedule_data) < 2: if len(schedule_data) < 2:
raise ValueError("Time of day schedule requires at least 2 points") raise ValueError("Time of day schedule requires at least 2 points")
source = AdaptiveValueSource( source = AdaptiveValueSource(
id=sid, name=name, source_type="adaptive", id=sid, name=name, source_type="adaptive_time",
created_at=now, updated_at=now, description=description, created_at=now, updated_at=now, description=description,
adaptive_mode=am,
schedule=schedule_data, schedule=schedule_data,
min_value=min_value if min_value is not None else 0.0,
max_value=max_value if max_value is not None else 1.0,
)
elif source_type == "adaptive_scene":
source = AdaptiveValueSource(
id=sid, name=name, source_type="adaptive_scene",
created_at=now, updated_at=now, description=description,
picture_source_id=picture_source_id or "", picture_source_id=picture_source_id or "",
scene_behavior=scene_behavior or "complement", scene_behavior=scene_behavior or "complement",
sensitivity=sensitivity if sensitivity is not None else 1.0, sensitivity=sensitivity if sensitivity is not None else 1.0,
@@ -182,7 +186,6 @@ class ValueSourceStore:
sensitivity: Optional[float] = None, sensitivity: Optional[float] = None,
smoothing: Optional[float] = None, smoothing: Optional[float] = None,
description: Optional[str] = None, description: Optional[str] = None,
adaptive_mode: Optional[str] = None,
schedule: Optional[list] = None, schedule: Optional[list] = None,
picture_source_id: Optional[str] = None, picture_source_id: Optional[str] = None,
scene_behavior: Optional[str] = None, scene_behavior: Optional[str] = None,
@@ -223,10 +226,8 @@ class ValueSourceStore:
if smoothing is not None: if smoothing is not None:
source.smoothing = smoothing source.smoothing = smoothing
elif isinstance(source, AdaptiveValueSource): elif isinstance(source, AdaptiveValueSource):
if adaptive_mode is not None:
source.adaptive_mode = adaptive_mode
if schedule is not None: if schedule is not None:
if source.adaptive_mode == "time_of_day" and len(schedule) < 2: if source.source_type == "adaptive_time" and len(schedule) < 2:
raise ValueError("Time of day schedule requires at least 2 points") raise ValueError("Time of day schedule requires at least 2 points")
source.schedule = schedule source.schedule = schedule
if picture_source_id is not None: if picture_source_id is not None:

View File

@@ -32,7 +32,8 @@
<option value="static" data-i18n="value_source.type.static">Static</option> <option value="static" data-i18n="value_source.type.static">Static</option>
<option value="animated" data-i18n="value_source.type.animated">Animated</option> <option value="animated" data-i18n="value_source.type.animated">Animated</option>
<option value="audio" data-i18n="value_source.type.audio">Audio</option> <option value="audio" data-i18n="value_source.type.audio">Audio</option>
<option value="adaptive" data-i18n="value_source.type.adaptive">Adaptive</option> <option value="adaptive_time" data-i18n="value_source.type.adaptive_time">Adaptive (Time of Day)</option>
<option value="adaptive_scene" data-i18n="value_source.type.adaptive_scene">Adaptive (Scene)</option>
</select> </select>
</div> </div>
@@ -161,23 +162,8 @@
</div> </div>
</div> </div>
<!-- Adaptive fields --> <!-- Adaptive Time of Day fields -->
<div id="value-source-adaptive-section" style="display:none"> <div id="value-source-adaptive-time-section" style="display:none">
<!-- Sub-mode selector -->
<div class="form-group">
<div class="label-row">
<label for="value-source-adaptive-mode" data-i18n="value_source.adaptive_mode">Adaptive Mode:</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.adaptive_mode.hint">Time of Day adjusts brightness on a daily schedule. Scene analyzes picture brightness in real time.</small>
<select id="value-source-adaptive-mode" onchange="onAdaptiveModeChange()">
<option value="time_of_day" data-i18n="value_source.adaptive_mode.time_of_day">Time of Day</option>
<option value="scene" data-i18n="value_source.adaptive_mode.scene">Scene Brightness</option>
</select>
</div>
<!-- Time of Day sub-section -->
<div id="value-source-tod-section">
<div class="form-group"> <div class="form-group">
<div class="label-row"> <div class="label-row">
<label data-i18n="value_source.schedule">Schedule:</label> <label data-i18n="value_source.schedule">Schedule:</label>
@@ -189,8 +175,8 @@
</div> </div>
</div> </div>
<!-- Scene sub-section --> <!-- Adaptive Scene fields -->
<div id="value-source-scene-section" style="display:none"> <div id="value-source-adaptive-scene-section" style="display:none">
<div class="form-group"> <div class="form-group">
<div class="label-row"> <div class="label-row">
<label for="value-source-picture-source" data-i18n="value_source.picture_source">Picture Source:</label> <label for="value-source-picture-source" data-i18n="value_source.picture_source">Picture Source:</label>
@@ -219,7 +205,7 @@
<label for="value-source-scene-sensitivity" data-i18n="value_source.sensitivity">Sensitivity:</label> <label for="value-source-scene-sensitivity" data-i18n="value_source.sensitivity">Sensitivity:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button> <button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div> </div>
<small class="input-hint" style="display:none" data-i18n="value_source.scene_behavior.hint">Gain multiplier for the luminance signal (higher = more reactive to brightness changes)</small> <small class="input-hint" style="display:none" data-i18n="value_source.scene_sensitivity.hint">Gain multiplier for the luminance signal (higher = more reactive to brightness changes)</small>
<div class="range-with-value"> <div class="range-with-value">
<input type="range" id="value-source-scene-sensitivity" min="0.1" max="5" step="0.1" value="1.0" <input type="range" id="value-source-scene-sensitivity" min="0.1" max="5" step="0.1" value="1.0"
oninput="document.getElementById('value-source-scene-sensitivity-display').textContent = this.value"> oninput="document.getElementById('value-source-scene-sensitivity-display').textContent = this.value">
@@ -241,7 +227,8 @@
</div> </div>
</div> </div>
<!-- Shared: output range --> <!-- Shared adaptive output range (shown for both adaptive types) -->
<div id="value-source-adaptive-range-section" style="display:none">
<div class="form-group"> <div class="form-group">
<div class="label-row"> <div class="label-row">
<label for="value-source-adaptive-min-value" data-i18n="value_source.adaptive_min_value">Min Value:</label> <label for="value-source-adaptive-min-value" data-i18n="value_source.adaptive_min_value">Min Value:</label>