mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 23:19:14 +00:00
feat: режим «Значение + статичный значок» (#588)
Пятый вариант «Отображение»: маркер показывает выбранное значение и при этом
никогда не меняет цвет — ни по состоянию, ни по тревоге, ни по недоступности,
ни под цвет RGB-света, ни по активности.
Четыре прежних режима задавали одним выбором две независимые вещи: что
нарисовано внутри маркера и красится ли он состоянием. Поэтому вместо
сравнения с одним токеном появились два производных предиката рядом с
`normalizeDeviceDisplay` — `displayWantsValue` и `displayIsNeutral`, — и их
спрашивают политика, слой пульсации, внешний бейдж, редактор и три ветки
живого слоя пылесоса.
Две ловушки, из-за которых режим не сводится к одной строке в словаре:
- быстрый путь «статичному маркеру источники не нужны» (`sourceDetails: false`)
— это основной путь рендера плана, карточки пространства и PDF. Он оставлен
только режиму без значения: иначе число пропало бы именно на плане и
осталось в предпросмотре редактора;
- три ветки живого пылесоса сравнивают режим строкой и не читают политику,
поэтому зелёная политика их не гарантирует. Свидетель держит две
конфигурации карты: сопоставленная доказывает puck и след, несопоставленная
— бейдж маршрута (при совпадающей калибровке маршрут `ready`, и бейджа не
было бы ни в одном режиме). Бейдж проверяется при наполненном буфере
позиций, иначе его отсутствие объяснялось бы первой веткой.
Потолок initial View перецентрирован 291_700 → 292_400 без изменения общего
бюджета: измеренный факт 291 346 Б оставлял под прежним центром 354 Б —
внутри шумовой полосы метрики.
Эталон: в `device-icon-state-table-{light,dark}` включённая RGB-лампа
переведена в новый режим. Кадры обязаны разойтись; приёмка — отдельным
коммитом класса D с полного линуксового артефакта Validate.
Issue: #588
User-Visible: yes
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { DeviceAvailability, DeviceVisualState } from './device-visual';
|
||||
import type { DeviceDisplayMode } from './logic';
|
||||
import { displayIsNeutral, displayWantsValue, type DeviceDisplayMode } from './logic';
|
||||
|
||||
export type PresentationReason =
|
||||
| 'neutral'
|
||||
@@ -23,6 +23,7 @@ export type PresentationReason =
|
||||
| 'composite_power_source'
|
||||
| 'activity_display_disabled'
|
||||
| 'static_icon'
|
||||
| 'value_static_icon'
|
||||
| 'ha_disabled'
|
||||
| 'orphaned';
|
||||
|
||||
@@ -94,7 +95,8 @@ export function resolveDevicePresentationPolicy(
|
||||
decisions.push('lifecycle.active');
|
||||
}
|
||||
|
||||
const staticIcon = input.display === 'static_icon';
|
||||
const neutralFace = displayIsNeutral(input.display);
|
||||
const wantsValue = displayWantsValue(input.display);
|
||||
let visual = input.controllerFace
|
||||
? { ...input.sourceVisual, availability: input.controllerAvailability }
|
||||
: input.sourceVisual;
|
||||
@@ -107,7 +109,7 @@ export function resolveDevicePresentationPolicy(
|
||||
if (effectiveHidden) {
|
||||
visual = NEUTRAL_VISUAL;
|
||||
decisions.push('face.hidden');
|
||||
} else if (staticIcon) {
|
||||
} else if (neutralFace) {
|
||||
visual = NEUTRAL_VISUAL;
|
||||
decisions.push('face.static');
|
||||
} else if (input.sourceVisual.status !== 'alarm' && !input.liveStates) {
|
||||
@@ -117,7 +119,7 @@ export function resolveDevicePresentationPolicy(
|
||||
decisions.push('face.dynamic');
|
||||
}
|
||||
|
||||
if (!staticIcon && !effectiveHidden && input.liveStates
|
||||
if (!neutralFace && !effectiveHidden && input.liveStates
|
||||
&& input.sourceVisual.status !== 'alarm' && input.shortActivity) {
|
||||
visual = { ...visual, activity: input.shortActivity };
|
||||
decisions.push(`activity.short_${input.shortActivity}`);
|
||||
@@ -129,17 +131,17 @@ export function resolveDevicePresentationPolicy(
|
||||
else if (visual.status === 'open') decisions.push('status.open');
|
||||
else decisions.push('status.neutral');
|
||||
|
||||
const face: PresentationFace = input.display === 'value'
|
||||
const face: PresentationFace = wantsValue
|
||||
&& !effectiveHidden && input.valueAvailable ? 'value' : 'icon';
|
||||
decisions.push(face === 'value' ? 'content.value' : input.display === 'value'
|
||||
decisions.push(face === 'value' ? 'content.value' : wantsValue
|
||||
? 'content.value_fallback_icon' : 'content.icon');
|
||||
if (input.display === 'value' && !input.valueAvailable && input.valueFallback) {
|
||||
if (wantsValue && !input.valueAvailable && input.valueFallback) {
|
||||
decisions.push(`content.${input.valueFallback}`);
|
||||
}
|
||||
|
||||
const dynamicIcon = input.liveStates && !staticIcon && !effectiveHidden;
|
||||
const metrics = !staticIcon && !effectiveHidden;
|
||||
const pulseEligible = !effectiveHidden && !bindingUnavailable && !staticIcon
|
||||
const dynamicIcon = input.liveStates && !neutralFace && !effectiveHidden;
|
||||
const metrics = !neutralFace && !effectiveHidden;
|
||||
const pulseEligible = !effectiveHidden && !bindingUnavailable && !neutralFace
|
||||
&& visual.availability === 'available'
|
||||
&& (visual.status === 'alarm' || (input.liveStates && input.display === 'icon_ripple'));
|
||||
const vacuumLive = metrics && input.vacuumLiveRequested;
|
||||
@@ -177,6 +179,7 @@ export function resolvePresentationReason(input: PresentationReasonInput): Prese
|
||||
if (input.lifecycle === 'ha_disabled') return 'ha_disabled';
|
||||
if (input.lifecycle === 'orphaned') return 'orphaned';
|
||||
if (input.display === 'static_icon') return 'static_icon';
|
||||
if (input.display === 'value_static_icon') return 'value_static_icon';
|
||||
if (input.visual.status === 'alarm') return 'alarm';
|
||||
if (!input.liveStates) return 'live_states_disabled';
|
||||
if (input.visual.availability === 'unavailable') return 'unavailable';
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type DeviceActivity, type DeviceAvailability, type DeviceVisualState, type EntityVisualSample,
|
||||
} from './device-visual';
|
||||
import {
|
||||
displayIsNeutral, displayWantsValue,
|
||||
hassValue, lightColorOf, lqiColor, normalizeDeviceDisplay, stateIcon, valueWithUnit,
|
||||
type DeviceDisplayMode,
|
||||
} from './logic';
|
||||
@@ -86,7 +87,7 @@ export function deviceAccessibleLabel(
|
||||
export function deviceA11yState(presentation: Pick<
|
||||
ResolvedDevicePresentation, 'visual' | 'lockState' | 'display'
|
||||
>): DeviceA11yState {
|
||||
if (presentation.display === 'static_icon') return 'neutral';
|
||||
if (displayIsNeutral(presentation.display)) return 'neutral';
|
||||
if (presentation.visual.status === 'alarm') return 'alarm';
|
||||
if (presentation.visual.availability === 'unavailable') return 'unavailable';
|
||||
if (presentation.lockState) return presentation.lockState;
|
||||
@@ -645,7 +646,7 @@ export function presentationClasses(presentation: Pick<
|
||||
'visual' | 'lockState' | 'activity' | 'display' | 'effectiveHidden' | 'activityGeneration' | 'pulse'
|
||||
>): string[] {
|
||||
if (presentation.effectiveHidden) return [];
|
||||
if (presentation.display === 'static_icon') return ['static-icon'];
|
||||
if (displayIsNeutral(presentation.display)) return ['static-icon'];
|
||||
const classes: string[] = [];
|
||||
const { visual } = presentation;
|
||||
if (presentation.pulse.kind === 'alarm') classes.push('alarm');
|
||||
@@ -672,8 +673,16 @@ export function resolveDevicePresentation(
|
||||
options: ResolvePresentationOptions,
|
||||
): ResolvedDevicePresentation {
|
||||
const display = normalizeDeviceDisplay(d.marker?.display);
|
||||
const staticIcon = display === 'static_icon';
|
||||
const sources = staticIcon && options.sourceDetails === false
|
||||
// Two independent questions, and conflating them is the trap this mode was
|
||||
// built to avoid (#588). `neutralFace` answers "does anything colour the
|
||||
// marker"; `sourceless` answers "can we skip resolving sources at all", and
|
||||
// only the value-free static mode can. `sourceDetails: false` is the main
|
||||
// plan render path, so answering the second question with the first would
|
||||
// drop the value exactly where the user looks and keep it in the editor
|
||||
// preview, where sources are always resolved.
|
||||
const neutralFace = displayIsNeutral(display);
|
||||
const sourceless = display === 'static_icon';
|
||||
const sources = sourceless && options.sourceDetails === false
|
||||
? EMPTY_SOURCES : resolvePresentationSources(
|
||||
hass, d, options.lightDevices || [d], options.lightSources,
|
||||
options.registryHass || hass,
|
||||
@@ -712,7 +721,7 @@ export function resolveDevicePresentation(
|
||||
).length > 0;
|
||||
const controllerFace = sources.sourceKind === 'controls'
|
||||
|| (configuredController && sources.sourceKind !== 'light' && sources.sourceKind !== 'cover');
|
||||
const value = staticIcon && options.sourceDetails === false
|
||||
const value = sourceless && options.sourceDetails === false
|
||||
? { source: null, text: null, fullText: null, fallback: null, explicit: false }
|
||||
: resolveValue(hass, d, sources, options.showTemperature, markerStates);
|
||||
const sourceSignature = signatureOf(d, sources, value.source);
|
||||
@@ -780,7 +789,7 @@ export function resolveDevicePresentation(
|
||||
const rippleScale = Number(d.marker?.ripple_size) > 0
|
||||
? Number(d.marker!.ripple_size) : DEVICE_PULSE_DEFAULT_SCALE;
|
||||
const configuredRippleColor = safeStoredColor(d.marker?.ripple_color, null);
|
||||
const rippleColor = staticIcon ? null : configuredRippleColor || lightColor || null;
|
||||
const rippleColor = neutralFace ? null : configuredRippleColor || lightColor || null;
|
||||
const pulse = resolveDevicePulse({
|
||||
display,
|
||||
visual,
|
||||
@@ -809,7 +818,7 @@ export function resolveDevicePresentation(
|
||||
});
|
||||
const notices: PresentationReason[] = [];
|
||||
if (options.designPreview && userHidden) notices.push('hidden_design_preview');
|
||||
if (!staticIcon && d.marker?.vacuum?.live === true) notices.push('vacuum_live_plan_only');
|
||||
if (!neutralFace && d.marker?.vacuum?.live === true) notices.push('vacuum_live_plan_only');
|
||||
const powerSource = sources.visualSources.some((source) =>
|
||||
source.role === 'power_gate' || isDevicePowerSwitch(hass, source.eid),
|
||||
);
|
||||
@@ -817,7 +826,7 @@ export function resolveDevicePresentation(
|
||||
eid.startsWith('switch.') && !hass?.entities?.[eid]?.entity_category,
|
||||
).length;
|
||||
if (powerSource && uncategorisedSwitches > 1) notices.push('composite_power_source');
|
||||
if (display !== 'icon_ripple' && display !== 'static_icon'
|
||||
if (display !== 'icon_ripple' && !neutralFace
|
||||
&& combined.status !== 'alarm' && combined.activity !== 'none') {
|
||||
notices.push('activity_display_disabled');
|
||||
}
|
||||
@@ -843,7 +852,7 @@ export function resolveDevicePresentation(
|
||||
icon,
|
||||
valueText,
|
||||
valueFullText: policy.face === 'value' ? value.fullText : null,
|
||||
fallbackReason: display === 'value' ? value.fallback : null,
|
||||
fallbackReason: displayWantsValue(display) ? value.fallback : null,
|
||||
activity,
|
||||
activityGeneration: rt?.gen || 1,
|
||||
pulse,
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { DeviceActivity, DeviceVisualState } from './device-visual';
|
||||
import type { DeviceDisplayMode } from './logic';
|
||||
import { displayIsNeutral, type DeviceDisplayMode } from './logic';
|
||||
|
||||
export type DevicePulseKind = 'none' | 'alarm' | 'short' | 'continuous';
|
||||
export type DevicePulseReason = 'none' | 'alarm' | 'event' | 'presence' | 'transition' | 'running';
|
||||
@@ -79,7 +79,7 @@ export function resolveDevicePulse(options: ResolveDevicePulseOptions): Resolved
|
||||
? Math.max(1, Number(options.diameterScale)) : DEVICE_PULSE_DEFAULT_SCALE;
|
||||
const color = options.color || semanticPulseColor(visual, semanticActivity);
|
||||
if (effectiveHidden || bindingUnavailable || visual.availability === 'unavailable'
|
||||
|| display === 'static_icon') {
|
||||
|| displayIsNeutral(display)) {
|
||||
return noPulse(generation, color, diameterScale);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
climateTempFor, humFor, isHumEntity, isTempEntity, lqiFor, resolvedLightSources, tempFor,
|
||||
} from './devices';
|
||||
import { isRegistryEntryEnabled } from './ha-binding-status';
|
||||
import { hassValue, valueWithUnit } from './logic';
|
||||
import { displayIsNeutral, displayWantsValue, hassValue, valueWithUnit, type DeviceDisplayMode } from './logic';
|
||||
import type {
|
||||
DevItem, MarkerValueBadge, ValueBadgePosition, ValueBadgeSource,
|
||||
} from './types';
|
||||
@@ -47,7 +47,7 @@ export interface ValueBadgeCandidate {
|
||||
export interface ResolveValueBadgeOptions {
|
||||
showTemperature: boolean;
|
||||
showSignal: boolean;
|
||||
display: string;
|
||||
display: DeviceDisplayMode;
|
||||
effectiveHidden: boolean;
|
||||
/** Canonical marker:* states already projected by presentation/light graph. */
|
||||
markerStates?: readonly { ref: string; on: boolean; name: string }[];
|
||||
@@ -276,7 +276,7 @@ export function resolveDeviceValueBadge(
|
||||
hass: any, d: DevItem, options: ResolveValueBadgeOptions,
|
||||
): ResolvedValueBadge | null {
|
||||
const configured = d.marker?.value_badge != null;
|
||||
if (options.effectiveHidden || options.display === 'static_icon') return null;
|
||||
if (options.effectiveHidden || displayIsNeutral(options.display)) return null;
|
||||
const stored = d.marker?.value_badge;
|
||||
if (configured) {
|
||||
if (!stored?.enabled) return null;
|
||||
@@ -302,7 +302,7 @@ export function resolveDeviceValueBadge(
|
||||
...resolveValueSource(hass, d, stored.source, options.markerStates),
|
||||
};
|
||||
}
|
||||
if (!options.showTemperature || options.display === 'value') return null;
|
||||
if (!options.showTemperature || displayWantsValue(options.display)) return null;
|
||||
const source = legacySource(hass, d);
|
||||
if (!source) return null;
|
||||
let legacyText: string | null = null;
|
||||
|
||||
+9
-12
@@ -45,7 +45,7 @@ import {
|
||||
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
|
||||
referencedContentUrls,
|
||||
DISPLAY_MODES, TAP_ACTIONS, SPACE_FILL_UI_MODES, ROOM_FILL_MODES,
|
||||
normalizeDeviceDisplay, isAlarmCapable, type DeviceDisplayMode,
|
||||
normalizeDeviceDisplay, isAlarmCapable, displayIsNeutral, type DeviceDisplayMode,
|
||||
liveText, liveTextReference, liveTextToken, hassValue, valueWithUnit, decorTextScale, decorTextLines,
|
||||
DECOR_TEXT_BASE,
|
||||
} from './logic';
|
||||
@@ -450,16 +450,13 @@ type ResizePreview = { space: string; sp: any };
|
||||
type ResizeWallUnion = ReturnType<typeof wallBodiesUnionPath>;
|
||||
type ResizeWallArtifact = ReturnType<typeof wallBodiesGeometry>;
|
||||
const DISPLAY_LABEL_KEYS: Record<DeviceDisplayMode, I18nKey> = {
|
||||
badge: 'display.badge',
|
||||
icon_ripple: 'display.icon_ripple',
|
||||
value: 'display.value',
|
||||
static_icon: 'display.static_icon',
|
||||
badge: 'display.badge', icon_ripple: 'display.icon_ripple', value: 'display.value',
|
||||
static_icon: 'display.static_icon', value_static_icon: 'display.value_static_icon',
|
||||
};
|
||||
const DISPLAY_HINT_KEYS: Record<DeviceDisplayMode, I18nKey> = {
|
||||
badge: 'marker.display_hint_badge',
|
||||
icon_ripple: 'marker.display_hint_icon_ripple',
|
||||
value: 'marker.display_hint_value',
|
||||
static_icon: 'marker.display_hint_static_icon',
|
||||
badge: 'marker.display_hint_badge', icon_ripple: 'marker.display_hint_icon_ripple',
|
||||
value: 'marker.display_hint_value', static_icon: 'marker.display_hint_static_icon',
|
||||
value_static_icon: 'marker.display_hint_value_static_icon',
|
||||
};
|
||||
/** Keeps every previously valid scale at the maximum 20 cm grid scale lossless. */
|
||||
const DECOR_TEXT_CM_MAX = 2000;
|
||||
@@ -12195,7 +12192,7 @@ export class HouseplanCard extends LitElement {
|
||||
if (!this.hass) return;
|
||||
for (const d of this._devices) {
|
||||
if (d.hidden || !this._isVacDev(d)) continue;
|
||||
if (normalizeDeviceDisplay(d.marker?.display) === 'static_icon') {
|
||||
if (displayIsNeutral(normalizeDeviceDisplay(d.marker?.display))) {
|
||||
this._vacRt.delete(d.id);
|
||||
continue;
|
||||
}
|
||||
@@ -12411,7 +12408,7 @@ export class HouseplanCard extends LitElement {
|
||||
const trails: TemplateResult[] = [];
|
||||
for (const d of devs) {
|
||||
if (d.hidden || !this._isVacDev(d)) continue;
|
||||
if (normalizeDeviceDisplay(d.marker?.display) === 'static_icon') continue;
|
||||
if (displayIsNeutral(normalizeDeviceDisplay(d.marker?.display))) continue;
|
||||
const fact = this._renderDeviceSnapshot?.facts.get(`vacuum:${d.id}`) as any;
|
||||
const src = fact?.source ?? this._vacSource(d, this._renderPlanHass);
|
||||
if (!src) continue;
|
||||
@@ -12590,7 +12587,7 @@ export class HouseplanCard extends LitElement {
|
||||
*/
|
||||
private _vacRouteBadge(d: DevItem): TemplateResult | typeof nothing {
|
||||
if (!this._isVacDev(d) || d.hidden) return nothing;
|
||||
if (normalizeDeviceDisplay(d.marker?.display) === 'static_icon') return nothing;
|
||||
if (displayIsNeutral(normalizeDeviceDisplay(d.marker?.display))) return nothing;
|
||||
const fact = this._renderDeviceSnapshot?.facts.get(`vacuum:${d.id}`) as
|
||||
{ resolution?: VacuumRouteResolution } | undefined;
|
||||
const reason = routeWarningKey(fact?.resolution, this._vacRt.get(d.id)?.moving ?? false);
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
|
||||
referencedContentUrls,
|
||||
DISPLAY_MODES, TAP_ACTIONS, SPACE_FILL_UI_MODES, ROOM_FILL_MODES,
|
||||
normalizeDeviceDisplay, isAlarmCapable, type DeviceDisplayMode,
|
||||
normalizeDeviceDisplay, isAlarmCapable, displayIsNeutral, displayWantsValue, type DeviceDisplayMode,
|
||||
liveText, liveTextReference, liveTextToken, hassValue, valueWithUnit, decorTextScale, decorTextLines,
|
||||
DECOR_TEXT_BASE,
|
||||
} from './logic';
|
||||
@@ -414,16 +414,13 @@ type ResizePreview = { space: string; sp: any };
|
||||
type ResizeWallUnion = ReturnType<typeof wallBodiesUnionPath>;
|
||||
type ResizeWallArtifact = ReturnType<typeof wallBodiesGeometry>;
|
||||
const DISPLAY_LABEL_KEYS: Record<DeviceDisplayMode, I18nKey> = {
|
||||
badge: 'display.badge',
|
||||
icon_ripple: 'display.icon_ripple',
|
||||
value: 'display.value',
|
||||
static_icon: 'display.static_icon',
|
||||
badge: 'display.badge', icon_ripple: 'display.icon_ripple', value: 'display.value',
|
||||
static_icon: 'display.static_icon', value_static_icon: 'display.value_static_icon',
|
||||
};
|
||||
const DISPLAY_HINT_KEYS: Record<DeviceDisplayMode, I18nKey> = {
|
||||
badge: 'marker.display_hint_badge',
|
||||
icon_ripple: 'marker.display_hint_icon_ripple',
|
||||
value: 'marker.display_hint_value',
|
||||
static_icon: 'marker.display_hint_static_icon',
|
||||
badge: 'marker.display_hint_badge', icon_ripple: 'marker.display_hint_icon_ripple',
|
||||
value: 'marker.display_hint_value', static_icon: 'marker.display_hint_static_icon',
|
||||
value_static_icon: 'marker.display_hint_value_static_icon',
|
||||
};
|
||||
/** Keeps every previously valid scale at the maximum 20 cm grid scale lossless. */
|
||||
const DECOR_TEXT_CM_MAX = 2000;
|
||||
@@ -13394,7 +13391,7 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
</option>`)}
|
||||
</select>
|
||||
<p class="muted">${this.host._t(DISPLAY_HINT_KEYS[d.display])}</p>
|
||||
${d.display === 'value' ? html`<div class="markerhelpfield markervaluesource">
|
||||
${displayWantsValue(d.display) ? html`<div class="markerhelpfield markervaluesource">
|
||||
<div class="markerhelplabel">
|
||||
<label for="marker-value-source">${this.host._t('marker.value_source')}</label>
|
||||
${this._help('marker.value_source.help')}
|
||||
@@ -13427,7 +13424,7 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
<ha-icon icon="mdi:alert-outline"></ha-icon>${this.host._t('marker.value_source_missing_hint')}
|
||||
</p>` : nothing}
|
||||
</div>` : nothing}
|
||||
${d.display === 'static_icon' && this.host._bindingHasAlarm(d.binding)
|
||||
${displayIsNeutral(d.display) && this.host._bindingHasAlarm(d.binding)
|
||||
? html`<div class="habindingbanner" role="note">
|
||||
<ha-icon icon="mdi:alert-outline"></ha-icon>
|
||||
<span>${this.host._t('marker.static_alarm_warning')}</span>
|
||||
@@ -13444,10 +13441,10 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
valueBadgeSource: source,
|
||||
valueBadgeTouched: true,
|
||||
};
|
||||
}, d.display === 'static_icon' || (!badgeCandidates.length && !d.valueBadgeSource))}
|
||||
}, displayIsNeutral(d.display) || (!badgeCandidates.length && !d.valueBadgeSource))}
|
||||
<span>${this.host._t('marker.value_badge_enabled')}</span>
|
||||
</label>
|
||||
${d.display === 'static_icon'
|
||||
${displayIsNeutral(d.display)
|
||||
? html`<p class="muted markerlightdisabled" role="note">
|
||||
<ha-icon icon="mdi:information-outline"></ha-icon>${this.host._t('marker.value_badge_static')}
|
||||
</p>`
|
||||
@@ -13483,7 +13480,7 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
${badgeSourceMissing ? html`<p class="muted markerlightwarning" role="status">
|
||||
<ha-icon icon="mdi:alert-outline"></ha-icon>${this.host._t('marker.value_badge_missing_hint')}
|
||||
</p>` : nothing}
|
||||
${d.display === 'value' && badgeSourceKey === innerValueSourceKey
|
||||
${displayWantsValue(d.display) && badgeSourceKey === innerValueSourceKey
|
||||
? html`<p class="muted markerlightwarning" role="note">
|
||||
<ha-icon icon="mdi:information-outline"></ha-icon>${this.host._t('marker.value_badge_duplicate')}
|
||||
</p>` : nothing}
|
||||
|
||||
@@ -184,10 +184,12 @@
|
||||
"display.badge": "Symbol + Zustand",
|
||||
"display.icon_ripple": "Symbol + Zustand und Aktivität",
|
||||
"display.static_icon": "Immer statisches Symbol",
|
||||
"display.value_static_icon": "Wert + statisches Symbol",
|
||||
"marker.display_hint_badge": "Das Symbol und die dynamische Platte zeigen den Gerätezustand ohne den üblichen Aktivitätspuls. Rote Alarme bleiben sichtbar.",
|
||||
"marker.display_hint_icon_ripple": "Das Symbol, die dynamische Platte und der Puls zeigen einen kurzen Puls für Ereignisse und einen kontinuierlichen Puls für Arbeit, Bewegung oder Anwesenheit. Rote Alarme haben eine separate Priorität.",
|
||||
"marker.display_hint_value": "Ein ausgewählter oder automatisch ermittelter Home Assistant-Wert ersetzt das Symbol, während die Platte weiterhin den Zustand anzeigt. Rote Alarme bleiben sichtbar.",
|
||||
"marker.display_hint_static_icon": "Die themenabhängige Hülle und das Symbol bleiben immer gleich. Zustand, Aktivität, Nichtverfügbarkeit und Alarme verändern nicht das Erscheinungsbild.",
|
||||
"marker.display_hint_value_static_icon": "Der Marker zeigt den gewählten Wert, während die Hülle themenabhängig bleibt. Zustand, Aktivität, Nichtverfügbarkeit und Alarme ändern die Markerfarbe nicht.",
|
||||
"marker.static_alarm_warning": "Dieses Gerät kann Alarme melden. Die statische Anzeige verbirgt die visuelle Alarmanzeige des Markers.",
|
||||
"marker.preview.title": "Anzeigevorschau",
|
||||
"marker.preview.actual": "Jetzt",
|
||||
@@ -236,6 +238,7 @@
|
||||
"marker.preview.reason.ha_disabled": "Die Home Assistant-Verbindung ist deaktiviert und wird im Plan ausgeblendet",
|
||||
"marker.preview.reason.orphaned": "Die gespeicherte Home Assistant-Verbindung konnte nicht mehr gefunden werden",
|
||||
"marker.preview.reason.static_icon": "Statischer Modus: Der Gerätestatus ändert das Symbol nicht",
|
||||
"marker.preview.reason.value_static_icon": "Statischer Modus mit Wert: Der Gerätestatus ändert die Markerfarbe nicht",
|
||||
"marker.activity_color": "Aktivitätspulsfarbe",
|
||||
"marker.ripple_size": "Aktivitätspulsgröße",
|
||||
"marker.activity_alarm_note": "Farbe und Größe betreffen nur gewöhnliche Aktivität und ändern keine roten Alarme.",
|
||||
|
||||
@@ -184,10 +184,12 @@
|
||||
"display.badge": "Icon + state",
|
||||
"display.icon_ripple": "Icon + state and activity",
|
||||
"display.static_icon": "Always static icon",
|
||||
"display.value_static_icon": "Value + static icon",
|
||||
"marker.display_hint_badge": "The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.",
|
||||
"marker.display_hint_icon_ripple": "The icon, dynamic plate and pulse show a short pulse for events and a continuous pulse for work, motion or presence. Red alarms have separate priority.",
|
||||
"marker.display_hint_value": "A selected or automatically resolved Home Assistant value replaces the icon while the plate continues to show state. Red alarms remain visible.",
|
||||
"marker.display_hint_static_icon": "The theme-aware shell and icon always stay the same. State, activity, unavailability and alarms do not change the face.",
|
||||
"marker.display_hint_value_static_icon": "The marker shows the selected value while the shell stays theme-aware. State, activity, unavailability and alarms do not change the marker colour.",
|
||||
"marker.static_alarm_warning": "This device can report alarms. Static display hides the marker's visual alarm indication.",
|
||||
"marker.preview.title": "Display preview",
|
||||
"marker.preview.actual": "Now",
|
||||
@@ -236,6 +238,7 @@
|
||||
"marker.preview.reason.ha_disabled": "The Home Assistant binding is disabled and will be hidden on the plan",
|
||||
"marker.preview.reason.orphaned": "The saved Home Assistant binding can no longer be found",
|
||||
"marker.preview.reason.static_icon": "Static mode: device state does not change the icon",
|
||||
"marker.preview.reason.value_static_icon": "Static mode with a value: device state does not change the marker colour",
|
||||
"marker.activity_color": "Activity pulse color",
|
||||
"marker.ripple_size": "Activity pulse size",
|
||||
"marker.activity_alarm_note": "Color and size affect ordinary activity only and do not change red alarms.",
|
||||
|
||||
@@ -184,10 +184,12 @@
|
||||
"display.badge": "Icône + état",
|
||||
"display.icon_ripple": "Icône + état et activité",
|
||||
"display.static_icon": "Icône toujours statique",
|
||||
"display.value_static_icon": "Valeur + icône statique",
|
||||
"marker.display_hint_badge": "L’icône et le fond dynamique affichent l’état de l’appareil sans la pulsation d’activité habituelle. Les alarmes rouges restent visibles.",
|
||||
"marker.display_hint_icon_ripple": "L’icône, le fond dynamique et la pulsation affichent une brève animation pour les événements et une animation continue pour le fonctionnement, le mouvement ou la présence. Les alarmes rouges restent prioritaires.",
|
||||
"marker.display_hint_value": "Une valeur Home Assistant sélectionnée ou résolue automatiquement remplace l’icône tandis que le fond continue d’indiquer l’état. Les alarmes rouges restent visibles.",
|
||||
"marker.display_hint_static_icon": "Le contour adapté au thème et l’icône restent toujours identiques. L’état, l’activité, l’indisponibilité et les alarmes ne modifient pas l’apparence.",
|
||||
"marker.display_hint_value_static_icon": "Le marqueur affiche la valeur choisie tandis que le contour reste adapté au thème. L’état, l’activité, l’indisponibilité et les alarmes ne modifient pas la couleur du marqueur.",
|
||||
"marker.static_alarm_warning": "Cet appareil peut signaler des alarmes. L’affichage statique masque l’indication visuelle d’alarme du marqueur.",
|
||||
"marker.preview.title": "Aperçu de l’affichage",
|
||||
"marker.preview.actual": "Actuel",
|
||||
@@ -236,6 +238,7 @@
|
||||
"marker.preview.reason.ha_disabled": "L’association Home Assistant est désactivée et sera masquée sur le plan",
|
||||
"marker.preview.reason.orphaned": "L’association Home Assistant enregistrée est introuvable",
|
||||
"marker.preview.reason.static_icon": "Mode statique : l’état de l’appareil ne modifie pas l’icône",
|
||||
"marker.preview.reason.value_static_icon": "Mode statique avec valeur : l’état de l’appareil ne modifie pas la couleur du marqueur",
|
||||
"marker.activity_color": "Couleur de la pulsation d’activité",
|
||||
"marker.ripple_size": "Taille de la pulsation d’activité",
|
||||
"marker.activity_alarm_note": "La couleur et la taille n’affectent que l’activité normale et ne modifient pas les alarmes rouges.",
|
||||
|
||||
@@ -184,10 +184,12 @@
|
||||
"display.badge": "Значок + состояние",
|
||||
"display.icon_ripple": "Значок + состояние и активность",
|
||||
"display.static_icon": "Всегда статичный значок",
|
||||
"display.value_static_icon": "Значение + статичный значок",
|
||||
"marker.display_hint_badge": "Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.",
|
||||
"marker.display_hint_icon_ripple": "Значок, динамическая подложка и пульсация: короткая — для событий, постоянная — для работы, движения или присутствия. Красная тревога имеет отдельный приоритет.",
|
||||
"marker.display_hint_value": "Значок заменяется выбранным или автоматически найденным значением Home Assistant; подложка продолжает показывать состояние. Красная тревога сохраняется.",
|
||||
"marker.display_hint_static_icon": "Подложка в цветах темы и значок всегда остаются одинаковыми. Состояния, активность, недоступность и тревоги не меняют отображение.",
|
||||
"marker.display_hint_value_static_icon": "Маркер показывает выбранное значение, а подложка остаётся в цветах темы. Состояния, активность, недоступность и тревоги не меняют цвет маркера.",
|
||||
"marker.static_alarm_warning": "Это устройство может сообщать о тревогах. Статичный режим скрывает визуальную тревогу маркера.",
|
||||
"marker.preview.title": "Предпросмотр отображения",
|
||||
"marker.preview.actual": "Сейчас",
|
||||
@@ -236,6 +238,7 @@
|
||||
"marker.preview.reason.ha_disabled": "Привязка отключена в Home Assistant, поэтому устройство будет скрыто на плане",
|
||||
"marker.preview.reason.orphaned": "Сохранённая привязка Home Assistant больше не найдена",
|
||||
"marker.preview.reason.static_icon": "Статичный режим: состояние устройства не меняет значок",
|
||||
"marker.preview.reason.value_static_icon": "Статичный режим со значением: состояние устройства не меняет цвет маркера",
|
||||
"marker.activity_color": "Цвет пульсации активности",
|
||||
"marker.ripple_size": "Размер пульсации активности",
|
||||
"marker.activity_alarm_note": "Цвет и размер относятся только к обычной активности и не влияют на красную тревогу.",
|
||||
|
||||
+14
-1
@@ -895,9 +895,22 @@ export function safeUrl(url: string | null | undefined): string | null {
|
||||
*/
|
||||
/** UI presentations. `ripple` remains a backend/read compatibility value only
|
||||
* and is normalised to `icon_ripple` by the shared normalizer. */
|
||||
export const DISPLAY_MODES = ['badge', 'icon_ripple', 'value', 'static_icon'] as const;
|
||||
export const DISPLAY_MODES = ['badge', 'icon_ripple', 'value', 'static_icon', 'value_static_icon'] as const;
|
||||
export type DeviceDisplayMode = typeof DISPLAY_MODES[number];
|
||||
|
||||
/* The four original modes tied two independent things together: what is drawn
|
||||
* inside the marker (icon or value) and whether the marker takes colour from
|
||||
* state. `value_static_icon` (#588) is the missing fourth combination, so the
|
||||
* two questions are asked separately from here on. Every renderer, the pulse
|
||||
* layer, the badge layer and the live vacuum branches ask these two predicates
|
||||
* instead of comparing the token, which is what kept the modes consistent. */
|
||||
/** The marker draws the resolved value instead of its icon when one exists. */
|
||||
export const displayWantsValue = (display: DeviceDisplayMode): boolean =>
|
||||
display === 'value' || display === 'value_static_icon';
|
||||
/** State, alarm, availability, live colour and activity never touch the face. */
|
||||
export const displayIsNeutral = (display: DeviceDisplayMode): boolean =>
|
||||
display === 'static_icon' || display === 'value_static_icon';
|
||||
|
||||
/** One read boundary for persisted/legacy display values. Unknown manual data
|
||||
* safely falls back to the default dynamic badge; the backend still rejects it
|
||||
* on write. Keep every renderer and editor on this normalizer. */
|
||||
|
||||
+1
-1
@@ -196,7 +196,7 @@ export interface Marker {
|
||||
* only use. */
|
||||
room_id?: string | null;
|
||||
/** `ripple` is legacy read compatibility; UI maps it to icon_ripple. */
|
||||
display?: 'badge' | 'ripple' | 'icon_ripple' | 'value' | 'static_icon' | null; // how the device is drawn
|
||||
display?: 'badge' | 'ripple' | 'icon_ripple' | 'value' | 'static_icon' | 'value_static_icon' | null; // how the device is drawn
|
||||
ripple_color?: string | null;
|
||||
ripple_size?: number | null; // max ring diameter, in icon diameters (default 1.5)
|
||||
size?: number | null; // icon size multiplier (default 1)
|
||||
|
||||
Reference in New Issue
Block a user