11import { LitElement , html , css } from 'lit' ;
22import './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-
364class 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