mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 14:19:04 +00:00
feat: add per-room temperature thresholds
Issue: #487 User-Visible: yes
This commit is contained in:
+17
-17
@@ -107,7 +107,7 @@ import {
|
||||
} from './initial-load';
|
||||
import { selectActiveSpaceModel, selectSpaceModelById } from './space-model-selection';
|
||||
import {
|
||||
createEmptySpaceConfig, initialSpaceDisplayDraft, switchSpacePlanSource, touchSpaceDisplay,
|
||||
createEmptySpaceConfig, initialSpaceDisplayDraft, roomTempRangeFromDraft, switchSpacePlanSource, touchSpaceDisplay,
|
||||
type SpaceDialogState,
|
||||
} from './space-dialog';
|
||||
import { mdiHomeCityOutline } from '@mdi/js';
|
||||
@@ -1991,7 +1991,7 @@ export class HouseplanCard extends LitElement {
|
||||
private _roomFill: '' | 'none' | 'lqi' | 'light' | 'temp' | 'custom' = ''; // '' = inherit
|
||||
/** null = inherit the space custom color; value = explicit room override. */
|
||||
private _roomCustomFill: FillColorEntry | null = null;
|
||||
private _roomTempSrc = ''; // '' = average
|
||||
private _roomTempMin = ''; private _roomTempMax = ''; private _roomTempSrc = ''; // blank inherits; zero is explicit
|
||||
private _roomHumSrc = '';
|
||||
private _roomSrcOpen: 'temp' | 'hum' | null = null;
|
||||
private _roomSrcFilter = '';
|
||||
@@ -2573,6 +2573,7 @@ export class HouseplanCard extends LitElement {
|
||||
_roomEditId: { state: true },
|
||||
_roomFill: { state: true },
|
||||
_roomCustomFill: { state: true },
|
||||
_roomTempMin: { state: true }, _roomTempMax: { state: true },
|
||||
_roomTempSrc: { state: true },
|
||||
_roomHumSrc: { state: true },
|
||||
_roomSrcOpen: { state: true },
|
||||
@@ -3499,7 +3500,7 @@ export class HouseplanCard extends LitElement {
|
||||
if (this._roomDialog) {
|
||||
return at('room', {
|
||||
editId: this._roomEditId, fill: this._roomFill, customFill: this._roomCustomFill,
|
||||
tempSrc: this._roomTempSrc,
|
||||
tempMin: this._roomTempMin, tempMax: this._roomTempMax, tempSrc: this._roomTempSrc,
|
||||
humSrc: this._roomHumSrc, srcOpen: this._roomSrcOpen, srcFilter: this._roomSrcFilter,
|
||||
nameScale: this._roomNameScale, labelScale: this._roomLabelScale,
|
||||
areaSel: this._areaSel, nameSel: this._nameSel,
|
||||
@@ -3584,7 +3585,8 @@ export class HouseplanCard extends LitElement {
|
||||
case 'room': {
|
||||
const r = d.data;
|
||||
this._roomEditId = r.editId; this._roomFill = r.fill;
|
||||
this._roomCustomFill = r.customFill || null; this._roomTempSrc = r.tempSrc;
|
||||
this._roomCustomFill = r.customFill || null; this._roomTempMin = r.tempMin || '';
|
||||
this._roomTempMax = r.tempMax || ''; this._roomTempSrc = r.tempSrc;
|
||||
this._roomHumSrc = r.humSrc; this._roomSrcOpen = r.srcOpen; this._roomSrcFilter = r.srcFilter;
|
||||
this._roomNameScale = r.nameScale; this._roomLabelScale = r.labelScale;
|
||||
this._areaSel = r.areaSel; this._nameSel = r.nameSel;
|
||||
@@ -9285,6 +9287,8 @@ export class HouseplanCard extends LitElement {
|
||||
const customFill = this._roomDialog && room.id === this._roomEditId
|
||||
? (this._roomCustomFill || disp.customFill)
|
||||
: roomCustomFillOf(disp.customFill, room);
|
||||
const tempRange = roomTempRangeFromDraft(disp.tempMin, disp.tempMax, room,
|
||||
this._roomTempMin, this._roomTempMax, this._roomDialog && room.id === this._roomEditId);
|
||||
const resolved = resolveEffectiveRoomFill(
|
||||
mode,
|
||||
mode === 'lqi' && room.area ? this._roomLqi(room.area) : null,
|
||||
@@ -9294,8 +9298,8 @@ export class HouseplanCard extends LitElement {
|
||||
))
|
||||
: 'none',
|
||||
mode === 'temp' ? this._roomTemp(room) : null,
|
||||
disp.tempMin,
|
||||
disp.tempMax,
|
||||
tempRange.min,
|
||||
tempRange.max,
|
||||
this._fillColors,
|
||||
customFill,
|
||||
);
|
||||
@@ -12555,20 +12559,16 @@ export class HouseplanCard extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
/** Room temperature honouring the tier-3 source override. */
|
||||
/** Room climate value honouring the tier-3 source override. */
|
||||
private _roomTemp(r: RoomCfg): number | null {
|
||||
const src = r.settings?.temp_source;
|
||||
if (src) return sourceValue(this._renderPlanHass, src, 'temp', this._markers);
|
||||
const key = roomClimateKey(this._spaceModel()?.id, r);
|
||||
return key ? this._climate().get(key)?.temp ?? null : null;
|
||||
const src = r.settings?.temp_source, key = roomClimateKey(this._spaceModel()?.id, r);
|
||||
return src ? sourceValue(this._renderPlanHass, src, 'temp', this._markers)
|
||||
: key ? this._climate().get(key)?.temp ?? null : null;
|
||||
}
|
||||
|
||||
/** Room humidity honouring the tier-3 source override. */
|
||||
private _roomHum(r: RoomCfg): number | null {
|
||||
const src = r.settings?.hum_source;
|
||||
if (src) return sourceValue(this._renderPlanHass, src, 'hum', this._markers);
|
||||
const key = roomClimateKey(this._spaceModel()?.id, r);
|
||||
return key ? this._climate().get(key)?.hum ?? null : null;
|
||||
const src = r.settings?.hum_source, key = roomClimateKey(this._spaceModel()?.id, r);
|
||||
return src ? sourceValue(this._renderPlanHass, src, 'hum', this._markers)
|
||||
: key ? this._climate().get(key)?.hum ?? null : null;
|
||||
}
|
||||
|
||||
// Ключи сравниваются только по ссылке (`===`), значение не читается ни разу.
|
||||
|
||||
@@ -126,10 +126,12 @@ import {
|
||||
} from './initial-load';
|
||||
import { selectActiveSpaceModel, selectSpaceModelById } from './space-model-selection';
|
||||
import {
|
||||
createEmptySpaceConfig, initialSpaceDisplayDraft, strictNumber, switchSpacePlanSource,
|
||||
applyRoomTempThresholdDraft, createEmptySpaceConfig, initialSpaceDisplayDraft,
|
||||
roomTempThresholdDraft, roomTempThresholdInputValues, strictNumber, switchSpacePlanSource,
|
||||
touchSpaceDisplay, type SpaceDialogState,
|
||||
} from './space-dialog';
|
||||
import { commitPlanOptimization } from './plan-optimize-write';
|
||||
import { roomTemperatureControls } from './room-temperature-controls';
|
||||
import { openSpaceCopyDialog, renderSpaceCopyDialog, saveSpaceCopy } from './space-copy-runtime';
|
||||
import { mdiHomeCityOutline } from '@mdi/js';
|
||||
import {
|
||||
@@ -1087,9 +1089,9 @@ export interface HouseplanEditorHostPort {
|
||||
_roomHumSrc: string;
|
||||
_roomLabelScale: number;
|
||||
_roomNameScale: number;
|
||||
_roomTempMin: string; _roomTempMax: string; _roomTempSrc: string;
|
||||
_roomSrcFilter: string;
|
||||
_roomSrcOpen: "temp" | "hum" | null;
|
||||
_roomTempSrc: string;
|
||||
_roomWallOpeningInputs: (openings?: readonly RenderOpening[], space?: SpaceModel | undefined) => Array<{ x: number; y: number; angle: number; length: number; }>;
|
||||
_rszLimitViolation: JunctionLimitViolation | null;
|
||||
_rulesDialog: { rules: IconRule[]; test: string; busy: boolean; } | null;
|
||||
@@ -6687,6 +6689,7 @@ public _markupMove(ev: MouseEvent): void {
|
||||
|
||||
public _saveRoom(): void {
|
||||
if (!this.host._areaSel && !this.host._nameSel.trim()) return;
|
||||
if (!roomTempThresholdDraft(this.host._roomTempMin, this.host._roomTempMax).valid) return;
|
||||
if (this.host._wallFaceBatch) {
|
||||
this._decideWallFace(true);
|
||||
return;
|
||||
@@ -10916,6 +10919,7 @@ public _resetRoomDialogFields(): void {
|
||||
this.host._roomEditId = null;
|
||||
this.host._roomFill = '';
|
||||
this.host._roomCustomFill = null;
|
||||
this.host._roomTempMin = ''; this.host._roomTempMax = '';
|
||||
this.host._roomTempSrc = '';
|
||||
this.host._roomHumSrc = '';
|
||||
this.host._roomSrcOpen = null;
|
||||
@@ -10936,6 +10940,7 @@ public _openRoomEdit(r: RoomCfg): void {
|
||||
this.host._roomCustomFill = rawCustom && typeof rawCustom === 'object'
|
||||
? customFillOf(rawCustom, spaceDisplayOf(this.host._curSpaceCfg).customFill)
|
||||
: null;
|
||||
[this.host._roomTempMin, this.host._roomTempMax] = roomTempThresholdInputValues(r.settings);
|
||||
this.host._roomTempSrc = r.settings?.temp_source || '';
|
||||
this.host._roomHumSrc = r.settings?.hum_source || '';
|
||||
this.host._roomNameScale = clampScale(r.settings?.name_scale);
|
||||
@@ -10951,6 +10956,7 @@ public _roomSettingsFromDialog(): RoomCfg['settings'] {
|
||||
if (this.host._roomCustomFill) st.custom_fill = this.host._roomCustomFill;
|
||||
if (this.host._roomTempSrc) st.temp_source = this.host._roomTempSrc;
|
||||
if (this.host._roomHumSrc) st.hum_source = this.host._roomHumSrc;
|
||||
applyRoomTempThresholdDraft(st, this.host._roomTempMin, this.host._roomTempMax);
|
||||
if (this.host._roomNameScale !== 1) st.name_scale = this.host._roomNameScale;
|
||||
if (this.host._roomLabelScale !== 1) st.label_scale = this.host._roomLabelScale;
|
||||
return Object.keys(st).length ? st : null;
|
||||
@@ -10971,6 +10977,7 @@ public _saveRoomEdit(): void {
|
||||
// seemingly unrelated room edit cannot switch the light overlay off.
|
||||
const previous = room.settings || {};
|
||||
const next: any = { ...previous };
|
||||
if (!applyRoomTempThresholdDraft(next, this.host._roomTempMin, this.host._roomTempMax)) return;
|
||||
if (this.host._roomFill) next.fill_mode = this.host._roomFill;
|
||||
else delete next.fill_mode;
|
||||
if (this.host._roomCustomFill) next.custom_fill = this.host._roomCustomFill;
|
||||
@@ -13885,10 +13892,11 @@ public _renderRoomDialog(): TemplateResult {
|
||||
current: faceBatch.index + 1, total: faceBatch.candidates.length,
|
||||
})
|
||||
: '';
|
||||
const canSaveNew = !!this.host._areaSel || !!this.host._nameSel.trim();
|
||||
const spaceDisplay = spaceDisplayOf(this.host._curSpaceCfg);
|
||||
const effectiveFill = this.host._roomFill || spaceDisplay.fill;
|
||||
const customFill = this.host._roomCustomFill || spaceDisplay.customFill;
|
||||
const tempValid = roomTempThresholdDraft(this.host._roomTempMin, this.host._roomTempMax).valid;
|
||||
const canSaveNew = (!!this.host._areaSel || !!this.host._nameSel.trim()) && tempValid;
|
||||
// the free-areas list must include the edited room's CURRENT area
|
||||
const areas = [...this.host._freeAreas];
|
||||
if (edit && this.host._areaSel && !areas.some((a) => a.area_id === this.host._areaSel)) {
|
||||
@@ -13951,9 +13959,9 @@ public _renderRoomDialog(): TemplateResult {
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="roomtemprange-host">${roomTemperatureControls(this.host, effectiveFill, spaceDisplay.tempMin, spaceDisplay.tempMax, this._help('room.temp_range.help')).template}</div>
|
||||
${this._renderRoomSource('temp')}
|
||||
${this._renderRoomSource('hum')}
|
||||
|
||||
<label class="dispsection">${this.host._t('room.sizes_section')}</label>
|
||||
<label>${this.host._t('room.name_scale')}</label>
|
||||
<div class="colorrow gsrow">
|
||||
@@ -13975,7 +13983,7 @@ public _renderRoomDialog(): TemplateResult {
|
||||
<button class="btn ghost" @click=${() => this._roomDialogCancel()}>${this.host._t('btn.cancel')}</button>
|
||||
<span class="spacer"></span>
|
||||
${edit
|
||||
? html`<button class="btn on" @click=${() => this._saveRoomEdit()} ?disabled=${!this.host._nameSel.trim()}>
|
||||
? html`<button class="btn on" @click=${() => this._saveRoomEdit()} ?disabled=${!this.host._nameSel.trim() || !tempValid}>
|
||||
<ha-icon icon="mdi:check"></ha-icon>${this.host._t('btn.save')}
|
||||
</button>`
|
||||
: html`${!this.host._pendingSplit ? html`<button class="btn ghost" @click=${() => this._keepClosedAsPartitions()}>
|
||||
|
||||
@@ -667,6 +667,12 @@
|
||||
"fill.inherit": "Als den Bereich",
|
||||
"room.custom_fill_space": "Bereichsfarbe",
|
||||
"room.custom_fill_own": "Raumfarbe",
|
||||
"room.temp_range_label": "Komfortbereich",
|
||||
"room.temp_range_min": "Untere Grenze der Komforttemperatur",
|
||||
"room.temp_range_max": "Obere Grenze der Komforttemperatur",
|
||||
"room.temp_range_reset": "Wie im Bereich",
|
||||
"room.temp_range.help": "Dieser Bereich gilt nur für diesen Raum. Leere Felder übernehmen die jeweilige Grenze des Bereichs; sind die Grenzen vertauscht, verwendet House Plan den kleineren Wert als Untergrenze und den größeren als Obergrenze.",
|
||||
"room.temp_range.help.aria": "Informationen zum Komfortbereich des Raums",
|
||||
"room.temp_src_label": "Temperaturquelle",
|
||||
"room.hum_src_label": "Luftfeuchtigkeitsquelle",
|
||||
"room.src_average": "Durchschnitt über die Sensoren des Raums (Standard)",
|
||||
|
||||
@@ -667,6 +667,12 @@
|
||||
"fill.inherit": "As the space",
|
||||
"room.custom_fill_space": "Space color",
|
||||
"room.custom_fill_own": "Room color",
|
||||
"room.temp_range_label": "Comfort range",
|
||||
"room.temp_range_min": "Lower comfort-temperature bound",
|
||||
"room.temp_range_max": "Upper comfort-temperature bound",
|
||||
"room.temp_range_reset": "As the space",
|
||||
"room.temp_range.help": "This range applies only to this room. Empty fields inherit the corresponding space bounds; if the bounds are reversed, House Plan uses the smaller value as the lower bound and the larger value as the upper bound.",
|
||||
"room.temp_range.help.aria": "About the room comfort range",
|
||||
"room.temp_src_label": "Temperature source",
|
||||
"room.hum_src_label": "Humidity source",
|
||||
"room.src_average": "Average over the room's sensors (default)",
|
||||
|
||||
@@ -667,6 +667,12 @@
|
||||
"fill.inherit": "Comme l’espace",
|
||||
"room.custom_fill_space": "Couleur de l’espace",
|
||||
"room.custom_fill_own": "Couleur de la pièce",
|
||||
"room.temp_range_label": "Plage de confort",
|
||||
"room.temp_range_min": "Limite basse de température confortable",
|
||||
"room.temp_range_max": "Limite haute de température confortable",
|
||||
"room.temp_range_reset": "Comme l’espace",
|
||||
"room.temp_range.help": "Cette plage s’applique uniquement à cette pièce. Un champ vide hérite de la limite correspondante de l’espace ; si les limites sont inversées, House Plan utilise la plus petite comme limite basse et la plus grande comme limite haute.",
|
||||
"room.temp_range.help.aria": "À propos de la plage de confort de la pièce",
|
||||
"room.temp_src_label": "Source de température",
|
||||
"room.hum_src_label": "Source d’humidité",
|
||||
"room.src_average": "Moyenne des capteurs de la pièce (par défaut)",
|
||||
|
||||
@@ -667,6 +667,12 @@
|
||||
"fill.inherit": "Как у пространства",
|
||||
"room.custom_fill_space": "Цвет пространства",
|
||||
"room.custom_fill_own": "Цвет комнаты",
|
||||
"room.temp_range_label": "Диапазон комфорта",
|
||||
"room.temp_range_min": "Нижняя граница комфортной температуры",
|
||||
"room.temp_range_max": "Верхняя граница комфортной температуры",
|
||||
"room.temp_range_reset": "Как у пространства",
|
||||
"room.temp_range.help": "Диапазон применяется только к этой комнате. Пустое поле наследует соответствующую границу пространства; если границы поменялись местами, House Plan использует меньшую как нижнюю, а большую как верхнюю.",
|
||||
"room.temp_range.help.aria": "О диапазоне комфорта комнаты",
|
||||
"room.temp_src_label": "Источник температуры",
|
||||
"room.hum_src_label": "Источник влажности",
|
||||
"room.src_average": "Средняя по датчикам комнаты (по умолчанию)",
|
||||
|
||||
@@ -1309,6 +1309,32 @@ export function roomCustomFillOf(
|
||||
return own && typeof own === 'object' ? customFillOf(own, spaceFill) : spaceFill;
|
||||
}
|
||||
|
||||
export interface RoomTempRange {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective comfort range for one room. Each side inherits independently,
|
||||
* then the complete pair is normalised. Invalid legacy/future values behave
|
||||
* like an absent override and are never written back by this read boundary.
|
||||
*/
|
||||
export function roomTempRangeOf(
|
||||
spaceMin: number,
|
||||
spaceMax: number,
|
||||
room: {
|
||||
settings?: { temp_min?: unknown; temp_max?: unknown } | null;
|
||||
} | null | undefined,
|
||||
): RoomTempRange {
|
||||
const finiteOr = (value: unknown, fallback: number): number =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||
const inheritedMin = finiteOr(spaceMin, DEFAULT_TEMP_MIN);
|
||||
const inheritedMax = finiteOr(spaceMax, DEFAULT_TEMP_MAX);
|
||||
const rawMin = finiteOr(room?.settings?.temp_min, inheritedMin);
|
||||
const rawMax = finiteOr(room?.settings?.temp_max, inheritedMax);
|
||||
return { min: Math.min(rawMin, rawMax), max: Math.max(rawMin, rawMax) };
|
||||
}
|
||||
|
||||
/** Resolve a space's display settings; spaces without a plan default to visible markup. */
|
||||
export function spaceDisplayOf(spaceCfg: any): SpaceDisplay {
|
||||
const s = spaceCfg?.settings || {};
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { html, nothing, type TemplateResult } from 'lit';
|
||||
import { roomTempThresholdDraft, strictNumber } from './space-dialog';
|
||||
|
||||
interface RoomTemperatureControlsHost {
|
||||
_roomTempMin: string;
|
||||
_roomTempMax: string;
|
||||
_t: (key: any) => string;
|
||||
requestUpdate: () => unknown;
|
||||
}
|
||||
|
||||
export interface RoomTemperatureControls {
|
||||
valid: boolean;
|
||||
template: TemplateResult | typeof nothing;
|
||||
}
|
||||
|
||||
/** Lazy-editor UI for the optional per-room comfort bounds. */
|
||||
export function roomTemperatureControls(
|
||||
host: RoomTemperatureControlsHost,
|
||||
effectiveFill: string,
|
||||
spaceMin: number,
|
||||
spaceMax: number,
|
||||
help: TemplateResult | typeof nothing,
|
||||
): RoomTemperatureControls {
|
||||
const draft = roomTempThresholdDraft(host._roomTempMin, host._roomTempMax);
|
||||
if (effectiveFill !== 'temp') return { valid: draft.valid, template: nothing };
|
||||
const minInvalid = host._roomTempMin.trim() !== '' && strictNumber(host._roomTempMin) === null;
|
||||
const maxInvalid = host._roomTempMax.trim() !== '' && strictNumber(host._roomTempMax) === null;
|
||||
return {
|
||||
valid: draft.valid,
|
||||
template: html`<div class="roomtemprange">
|
||||
<div class="roomtemprange-head">
|
||||
<label>${host._t('room.temp_range_label')}</label>
|
||||
${help}
|
||||
</div>
|
||||
<div class="roomtemprange-fields">
|
||||
<input class="namein" type="number" step="0.5"
|
||||
aria-label=${host._t('room.temp_range_min')}
|
||||
aria-invalid=${minInvalid ? 'true' : 'false'}
|
||||
placeholder=${String(spaceMin)} .value=${host._roomTempMin}
|
||||
@input=${(e: Event) => {
|
||||
host._roomTempMin = (e.target as HTMLInputElement).value;
|
||||
host.requestUpdate();
|
||||
}} />
|
||||
<span aria-hidden="true">—</span>
|
||||
<input class="namein" type="number" step="0.5"
|
||||
aria-label=${host._t('room.temp_range_max')}
|
||||
aria-invalid=${maxInvalid ? 'true' : 'false'}
|
||||
placeholder=${String(spaceMax)} .value=${host._roomTempMax}
|
||||
@input=${(e: Event) => {
|
||||
host._roomTempMax = (e.target as HTMLInputElement).value;
|
||||
host.requestUpdate();
|
||||
}} />
|
||||
<span>°C</span>
|
||||
${host._roomTempMin.trim() || host._roomTempMax.trim()
|
||||
? html`<button class="btn ghost" type="button" @click=${() => {
|
||||
host._roomTempMin = '';
|
||||
host._roomTempMax = '';
|
||||
host.requestUpdate();
|
||||
}}>${host._t('room.temp_range_reset')}</button>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>`,
|
||||
};
|
||||
}
|
||||
+67
-1
@@ -1,4 +1,4 @@
|
||||
import type { FillColorEntry } from './logic';
|
||||
import { roomTempRangeOf, type FillColorEntry, type RoomTempRange } from './logic';
|
||||
import type { ZeroWallStyle } from './types';
|
||||
|
||||
export type SpacePlanSource = 'file' | 'draw';
|
||||
@@ -119,3 +119,69 @@ export function strictNumber(value: string): number | null {
|
||||
const parsed = Number(text);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
export interface RoomTempThresholdDraft {
|
||||
valid: boolean;
|
||||
min: number | null;
|
||||
max: number | null;
|
||||
}
|
||||
|
||||
/** Parse room threshold inputs without conflating an empty field with zero. */
|
||||
export function roomTempThresholdDraft(
|
||||
rawMin: string,
|
||||
rawMax: string,
|
||||
): RoomTempThresholdDraft {
|
||||
const parse = (raw: string): { valid: boolean; value: number | null } => {
|
||||
if (String(raw ?? '').trim() === '') return { valid: true, value: null };
|
||||
const value = strictNumber(raw);
|
||||
return { valid: value !== null, value };
|
||||
};
|
||||
const lower = parse(rawMin);
|
||||
const upper = parse(rawMax);
|
||||
if (!lower.valid || !upper.valid) {
|
||||
return { valid: false, min: lower.value, max: upper.value };
|
||||
}
|
||||
if (lower.value !== null && upper.value !== null && lower.value > upper.value) {
|
||||
return { valid: true, min: upper.value, max: lower.value };
|
||||
}
|
||||
return { valid: true, min: lower.value, max: upper.value };
|
||||
}
|
||||
|
||||
/** Resolve saved or pending room bounds through the same production inheritance rule. */
|
||||
export function roomTempRangeFromDraft(
|
||||
spaceMin: number,
|
||||
spaceMax: number,
|
||||
room: { settings?: { temp_min?: unknown; temp_max?: unknown } | null },
|
||||
rawMin: string,
|
||||
rawMax: string,
|
||||
useDraft: boolean,
|
||||
): RoomTempRange {
|
||||
const draft = roomTempThresholdDraft(rawMin, rawMax);
|
||||
const source = useDraft && draft.valid
|
||||
? { settings: { ...(room.settings || {}), temp_min: draft.min, temp_max: draft.max } }
|
||||
: room;
|
||||
return roomTempRangeOf(spaceMin, spaceMax, source);
|
||||
}
|
||||
|
||||
export function roomTempThresholdInputValues(
|
||||
settings: { temp_min?: unknown; temp_max?: unknown } | null | undefined,
|
||||
): [string, string] {
|
||||
const value = (value: unknown): string =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? String(value) : '';
|
||||
return [value(settings?.temp_min), value(settings?.temp_max)];
|
||||
}
|
||||
|
||||
/** Mutate a dialog-owned settings draft; false means the inputs must not be saved. */
|
||||
export function applyRoomTempThresholdDraft(
|
||||
settings: Record<string, unknown>,
|
||||
rawMin: string,
|
||||
rawMax: string,
|
||||
): boolean {
|
||||
const draft = roomTempThresholdDraft(rawMin, rawMax);
|
||||
if (!draft.valid) return false;
|
||||
if (draft.min === null) delete settings.temp_min;
|
||||
else settings.temp_min = draft.min;
|
||||
if (draft.max === null) delete settings.temp_max;
|
||||
else settings.temp_max = draft.max;
|
||||
return true;
|
||||
}
|
||||
|
||||
+4
-3
@@ -13,7 +13,7 @@ import {
|
||||
} from './devices';
|
||||
import {
|
||||
spaceDisplayOf, fillColorsOf, roomFillModeOf, roomGlowOf,
|
||||
roomCustomFillOf, resolveEffectiveRoomFill, stageBgOf, paperRoomShapes,
|
||||
roomCustomFillOf, roomTempRangeOf, resolveEffectiveRoomFill, stageBgOf, paperRoomShapes,
|
||||
openingAmount, roomPoly, outlineWithout, islandsOf,
|
||||
type ResolvedRoomFill,
|
||||
} from './logic';
|
||||
@@ -494,6 +494,7 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
// and therefore receives the same base darkness as explicit `none`.
|
||||
const resolvedRoomFills = new Map(space.rooms.map((room) => {
|
||||
const fill = roomFillModeOf(disp.fill, room);
|
||||
const tempRange = roomTempRangeOf(disp.tempMin, disp.tempMax, room);
|
||||
return [room, resolveEffectiveRoomFill(
|
||||
fill,
|
||||
fill === 'lqi' && room.area ? areaLqi(planHass, spaceDevs, room.area) : null,
|
||||
@@ -501,8 +502,8 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
? resolvedLightState(resolvedLightSources(planHass, spaceDevs, room, o.virtualLights))
|
||||
: 'none',
|
||||
fill === 'temp' ? roomTemperature(room) : null,
|
||||
disp.tempMin,
|
||||
disp.tempMax,
|
||||
tempRange.min,
|
||||
tempRange.max,
|
||||
colors,
|
||||
roomCustomFillOf(disp.customFill, room),
|
||||
)] as const;
|
||||
|
||||
@@ -398,11 +398,36 @@ export const dialogsStyles = css`
|
||||
font-weight: 600;
|
||||
color: var(--hp-txt) !important;
|
||||
}
|
||||
.colorrow {
|
||||
.colorrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-4);
|
||||
}
|
||||
}
|
||||
.roomtemprange {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.roomtemprange-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.roomtemprange-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.roomtemprange-fields input {
|
||||
flex: 0 1 104px;
|
||||
min-width: 72px;
|
||||
}
|
||||
.roomtemprange-fields input[aria-invalid='true'] {
|
||||
border-color: var(--error-color, #db4437);
|
||||
}
|
||||
.gsrow > hp-color-opacity {
|
||||
min-width: min(100%, 210px);
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -17,6 +17,9 @@ export interface RoomCfg {
|
||||
/** 'device:<id>' or 'entity:<eid>'; unset = average over the room sensors. */
|
||||
temp_source?: string | null;
|
||||
hum_source?: string | null;
|
||||
/** Room comfort bounds; each absent/null side inherits from the space. */
|
||||
temp_min?: number | null;
|
||||
temp_max?: number | null;
|
||||
/** Font multipliers for THIS room's card (0.5-3, unset = 1). */
|
||||
name_scale?: number | null;
|
||||
label_scale?: number | null;
|
||||
|
||||
Reference in New Issue
Block a user