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