mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 12:18:51 +00:00
+84
-21
@@ -22,7 +22,10 @@ import {
|
||||
import { resolveToggleIntent, toggleCoverEntity } from './device-toggle';
|
||||
import type { DevItem } from './types';
|
||||
import { safeStoredColor } from './color';
|
||||
import { resolveDeviceValueBadge, type ResolvedValueBadge } from './device-value-badge';
|
||||
import {
|
||||
resolveDeviceValueBadge, resolveValueSource, valueBadgeSourceKey,
|
||||
type ResolvedValueBadge,
|
||||
} from './device-value-badge';
|
||||
import {
|
||||
DEVICE_PULSE_DEFAULT_SCALE, resolveDevicePulse, type ResolvedDevicePulse,
|
||||
} from './device-pulse';
|
||||
@@ -80,10 +83,11 @@ export interface ResolvedPresentationSource {
|
||||
}
|
||||
|
||||
export interface ResolvedValueSource {
|
||||
kind: 'temperature' | 'humidity' | 'entity';
|
||||
kind: 'temperature' | 'humidity' | 'entity' | 'derived_lqi' | 'derived_marker_state';
|
||||
eid: string;
|
||||
attribute?: string;
|
||||
text: string;
|
||||
sourceKey: string;
|
||||
}
|
||||
|
||||
export interface PresentationExplanation {
|
||||
@@ -470,24 +474,72 @@ function resolveValue(
|
||||
d: DevItem,
|
||||
sources: ResolvedPresentationSources,
|
||||
showTemperature: boolean,
|
||||
): { source: ResolvedValueSource | null; text: string | null; fallback: ValueFallbackReason | null } {
|
||||
if (d.virtual) return { source: null, text: null, fallback: 'value_virtual' };
|
||||
markerStates: readonly { ref: string; on: boolean; name: string }[] = [],
|
||||
): {
|
||||
source: ResolvedValueSource | null;
|
||||
text: string | null;
|
||||
fullText: string | null;
|
||||
fallback: ValueFallbackReason | null;
|
||||
explicit: boolean;
|
||||
} {
|
||||
if (d.virtual) {
|
||||
return { source: null, text: null, fullText: null, fallback: 'value_virtual', explicit: false };
|
||||
}
|
||||
const explicitSource = d.marker?.value_source;
|
||||
if (explicitSource) {
|
||||
const resolved = resolveValueSource(hass, d, explicitSource, markerStates);
|
||||
const kind: ResolvedValueSource['kind'] = explicitSource.kind === 'derived_lqi'
|
||||
? 'derived_lqi'
|
||||
: explicitSource.kind === 'derived_marker_state'
|
||||
? 'derived_marker_state'
|
||||
: explicitSource.kind === 'entity_attribute' && explicitSource.attribute.includes('temperature')
|
||||
? 'temperature'
|
||||
: explicitSource.kind === 'entity_attribute' && explicitSource.attribute.includes('humidity')
|
||||
? 'humidity' : 'entity';
|
||||
const eid = explicitSource.kind === 'entity_state' || explicitSource.kind === 'entity_attribute'
|
||||
? explicitSource.entity_id
|
||||
: explicitSource.kind === 'derived_marker_state' ? explicitSource.ref : 'derived:lqi';
|
||||
const attribute = explicitSource.kind === 'entity_attribute'
|
||||
? explicitSource.attribute : undefined;
|
||||
return {
|
||||
source: {
|
||||
kind, eid, attribute, text: resolved.text,
|
||||
sourceKey: valueBadgeSourceKey(explicitSource),
|
||||
},
|
||||
text: resolved.text,
|
||||
fullText: resolved.fullText,
|
||||
fallback: resolved.failure === 'non_scalar' ? 'value_non_scalar'
|
||||
: resolved.failure === 'missing' ? 'value_no_state' : null,
|
||||
explicit: true,
|
||||
};
|
||||
}
|
||||
if (showTemperature) {
|
||||
const climate = firstClimateTemperature(hass, d);
|
||||
if (climate) {
|
||||
return {
|
||||
source: { kind: 'temperature', eid: climate.eid, attribute: 'current_temperature', text: climate.text },
|
||||
source: {
|
||||
kind: 'temperature', eid: climate.eid, attribute: 'current_temperature', text: climate.text,
|
||||
sourceKey: `attr:${climate.eid}:current_temperature`,
|
||||
},
|
||||
text: climate.text,
|
||||
fullText: climate.text,
|
||||
fallback: null,
|
||||
explicit: false,
|
||||
};
|
||||
}
|
||||
const temp = firstTemperature(hass, d);
|
||||
if (temp) {
|
||||
return { source: { kind: 'temperature', eid: temp.eid, text: temp.text }, text: temp.text, fallback: null };
|
||||
return {
|
||||
source: { kind: 'temperature', eid: temp.eid, text: temp.text, sourceKey: `state:${temp.eid}` },
|
||||
text: temp.text, fullText: temp.text, fallback: null, explicit: false,
|
||||
};
|
||||
}
|
||||
const hum = firstHumidity(hass, d);
|
||||
if (hum) {
|
||||
return { source: { kind: 'humidity', eid: hum.eid, text: hum.text }, text: hum.text, fallback: null };
|
||||
return {
|
||||
source: { kind: 'humidity', eid: hum.eid, text: hum.text, sourceKey: `state:${hum.eid}` },
|
||||
text: hum.text, fullText: hum.text, fallback: null, explicit: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Passive marker sources deliberately use a `marker:*` identity. They are
|
||||
@@ -510,13 +562,20 @@ function resolveValue(
|
||||
return {
|
||||
source: null,
|
||||
text: null,
|
||||
fullText: null,
|
||||
fallback: valueEids.length ? 'value_ambiguous_sources' : 'value_no_state',
|
||||
explicit: false,
|
||||
};
|
||||
}
|
||||
const eid = valueEids[0];
|
||||
const value = validStateValue(hass, eid);
|
||||
if (value.fallback) return { source: null, text: null, fallback: value.fallback };
|
||||
return { source: { kind: 'entity', eid, text: value.text }, text: value.text, fallback: null };
|
||||
if (value.fallback) {
|
||||
return { source: null, text: null, fullText: null, fallback: value.fallback, explicit: false };
|
||||
}
|
||||
return {
|
||||
source: { kind: 'entity', eid, text: value.text, sourceKey: `state:${eid}` },
|
||||
text: value.text, fullText: value.text, fallback: null, explicit: false,
|
||||
};
|
||||
}
|
||||
|
||||
function signatureOf(
|
||||
@@ -530,7 +589,8 @@ function signatureOf(
|
||||
...sources.visualSources.map((source) => `${source.role}:${source.eid}`),
|
||||
...sources.criticalSources.map((source) => `critical:${source.eid}`),
|
||||
].sort();
|
||||
const value = valueSource ? `${valueSource.kind}:${valueSource.eid}:${valueSource.attribute || ''}` : 'none';
|
||||
const value = valueSource
|
||||
? `${valueSource.kind}:${valueSource.sourceKey}:${valueSource.eid}:${valueSource.attribute || ''}` : 'none';
|
||||
return [binding, sources.sourceKind, ...rows, `value:${value}`].join('\n');
|
||||
}
|
||||
|
||||
@@ -595,6 +655,13 @@ export function resolveDevicePresentation(
|
||||
hass, d, options.lightDevices || [d], options.lightSources,
|
||||
options.registryHass || hass,
|
||||
);
|
||||
const needsMarkerState = d.marker?.value_source?.kind === 'derived_marker_state'
|
||||
|| d.marker?.value_badge?.source?.kind === 'derived_marker_state';
|
||||
const markerStateGraph = options.lightSources
|
||||
|| (needsMarkerState ? resolvedLightSources(hass, options.lightDevices || [d]) : []);
|
||||
const markerStates = markerStateGraph
|
||||
.filter((source) => source.key.startsWith('marker:'))
|
||||
.map((source) => ({ ref: source.key, on: source.on, name: source.device.name }));
|
||||
const status = d.bindingStatus;
|
||||
const haDisabled = status?.kind === 'ha_disabled';
|
||||
const orphaned = status?.kind === 'orphaned';
|
||||
@@ -623,8 +690,8 @@ export function resolveDevicePresentation(
|
||||
const controllerFace = sources.sourceKind === 'controls'
|
||||
|| (configuredController && sources.sourceKind !== 'light' && sources.sourceKind !== 'cover');
|
||||
const value = staticIcon && options.sourceDetails === false
|
||||
? { source: null, text: null, fallback: null }
|
||||
: resolveValue(hass, d, sources, options.showTemperature);
|
||||
? { source: null, text: null, fullText: null, fallback: null, explicit: false }
|
||||
: resolveValue(hass, d, sources, options.showTemperature, markerStates);
|
||||
const sourceSignature = signatureOf(d, sources, value.source);
|
||||
const activitySignature = activitySourceSignature(hass, d, sources);
|
||||
const rt = options.activityRuntime;
|
||||
@@ -664,17 +731,12 @@ export function resolveDevicePresentation(
|
||||
&& !!d.primary && isHumEntity(hass, d.primary)
|
||||
? humFor(hass, d.entities) : null;
|
||||
const lqi = policy.metrics && options.showSignal && !d.virtual ? lqiFor(hass, d.entities) : null;
|
||||
const markerStateGraph = options.lightSources || (d.marker?.value_badge?.source?.kind === 'derived_marker_state'
|
||||
? resolvedLightSources(hass, options.lightDevices || [d])
|
||||
: []);
|
||||
const valueBadge = resolveDeviceValueBadge(hass, d, {
|
||||
showTemperature: options.showTemperature,
|
||||
showSignal: options.showSignal,
|
||||
display,
|
||||
effectiveHidden,
|
||||
markerStates: markerStateGraph
|
||||
.filter((source) => source.key.startsWith('marker:'))
|
||||
.map((source) => ({ ref: source.key, on: source.on, name: source.device.name })),
|
||||
markerStates,
|
||||
});
|
||||
const realVisualSource = sources.visualSources.find((source) => !source.eid.startsWith('marker:'));
|
||||
const actEid = sources.sourceKind === 'cover'
|
||||
@@ -757,8 +819,8 @@ export function resolveDevicePresentation(
|
||||
display,
|
||||
icon,
|
||||
valueText,
|
||||
valueFullText: valueText,
|
||||
fallbackReason: display === 'value' && !valueText ? value.fallback : null,
|
||||
valueFullText: policy.face === 'value' ? value.fullText : null,
|
||||
fallbackReason: display === 'value' ? value.fallback : null,
|
||||
activity,
|
||||
activityGeneration: rt?.gen || 1,
|
||||
pulse,
|
||||
@@ -766,7 +828,8 @@ export function resolveDevicePresentation(
|
||||
tempText: temp == null ? null : String(temp),
|
||||
humText: hum == null ? null : String(hum),
|
||||
valueBadge,
|
||||
lqiText: lqi == null || valueBadge?.isLqi ? null : String(lqi),
|
||||
lqiText: lqi == null || valueBadge?.isLqi || value.source?.kind === 'derived_lqi'
|
||||
? null : String(lqi),
|
||||
lqiColor: lqi == null ? null : markerLqiColor(lqi),
|
||||
lqiBand: lqi == null ? null : markerLqiBand(lqi),
|
||||
lightColor,
|
||||
|
||||
+71
-13
@@ -31,6 +31,8 @@ export interface ResolvedValueBadge {
|
||||
availability: 'available' | 'unavailable' | 'missing';
|
||||
isLqi: boolean;
|
||||
tone: 'temperature' | 'humidity' | 'lqi' | 'default';
|
||||
/** Why the configured source could not produce a scalar value. */
|
||||
failure: 'missing' | 'non_scalar' | null;
|
||||
}
|
||||
|
||||
export interface ValueBadgeCandidate {
|
||||
@@ -90,7 +92,8 @@ export function valueBadgeSourceKey(source: ValueBadgeSource | null | undefined)
|
||||
if (source.kind === 'entity_state') return `state:${source.entity_id}`;
|
||||
if (source.kind === 'entity_attribute') return `attr:${source.entity_id}:${source.attribute}`;
|
||||
if (source.kind === 'derived_marker_state') return `marker:${source.ref}`;
|
||||
return 'derived:lqi';
|
||||
if (source.kind === 'derived_lqi') return 'derived:lqi';
|
||||
return '';
|
||||
}
|
||||
|
||||
export function valueBadgeSourceFromKey(key: string): ValueBadgeSource | null {
|
||||
@@ -153,42 +156,81 @@ function sourceTone(source: ValueBadgeSource): ResolvedValueBadge['tone'] {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
function resolveSource(
|
||||
export function resolveValueSource(
|
||||
hass: any, d: DevItem, source: ValueBadgeSource,
|
||||
markerStates: readonly { ref: string; on: boolean; name: string }[] = [],
|
||||
): Omit<ResolvedValueBadge, 'configured' | 'enabled' | 'position'> {
|
||||
let text: string | null = null;
|
||||
let label = '';
|
||||
let availability: ResolvedValueBadge['availability'] = 'available';
|
||||
let failure: ResolvedValueBadge['failure'] = null;
|
||||
if (source.kind === 'entity_state') {
|
||||
label = entityName(hass, source.entity_id);
|
||||
const st = hass?.states?.[source.entity_id];
|
||||
if (!st) availability = 'missing';
|
||||
else if (!sourceRegistryEnabled(hass, source.entity_id)) availability = 'unavailable';
|
||||
else if (['unknown', 'unavailable'].includes(String(st.state).toLowerCase())) availability = 'unavailable';
|
||||
if (!st) {
|
||||
availability = 'missing';
|
||||
failure = 'missing';
|
||||
} else if (!sourceRegistryEnabled(hass, source.entity_id)) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
} else if (!['string', 'number', 'boolean'].includes(typeof st.state)) {
|
||||
availability = 'unavailable';
|
||||
failure = 'non_scalar';
|
||||
} else if (['unknown', 'unavailable'].includes(String(st.state).toLowerCase())) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
}
|
||||
else {
|
||||
const value = hassValue(hass, source.entity_id);
|
||||
text = value ? valueWithUnit(value, String(st.attributes?.unit_of_measurement || '')) : null;
|
||||
if (!text) availability = 'unavailable';
|
||||
if (!text) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
}
|
||||
}
|
||||
} else if (source.kind === 'entity_attribute') {
|
||||
label = `${attrLabel(source.attribute)} · ${entityName(hass, source.entity_id)}`;
|
||||
const st = hass?.states?.[source.entity_id];
|
||||
if (!st) availability = 'missing';
|
||||
else if (!sourceRegistryEnabled(hass, source.entity_id)) availability = 'unavailable';
|
||||
const hasAttribute = !!st && source.attribute in (st.attributes || {});
|
||||
const raw = hasAttribute ? st.attributes?.[source.attribute] : undefined;
|
||||
if (!st) {
|
||||
availability = 'missing';
|
||||
failure = 'missing';
|
||||
} else if (!sourceRegistryEnabled(hass, source.entity_id)) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
} else if (['unknown', 'unavailable'].includes(String(st.state).toLowerCase())) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
} else if (!hasAttribute) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
} else if (!['string', 'number', 'boolean'].includes(typeof raw)) {
|
||||
availability = 'unavailable';
|
||||
failure = 'non_scalar';
|
||||
}
|
||||
else {
|
||||
text = formatAttribute(hass, source.entity_id, source.attribute);
|
||||
if (text == null) availability = 'unavailable';
|
||||
if (text == null) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
}
|
||||
}
|
||||
} else if (source.kind === 'derived_lqi') {
|
||||
label = 'LQI';
|
||||
const lqi = lqiFor(hass, d.entities);
|
||||
if (lqi == null) availability = 'unavailable';
|
||||
if (lqi == null) {
|
||||
availability = 'unavailable';
|
||||
failure = 'missing';
|
||||
}
|
||||
else text = String(lqi);
|
||||
} else {
|
||||
label = 'Light state';
|
||||
const match = markerStates.find((item) => item.ref === source.ref);
|
||||
if (!match) availability = 'missing';
|
||||
if (!match) {
|
||||
availability = 'missing';
|
||||
failure = 'missing';
|
||||
}
|
||||
else {
|
||||
const state = match.on ? 'on' : 'off';
|
||||
const loc = hass?.localize?.(`state.default.${state}`);
|
||||
@@ -205,6 +247,7 @@ function resolveSource(
|
||||
availability,
|
||||
isLqi: source.kind === 'derived_lqi',
|
||||
tone: sourceTone(source),
|
||||
failure,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -252,10 +295,11 @@ export function resolveDeviceValueBadge(
|
||||
availability: 'missing',
|
||||
isLqi: false,
|
||||
tone: 'default',
|
||||
failure: 'missing',
|
||||
};
|
||||
return {
|
||||
configured: true, enabled: true, position: stored.position || 'right',
|
||||
...resolveSource(hass, d, stored.source, options.markerStates),
|
||||
...resolveValueSource(hass, d, stored.source, options.markerStates),
|
||||
};
|
||||
}
|
||||
if (!options.showTemperature || options.display === 'value') return null;
|
||||
@@ -283,6 +327,7 @@ export function resolveDeviceValueBadge(
|
||||
? `${attrLabel(source.attribute)} · ${entityName(hass, source.entity_id)}`
|
||||
: source.kind === 'entity_state' ? entityName(hass, source.entity_id) : '',
|
||||
text: legacyText, fullText: legacyText, availability: 'available', isLqi: false, tone,
|
||||
failure: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -317,7 +362,7 @@ export function valueBadgeCandidates(
|
||||
const add = (source: ValueBadgeSource): void => {
|
||||
const key = valueBadgeSourceKey(source);
|
||||
if (candidates.some((item) => item.key === key)) return;
|
||||
const resolved = resolveSource(hass, d, source, markerStates);
|
||||
const resolved = resolveValueSource(hass, d, source, markerStates);
|
||||
const technical = source.kind === 'entity_attribute'
|
||||
? `${source.entity_id} · ${source.attribute}`
|
||||
: source.kind === 'entity_state' ? source.entity_id
|
||||
@@ -395,3 +440,16 @@ export function valueBadgeWriteFields(options: {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Pure persistence gate for the value face; auto is represented by absence. */
|
||||
export function valueSourceWriteFields(options: {
|
||||
touched: boolean;
|
||||
originalHas: boolean;
|
||||
original: ValueBadgeSource | null | undefined;
|
||||
source: ValueBadgeSource | null;
|
||||
}): { value_source?: ValueBadgeSource | null } {
|
||||
if (!options.touched) {
|
||||
return options.originalHas ? { value_source: options.original } : {};
|
||||
}
|
||||
return options.source ? { value_source: options.source } : {};
|
||||
}
|
||||
|
||||
+6
-1
@@ -883,8 +883,13 @@ export function rewriteMarkerControlReferences(
|
||||
source: { ...marker.value_badge.source, ref: to as `marker:${string}` },
|
||||
}
|
||||
: marker.value_badge;
|
||||
const valueSource = marker.value_source?.kind === 'derived_marker_state'
|
||||
&& marker.value_source.ref === from
|
||||
? { ...marker.value_source, ref: to as `marker:${string}` }
|
||||
: marker.value_source;
|
||||
return controls === marker.controls && valueBadge === marker.value_badge
|
||||
? marker : { ...marker, controls, value_badge: valueBadge };
|
||||
&& valueSource === marker.value_source
|
||||
? marker : { ...marker, controls, value_badge: valueBadge, value_source: valueSource };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2107,6 +2107,10 @@ export class HouseplanCard extends LitElement {
|
||||
valueBadgeTouched: boolean;
|
||||
originalHasValueBadge: boolean;
|
||||
originalValueBadge: MarkerValueBadge | null | undefined;
|
||||
valueSource: ValueBadgeSource | null;
|
||||
valueSourceTouched: boolean;
|
||||
originalHasValueSource: boolean;
|
||||
originalValueSource: ValueBadgeSource | null | undefined;
|
||||
useClimateTemp: boolean; // badge + room-average vote from climate current_temperature
|
||||
model: string;
|
||||
link: string;
|
||||
|
||||
@@ -267,7 +267,8 @@ import {
|
||||
} from './activity-runtime';
|
||||
import {
|
||||
recommendedValueBadgeSource, valueBadgeCandidates, valueBadgeSourceFromKey,
|
||||
valueBadgeSourceKey, valueBadgeTitle, valueBadgeWriteFields, type ValueBadgeCandidate,
|
||||
valueBadgeSourceKey, valueBadgeTitle, valueBadgeWriteFields, valueSourceWriteFields,
|
||||
type ValueBadgeCandidate,
|
||||
} from './device-value-badge';
|
||||
import {
|
||||
createRenderDeviceSnapshot, presentationSnapshotKey, renderDeviceSnapshotPositions,
|
||||
@@ -958,7 +959,7 @@ export interface HouseplanEditorHostPort {
|
||||
_layout: Record<string, { x: number; y: number; s?: string; k?: number; }>;
|
||||
_layoutRev: number;
|
||||
_logicalViewCenter: (projection: "flat" | "iso") => { x: number; y: number; } | null;
|
||||
_markerDialog: { devId?: string; uploadId?: string; name: string; binding: string; bindingMode: "virtual" | "ha"; bindingOpen: boolean; showEntities: boolean; bindingFilter: string; icon: string; autoIcon: string; display: DeviceDisplayMode; rippleColor: string; rippleSize: number; size: number; angle: number; tapAction: string; tapActionTouched: boolean; originalHasTapAction: boolean; originalTapAction: string | null | undefined; tapHintAnnouncement: string; toggleEntity: string; toggleEntityTouched: boolean; originalHasToggleEntity: boolean; originalToggleEntity: string | null | undefined; tapTarget: string; tapConfirm: boolean; runFilter: string; controls: string[]; controlsFilter: string; glowRadius: string; lightRole: "auto" | "always" | "never"; lightRoleTouched: boolean; originalHasIsLight: boolean; originalIsLight: boolean | null | undefined; lightEntity: string; lightEntityTouched: boolean; originalHasLightEntity: boolean; originalLightEntity: string | null | undefined; glowMode: "auto" | "color" | "fixed"; glowColor: string; glowBrightness: number; glowColorDrafted: boolean; glowBrightnessDrafted: boolean; glowTouched: boolean; originalHasGlowColor: boolean; originalGlowColor: { c: string; bri?: number | null; } | null | undefined; valueBadgeEnabled: boolean; valueBadgeSource: ValueBadgeSource | null; valueBadgePosition: ValueBadgePosition; valueBadgeTouched: boolean; originalHasValueBadge: boolean; originalValueBadge: MarkerValueBadge | null | undefined; useClimateTemp: boolean; model: string; link: string; description: string; pdfs: PdfRef[]; room: string; hideFromPlan: boolean; busy: boolean; } | null;
|
||||
_markerDialog: { devId?: string; uploadId?: string; name: string; binding: string; bindingMode: "virtual" | "ha"; bindingOpen: boolean; showEntities: boolean; bindingFilter: string; icon: string; autoIcon: string; display: DeviceDisplayMode; rippleColor: string; rippleSize: number; size: number; angle: number; tapAction: string; tapActionTouched: boolean; originalHasTapAction: boolean; originalTapAction: string | null | undefined; tapHintAnnouncement: string; toggleEntity: string; toggleEntityTouched: boolean; originalHasToggleEntity: boolean; originalToggleEntity: string | null | undefined; tapTarget: string; tapConfirm: boolean; runFilter: string; controls: string[]; controlsFilter: string; glowRadius: string; lightRole: "auto" | "always" | "never"; lightRoleTouched: boolean; originalHasIsLight: boolean; originalIsLight: boolean | null | undefined; lightEntity: string; lightEntityTouched: boolean; originalHasLightEntity: boolean; originalLightEntity: string | null | undefined; glowMode: "auto" | "color" | "fixed"; glowColor: string; glowBrightness: number; glowColorDrafted: boolean; glowBrightnessDrafted: boolean; glowTouched: boolean; originalHasGlowColor: boolean; originalGlowColor: { c: string; bri?: number | null; } | null | undefined; valueBadgeEnabled: boolean; valueBadgeSource: ValueBadgeSource | null; valueBadgePosition: ValueBadgePosition; valueBadgeTouched: boolean; originalHasValueBadge: boolean; originalValueBadge: MarkerValueBadge | null | undefined; valueSource: ValueBadgeSource | null; valueSourceTouched: boolean; originalHasValueSource: boolean; originalValueSource: ValueBadgeSource | null | undefined; useClimateTemp: boolean; model: string; link: string; description: string; pdfs: PdfRef[]; room: string; hideFromPlan: boolean; busy: boolean; } | null;
|
||||
_markerPreviewDevicesMemo: { base: readonly DevItem[]; preview: DevItem; devices: readonly DevItem[]; } | null;
|
||||
_markerPreviewMemo: { key: string; device: DevItem | null; } | null;
|
||||
_markers: Marker[];
|
||||
@@ -7642,6 +7643,7 @@ public _openMarkerDialog(d?: DevItem): void {
|
||||
const hasLightEntity = Object.prototype.hasOwnProperty.call(marker || {}, 'light_entity');
|
||||
const hasGlowColor = Object.prototype.hasOwnProperty.call(marker || {}, 'glow_color');
|
||||
const hasValueBadge = Object.prototype.hasOwnProperty.call(marker || {}, 'value_badge');
|
||||
const hasValueSource = Object.prototype.hasOwnProperty.call(marker || {}, 'value_source');
|
||||
const hasTapAction = Object.prototype.hasOwnProperty.call(marker || {}, 'tap_action');
|
||||
const hasToggleEntity = Object.prototype.hasOwnProperty.call(marker || {}, 'toggle_entity');
|
||||
const glowOverride = normalizeGlowColorOverride(marker?.glow_color);
|
||||
@@ -7703,6 +7705,10 @@ public _openMarkerDialog(d?: DevItem): void {
|
||||
valueBadgeTouched: false,
|
||||
originalHasValueBadge: hasValueBadge,
|
||||
originalValueBadge: marker?.value_badge,
|
||||
valueSource: marker?.value_source || null,
|
||||
valueSourceTouched: false,
|
||||
originalHasValueSource: hasValueSource,
|
||||
originalValueSource: marker?.value_source,
|
||||
useClimateTemp: d.marker?.use_climate_temp === true,
|
||||
glowRadius: Number(d.marker?.glow_radius_cm) > 0
|
||||
? String(this.host._imperial
|
||||
@@ -7739,6 +7745,8 @@ public _openMarkerDialog(d?: DevItem): void {
|
||||
originalHasGlowColor: false, originalGlowColor: undefined,
|
||||
valueBadgeEnabled: false, valueBadgeSource: null, valueBadgePosition: 'right',
|
||||
valueBadgeTouched: false, originalHasValueBadge: false, originalValueBadge: undefined,
|
||||
valueSource: null, valueSourceTouched: false,
|
||||
originalHasValueSource: false, originalValueSource: undefined,
|
||||
useClimateTemp: false, glowRadius: '', model: '',
|
||||
link: '', description: '', pdfs: [], room: '', hideFromPlan: false, busy: false,
|
||||
uploadId: 'up_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
||||
@@ -8029,6 +8037,7 @@ public async _saveMarker(): Promise<void> {
|
||||
// pdfs may be rewritten below when rebinding changes the marker id
|
||||
...this._markerLightFields(dlg),
|
||||
...this._markerValueBadgeFields(dlg),
|
||||
...this._markerValueSourceFields(dlg),
|
||||
use_climate_temp: dlg.useClimateTemp ? true : null,
|
||||
glow_radius_cm: (() => {
|
||||
const v = strictNumber(dlg.glowRadius);
|
||||
@@ -8083,6 +8092,10 @@ public async _saveMarker(): Promise<void> {
|
||||
&& marker.value_badge.source.ref === `marker:${oldId}`) {
|
||||
marker.value_badge.source = { kind: 'derived_marker_state', ref: `marker:${id}` };
|
||||
}
|
||||
if (oldId && oldId !== id && marker.value_source?.kind === 'derived_marker_state'
|
||||
&& marker.value_source.ref === `marker:${oldId}`) {
|
||||
marker.value_source = { kind: 'derived_marker_state', ref: `marker:${id}` };
|
||||
}
|
||||
// remove the previous marker (by the old id and by the new id)
|
||||
cfg.markers = cfg.markers.filter(
|
||||
(m) => m.id !== id && m.id !== oldId
|
||||
@@ -11747,6 +11760,17 @@ public _markerValueBadgeFields(
|
||||
});
|
||||
}
|
||||
|
||||
public _markerValueSourceFields(
|
||||
d: NonNullable<HouseplanEditorHostPort['_markerDialog']>,
|
||||
): Pick<Marker, 'value_source'> | Record<string, never> {
|
||||
return valueSourceWriteFields({
|
||||
touched: d.valueSourceTouched,
|
||||
originalHas: d.originalHasValueSource,
|
||||
original: d.originalValueSource,
|
||||
source: d.valueSource,
|
||||
});
|
||||
}
|
||||
|
||||
public _markerDraft(d: NonNullable<HouseplanEditorHostPort['_markerDialog']>): Marker | null {
|
||||
if (d.bindingMode === 'ha' && (!d.binding || d.binding === 'virtual')) return null;
|
||||
const roomRef = parseRoomRef(d.room);
|
||||
@@ -11773,6 +11797,7 @@ public _markerDraft(d: NonNullable<HouseplanEditorHostPort['_markerDialog']>): M
|
||||
controls: controls.length ? controls : null,
|
||||
...this._markerLightFields(d),
|
||||
...this._markerValueBadgeFields(d),
|
||||
...this._markerValueSourceFields(d),
|
||||
use_climate_temp: d.useClimateTemp ? true : null,
|
||||
glow_radius_cm: (() => {
|
||||
const value = strictNumber(d.glowRadius);
|
||||
@@ -12150,11 +12175,11 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
const badgeSourceMissing = !!effectiveBadgeSource
|
||||
&& !badgeCandidates.some((item) => item.key === badgeSourceKey);
|
||||
const selectedBadgeCandidate = badgeCandidates.find((item) => item.key === badgeSourceKey);
|
||||
const innerValueSourceKey = previewPresentation?.valueSource
|
||||
? previewPresentation.valueSource.attribute
|
||||
? `attr:${previewPresentation.valueSource.eid}:${previewPresentation.valueSource.attribute}`
|
||||
: `state:${previewPresentation.valueSource.eid}`
|
||||
: '';
|
||||
const valueSourceKey = valueBadgeSourceKey(d.valueSource);
|
||||
const valueSourceMissing = !!d.valueSource && (!valueSourceKey
|
||||
|| !badgeCandidates.some((item) => item.key === valueSourceKey));
|
||||
const selectedValueSourceCandidate = badgeCandidates.find((item) => item.key === valueSourceKey);
|
||||
const innerValueSourceKey = previewPresentation?.valueSource?.sourceKey || '';
|
||||
const autoHasSpatialSource = this._markerAutoHasSpatialSource(d);
|
||||
const statefulSource = !!previewDevice
|
||||
&& hasOwnStatefulLightSource(this.host._planHass, { ...previewDevice, hidden: false });
|
||||
@@ -12233,6 +12258,7 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
};
|
||||
this.host._markerDialog = this._announceToggleDraft({
|
||||
...next, ...this._valueBadgeForBinding(next, 'virtual'),
|
||||
valueSource: null, valueSourceTouched: true,
|
||||
});
|
||||
}} />
|
||||
<span>${this.host._t('marker.virtual_option')}</span>
|
||||
@@ -12280,6 +12306,7 @@ public _renderMarkerDialog(): TemplateResult {
|
||||
};
|
||||
this.host._markerDialog = this._announceToggleDraft({
|
||||
...next, ...this._valueBadgeForBinding(next, c.value),
|
||||
valueSource: null, valueSourceTouched: true,
|
||||
});
|
||||
}}>
|
||||
<span class="cl">${c.label}</span><span class="cs">${c.sub}</span>
|
||||
@@ -12583,6 +12610,39 @@ 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">
|
||||
<div class="markerhelplabel">
|
||||
<label for="marker-value-source">${this.host._t('marker.value_source')}</label>
|
||||
${this._help('marker.value_source.help')}
|
||||
</div>
|
||||
<select id="marker-value-source" class="areasel" ?disabled=${isVirtual}
|
||||
@change=${(e: Event) => (this.host._markerDialog = {
|
||||
...d,
|
||||
valueSource: valueBadgeSourceFromKey((e.target as HTMLSelectElement).value),
|
||||
valueSourceTouched: true,
|
||||
})}>
|
||||
<option value="" ?selected=${!d.valueSource}>
|
||||
${this.host._t('marker.value_source_auto')}
|
||||
</option>
|
||||
${valueSourceMissing ? html`<option value=${valueSourceKey || '__missing__'} selected>
|
||||
${this.host._t('marker.value_badge_missing')}
|
||||
</option>` : nothing}
|
||||
${badgeCandidates.map((candidate) => html`<option value=${candidate.key}
|
||||
?selected=${candidate.key === valueSourceKey}
|
||||
title=${candidate.technical}>
|
||||
${this._valueBadgeCandidateLabel(candidate)} · ${candidate.value}
|
||||
</option>`)}
|
||||
</select>
|
||||
${selectedValueSourceCandidate
|
||||
? html`<p class="muted markerbadgetechnical"><code>${selectedValueSourceCandidate.technical}</code></p>`
|
||||
: nothing}
|
||||
${isVirtual ? html`<p class="muted markerlightdisabled" role="note">
|
||||
<ha-icon icon="mdi:information-outline"></ha-icon>
|
||||
${this.host._t('marker.preview.reason.value_virtual')}
|
||||
</p>` : valueSourceMissing ? html`<p class="muted markerlightwarning" role="status">
|
||||
<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)
|
||||
? html`<div class="habindingbanner" role="note">
|
||||
<ha-icon icon="mdi:alert-outline"></ha-icon>
|
||||
|
||||
+6
-1
@@ -190,7 +190,7 @@
|
||||
"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": "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 eindeutiger Home Assistant-Wert ersetzt das Symbol, während die Platte weiterhin den Zustand anzeigt. Rote Alarme bleiben sichtbar.",
|
||||
"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.static_alarm_warning": "Dieses Gerät kann Alarme melden. Die statische Anzeige verbirgt die visuelle Alarmanzeige des Markers.",
|
||||
"marker.preview.title": "Anzeigevorschau",
|
||||
@@ -665,6 +665,11 @@
|
||||
"marker.value_badge_attr_volume_level": "Lautstärke · {name}",
|
||||
"marker.value_badge_attr_battery_level": "Batterie · {name}",
|
||||
"marker.value_badge_attr_fan_speed": "Lüftergeschwindigkeit · {name}",
|
||||
"marker.value_source": "Wertquelle",
|
||||
"marker.value_source.help": "Wähle den Wert, der das Symbol ersetzt. Die Tipp-Aktion bleibt unverändert.",
|
||||
"marker.value_source.help.aria": "Hilfe: Wertquelle in der Gerätefläche",
|
||||
"marker.value_source_auto": "Automatisch (wie bisher)",
|
||||
"marker.value_source_missing_hint": "Die gespeicherte Quelle ist derzeit nicht verfügbar. Bis sie wieder verfügbar ist oder ein anderer Wert gewählt wird, zeigt die Fläche —.",
|
||||
"marker.light_role_label": "Ist dieses Gerät eine Lichtquelle?",
|
||||
"marker.light_role.help": "Steuert, ob dieser Marker selbst eine räumliche Lichtquelle ist. Verknüpfte Lichter oben bleiben unabhängige Raumlichtsteuerungen.",
|
||||
"marker.light_role.help.aria": "Hilfe: ob dieses Gerät eine Lichtquelle ist",
|
||||
|
||||
+6
-1
@@ -190,7 +190,7 @@
|
||||
"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": "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": "One unambiguous Home Assistant value replaces the icon while the plate continues to show state. Red alarms remain visible.",
|
||||
"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.static_alarm_warning": "This device can report alarms. Static display hides the marker's visual alarm indication.",
|
||||
"marker.preview.title": "Display preview",
|
||||
@@ -665,6 +665,11 @@
|
||||
"marker.value_badge_attr_volume_level": "Volume · {name}",
|
||||
"marker.value_badge_attr_battery_level": "Battery · {name}",
|
||||
"marker.value_badge_attr_fan_speed": "Fan speed · {name}",
|
||||
"marker.value_source": "Value source",
|
||||
"marker.value_source.help": "Choose the value that replaces the icon. The tap action does not change.",
|
||||
"marker.value_source.help.aria": "Help: value face source",
|
||||
"marker.value_source_auto": "Automatic (as before)",
|
||||
"marker.value_source_missing_hint": "The saved source is currently unavailable. The face shows — until it recovers or you choose another value.",
|
||||
"marker.light_role_label": "Is this device a light source?",
|
||||
"marker.light_role.help": "Controls whether this marker itself is a spatial light source. Linked lights above remain independent room-light controls.",
|
||||
"marker.light_role.help.aria": "Help: whether this device is a light source",
|
||||
|
||||
+6
-1
@@ -190,7 +190,7 @@
|
||||
"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": "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 non ambiguë remplace l’icône tandis que le fond continue d’indiquer l’état. Les alarmes rouges restent visibles.",
|
||||
"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.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",
|
||||
@@ -665,6 +665,11 @@
|
||||
"marker.value_badge_attr_volume_level": "Volume · {name}",
|
||||
"marker.value_badge_attr_battery_level": "Batterie · {name}",
|
||||
"marker.value_badge_attr_fan_speed": "Vitesse du ventilateur · {name}",
|
||||
"marker.value_source": "Source de la valeur",
|
||||
"marker.value_source.help": "Choisissez la valeur qui remplace l’icône. L’action au toucher ne change pas.",
|
||||
"marker.value_source.help.aria": "Aide : source de la valeur dans le marqueur",
|
||||
"marker.value_source_auto": "Automatique (comme avant)",
|
||||
"marker.value_source_missing_hint": "La source enregistrée est indisponible. Le marqueur affiche — jusqu’à son retour ou jusqu’au choix d’une autre valeur.",
|
||||
"marker.light_role_label": "Cet appareil est-il une source lumineuse ?",
|
||||
"marker.light_role.help": "Détermine si ce marqueur est lui-même une source lumineuse spatiale. Les lumières liées ci-dessus restent des contrôles de lumière de pièce indépendants.",
|
||||
"marker.light_role.help.aria": "Aide : cet appareil est-il une source lumineuse",
|
||||
|
||||
+6
-1
@@ -190,7 +190,7 @@
|
||||
"marker.display_hint_badge": "Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.",
|
||||
"marker.display_hint_icon": "Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.",
|
||||
"marker.display_hint_icon_ripple": "Значок, динамическая подложка и пульсация: короткая — для событий, постоянная — для работы, движения или присутствия. Красная тревога имеет отдельный приоритет.",
|
||||
"marker.display_hint_value": "Значок заменяется одним однозначным значением Home Assistant; подложка продолжает показывать состояние. Красная тревога сохраняется.",
|
||||
"marker.display_hint_value": "Значок заменяется выбранным или автоматически найденным значением Home Assistant; подложка продолжает показывать состояние. Красная тревога сохраняется.",
|
||||
"marker.display_hint_static_icon": "Подложка в цветах темы и значок всегда остаются одинаковыми. Состояния, активность, недоступность и тревоги не меняют отображение.",
|
||||
"marker.static_alarm_warning": "Это устройство может сообщать о тревогах. Статичный режим скрывает визуальную тревогу маркера.",
|
||||
"marker.preview.title": "Предпросмотр отображения",
|
||||
@@ -665,6 +665,11 @@
|
||||
"marker.value_badge_attr_volume_level": "Громкость · {name}",
|
||||
"marker.value_badge_attr_battery_level": "Заряд батареи · {name}",
|
||||
"marker.value_badge_attr_fan_speed": "Скорость вентилятора · {name}",
|
||||
"marker.value_source": "Источник значения",
|
||||
"marker.value_source.help": "Выберите, что заменит значок. Действие по нажатию не изменится.",
|
||||
"marker.value_source.help.aria": "Подсказка: источник значения внутри значка",
|
||||
"marker.value_source_auto": "Автоматически (как раньше)",
|
||||
"marker.value_source_missing_hint": "Сохранённый источник сейчас недоступен. До его восстановления или выбора другого значения внутри будет показано —.",
|
||||
"marker.light_role_label": "Является источником света",
|
||||
"marker.light_role.help": "Определяет, является ли сам маркер пространственным источником света. Привязанные выше лампы остаются независимыми источниками для комнаты.",
|
||||
"marker.light_role.help.aria": "Подсказка: является ли устройство источником света",
|
||||
|
||||
@@ -176,6 +176,9 @@ export interface Marker {
|
||||
/** Optional, user-controlled value satellite around the device face.
|
||||
* Missing keeps the legacy temperature/humidity compatibility heuristic. */
|
||||
value_badge?: MarkerValueBadge | null;
|
||||
/** Explicit source for the `value` display face. Missing/null keeps the
|
||||
* historical automatic entity-state resolver. */
|
||||
value_source?: ValueBadgeSource | null;
|
||||
/**
|
||||
* Climate devices (AC, thermostat) know the room temperature
|
||||
* (attributes.current_temperature). Opt-in per marker: show it as a badge
|
||||
|
||||
Reference in New Issue
Block a user