Skip to content

Commit f4cc488

Browse files
squizzer73claude
andcommitted
v0.1.3: display improvements + humidity sensor support
- Fill available space with flex layout (ha-card height:100%, grid flex:1) - Pseudo-3D room tiles: asymmetric borders, depth gradient, drop shadow - Light-on glow effect with warm amber gradient - Pulsing occupancy dot (top-right, animated @Keyframes) - Temperature + humidity displayed top-left (blue tint for humidity) - Room name in gradient fade overlay at bottom - Humidity entity picker added to room editor Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 930e75c commit f4cc488

2 files changed

Lines changed: 117 additions & 109 deletions

File tree

src/house-card-editor.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ class HouseCardEditor extends LitElement {
801801
<div style="flex:1;font-size:0.88rem;">${room.name}</div>
802802
<div style="font-size:0.75rem;color:var(--secondary-text-color);">
803803
${room.width}×${room.height}
804-
${room.entities?.light || room.entities?.occupancy || room.entities?.temperature ? '· entities configured' : '· no entities'}
804+
${room.entities?.light || room.entities?.occupancy || room.entities?.temperature || room.entities?.humidity ? '· entities configured' : '· no entities'}
805805
</div>
806806
</div>
807807
`)}
@@ -838,6 +838,7 @@ class HouseCardEditor extends LitElement {
838838
{ key: 'light', label: 'Light', domains: ['light'] },
839839
{ key: 'occupancy', label: 'Occupancy', domains: ['binary_sensor'] },
840840
{ key: 'temperature', label: 'Temperature', domains: ['sensor'] },
841+
{ key: 'humidity', label: 'Humidity', domains: ['sensor'] },
841842
].map(({ key, label, domains }) => html`
842843
<div class="entity-row">
843844
<div class="entity-type-label">${label}</div>

src/house-card.js

Lines changed: 115 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,6 @@
11
import { LitElement, html, css } from 'lit';
22
import './house-card-editor.js';
33

4-
/**
5-
* House Card - Visual floorplan card for Home Assistant
6-
*
7-
* Config schema:
8-
* {
9-
* type: 'custom:house-card',
10-
* title: 'My House',
11-
* floors: [
12-
* {
13-
* id: 'ground',
14-
* name: 'Ground Floor',
15-
* cols: 8,
16-
* rows: 6,
17-
* rooms: [
18-
* {
19-
* id: 'lounge',
20-
* name: 'Lounge',
21-
* col: 0, row: 0, width: 3, height: 2,
22-
* color: '#4a90d9',
23-
* entities: {
24-
* light: 'light.lounge',
25-
* occupancy: 'binary_sensor.lounge_motion',
26-
* temperature: 'sensor.lounge_temperature',
27-
* }
28-
* }
29-
* ]
30-
* }
31-
* ]
32-
* }
33-
*/
34-
35-
364
class HouseCard extends LitElement {
375
static get properties() {
386
return {
@@ -46,12 +14,16 @@ class HouseCard extends LitElement {
4614
return css`
4715
:host {
4816
display: block;
17+
height: 100%;
4918
font-family: var(--primary-font-family, sans-serif);
5019
}
5120
5221
ha-card {
5322
overflow: hidden;
5423
padding: 0;
24+
height: 100%;
25+
display: flex;
26+
flex-direction: column;
5527
}
5628
5729
.card-header {
@@ -62,13 +34,15 @@ class HouseCard extends LitElement {
6234
font-size: 1.1rem;
6335
font-weight: 500;
6436
color: var(--primary-text-color);
37+
flex-shrink: 0;
6538
}
6639
6740
.floor-tabs {
6841
display: flex;
6942
gap: 4px;
7043
padding: 10px 16px 0;
7144
border-bottom: 1px solid var(--divider-color, rgba(255,255,255,0.1));
45+
flex-shrink: 0;
7246
}
7347
7448
.floor-tab {
@@ -95,84 +69,123 @@ class HouseCard extends LitElement {
9569
}
9670
9771
.grid-wrapper {
98-
padding: 16px;
72+
padding: 12px;
9973
box-sizing: border-box;
74+
flex: 1;
75+
display: flex;
76+
flex-direction: column;
77+
min-height: 0;
10078
}
10179
10280
.grid-canvas {
10381
position: relative;
10482
width: 100%;
83+
flex: 1;
84+
min-height: 80px;
10585
}
10686
87+
/* ── Room cell — 3D raised tile ── */
88+
10789
.room-cell {
10890
position: absolute;
10991
box-sizing: border-box;
110-
border: 1px solid rgba(255,255,255,0.15);
111-
border-radius: 4px;
92+
border-radius: 6px;
11293
overflow: hidden;
113-
transition: background 0.4s ease;
11494
cursor: default;
95+
transition: box-shadow 0.4s ease;
96+
/* Asymmetric borders: bright top/left, dark bottom/right */
97+
border-top: 1px solid rgba(255,255,255,0.18);
98+
border-left: 1px solid rgba(255,255,255,0.12);
99+
border-right: 1px solid rgba(0,0,0,0.28);
100+
border-bottom: 1px solid rgba(0,0,0,0.35);
101+
/* Drop shadow + inner highlight */
102+
box-shadow: 2px 3px 6px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.08);
103+
/* Depth gradient on top of inline background-color */
104+
background-image: linear-gradient(145deg, rgba(255,255,255,0.07) 0%, rgba(0,0,0,0.1) 100%);
115105
}
116106
117107
.room-cell.light-on {
118-
background: rgba(255, 220, 80, 0.35);
119-
box-shadow: inset 0 0 12px rgba(255, 220, 80, 0.2);
120-
}
121-
122-
.room-cell.light-off {
123-
background: rgba(255, 255, 255, 0.04);
108+
background-image: linear-gradient(145deg, rgba(255,235,100,0.48) 0%, rgba(255,185,30,0.22) 100%);
109+
box-shadow: 2px 3px 6px rgba(0,0,0,0.45),
110+
inset 0 0 22px rgba(255,220,80,0.18),
111+
0 0 12px rgba(255,200,50,0.14);
124112
}
125113
126114
.room-cell.occupied {
127-
border-color: rgba(80, 200, 120, 0.5);
115+
border-top-color: rgba(80,200,120,0.45);
116+
border-left-color: rgba(80,200,120,0.35);
128117
}
129118
130-
.room-label {
131-
position: absolute;
132-
bottom: 4px;
133-
left: 6px;
134-
font-size: 0.7rem;
135-
font-weight: 500;
136-
color: var(--primary-text-color);
137-
opacity: 0.8;
138-
pointer-events: none;
139-
white-space: nowrap;
140-
overflow: hidden;
141-
text-overflow: ellipsis;
142-
max-width: calc(100% - 8px);
143-
}
119+
/* ── Sensor info (top-left) ── */
144120
145-
.room-icons {
121+
.room-info {
146122
position: absolute;
147-
top: 4px;
148-
right: 4px;
123+
top: 5px;
124+
left: 6px;
149125
display: flex;
150126
flex-direction: column;
151-
align-items: flex-end;
152-
gap: 2px;
127+
gap: 3px;
128+
pointer-events: none;
153129
}
154130
155131
.room-temp {
156-
position: absolute;
157-
top: 4px;
158-
left: 6px;
159-
font-size: 0.7rem;
132+
font-size: 0.82rem;
133+
font-weight: 700;
134+
color: rgba(255,255,255,0.95);
135+
text-shadow: 0 1px 4px rgba(0,0,0,0.85);
136+
line-height: 1;
137+
}
138+
139+
.room-humidity {
140+
font-size: 0.72rem;
160141
font-weight: 600;
161-
color: var(--primary-text-color);
162-
opacity: 0.85;
142+
color: rgba(140,210,255,0.92);
143+
text-shadow: 0 1px 4px rgba(0,0,0,0.85);
144+
line-height: 1;
163145
}
164146
147+
/* ── Occupancy dot (top-right) ── */
148+
165149
.occupancy-dot {
166-
width: 8px;
167-
height: 8px;
150+
position: absolute;
151+
top: 6px;
152+
right: 6px;
153+
width: 11px;
154+
height: 11px;
168155
border-radius: 50%;
169-
background: #50c878;
170-
box-shadow: 0 0 6px rgba(80, 200, 120, 0.8);
156+
background: #4cdf80;
157+
box-shadow: 0 0 0 2px rgba(76,223,128,0.3), 0 0 10px rgba(76,223,128,0.75);
158+
animation: occ-pulse 2s ease-in-out infinite;
159+
pointer-events: none;
171160
}
172161
173-
.light-icon {
174-
color: #ffdc50;
175-
font-size: 0.8rem;
162+
@keyframes occ-pulse {
163+
0%, 100% {
164+
box-shadow: 0 0 0 2px rgba(76,223,128,0.3), 0 0 10px rgba(76,223,128,0.75);
165+
}
166+
50% {
167+
box-shadow: 0 0 0 4px rgba(76,223,128,0.15), 0 0 18px rgba(76,223,128,0.95);
168+
}
169+
}
170+
171+
/* ── Room name (bottom gradient overlay) ── */
172+
173+
.room-label {
174+
position: absolute;
175+
bottom: 0;
176+
left: 0;
177+
right: 0;
178+
padding: 6px 6px 5px;
179+
font-size: 0.72rem;
180+
font-weight: 600;
181+
color: rgba(255,255,255,0.93);
182+
text-align: center;
183+
text-shadow: 0 1px 4px rgba(0,0,0,0.95);
184+
white-space: nowrap;
185+
overflow: hidden;
186+
text-overflow: ellipsis;
187+
background: linear-gradient(to top, rgba(0,0,0,0.55) 0%, transparent 100%);
188+
pointer-events: none;
176189
}
177190
178191
.no-floor {
@@ -201,15 +214,7 @@ class HouseCard extends LitElement {
201214
static getStubConfig() {
202215
return {
203216
title: 'My House',
204-
floors: [
205-
{
206-
id: 'ground',
207-
name: 'Ground Floor',
208-
cols: 8,
209-
rows: 6,
210-
rooms: [],
211-
},
212-
],
217+
floors: [{ id: 'ground', name: 'Ground Floor', cols: 8, rows: 6, rooms: [] }],
213218
};
214219
}
215220

@@ -219,13 +224,11 @@ class HouseCard extends LitElement {
219224
}
220225

221226
_isLightOn(entityId) {
222-
const state = this._getEntityState(entityId);
223-
return state?.state === 'on';
227+
return this._getEntityState(entityId)?.state === 'on';
224228
}
225229

226230
_isOccupied(entityId) {
227-
const state = this._getEntityState(entityId);
228-
return state?.state === 'on';
231+
return this._getEntityState(entityId)?.state === 'on';
229232
}
230233

231234
_getTemperature(entityId) {
@@ -237,24 +240,25 @@ class HouseCard extends LitElement {
237240
return `${val.toFixed(1)}${unit}`;
238241
}
239242

240-
_renderFloor(floor) {
241-
if (!floor || !floor.cols || !floor.rows) return html`<div class="no-floor">Floor not configured</div>`;
243+
_getHumidity(entityId) {
244+
const state = this._getEntityState(entityId);
245+
if (!state) return null;
246+
const val = parseFloat(state.state);
247+
if (isNaN(val)) return null;
248+
return `${Math.round(val)}%`;
249+
}
242250

251+
_renderFloor(floor) {
252+
if (!floor || !floor.cols || !floor.rows) {
253+
return html`<div class="no-floor">Floor not configured</div>`;
254+
}
243255
const rooms = floor.rooms || [];
244-
const cols = floor.cols;
245-
const rows = floor.rows;
246-
247-
// We render the grid as a percentage-based positioned canvas
248-
// Each cell = (100/cols)% wide, (100/rows)% tall
249-
const cellWPct = 100 / cols;
250-
const cellHPct = 100 / rows;
251-
252-
// Use a padding-bottom trick to maintain aspect ratio
253-
const aspectPct = (rows / cols) * 100;
256+
const cellWPct = 100 / floor.cols;
257+
const cellHPct = 100 / floor.rows;
254258

255259
return html`
256260
<div class="grid-wrapper">
257-
<div class="grid-canvas" style="padding-bottom: ${aspectPct}%; position: relative;">
261+
<div class="grid-canvas" style="aspect-ratio: ${floor.cols} / ${floor.rows};">
258262
${rooms.map(room => this._renderRoom(room, cellWPct, cellHPct))}
259263
</div>
260264
</div>
@@ -265,6 +269,7 @@ class HouseCard extends LitElement {
265269
const lightOn = room.entities?.light ? this._isLightOn(room.entities.light) : false;
266270
const occupied = room.entities?.occupancy ? this._isOccupied(room.entities.occupancy) : false;
267271
const temp = room.entities?.temperature ? this._getTemperature(room.entities.temperature) : null;
272+
const humidity = room.entities?.humidity ? this._getHumidity(room.entities.humidity) : null;
268273

269274
const left = `${room.col * cellWPct}%`;
270275
const top = `${room.row * cellHPct}%`;
@@ -273,20 +278,22 @@ class HouseCard extends LitElement {
273278

274279
const classes = [
275280
'room-cell',
276-
lightOn ? 'light-on' : 'light-off',
281+
lightOn ? 'light-on' : '',
277282
occupied ? 'occupied' : '',
278283
].filter(Boolean).join(' ');
279284

280285
return html`
281286
<div
282287
class="${classes}"
283-
style="left:${left}; top:${top}; width:${width}; height:${height};"
288+
style="left:${left}; top:${top}; width:${width}; height:${height}; background-color:${room.color}22;"
284289
>
285-
${temp ? html`<div class="room-temp">${temp}</div>` : ''}
286-
<div class="room-icons">
287-
${occupied ? html`<div class="occupancy-dot"></div>` : ''}
288-
${lightOn ? html`<span class="light-icon">💡</span>` : ''}
289-
</div>
290+
${(temp || humidity) ? html`
291+
<div class="room-info">
292+
${temp ? html`<span class="room-temp">${temp}</span>` : ''}
293+
${humidity ? html`<span class="room-humidity">💧 ${humidity}</span>` : ''}
294+
</div>
295+
` : ''}
296+
${occupied ? html`<div class="occupancy-dot"></div>` : ''}
290297
<div class="room-label">${room.name}</div>
291298
</div>
292299
`;

0 commit comments

Comments
 (0)