mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
A space carried an aspect ratio, and coordinates were normalised against it: x by the width, y by the height. Every geometric question therefore depended on a per-space number, and picking a canvas orientation was a decision the user had no reason to make. The render space is now NORM_W x NORM_W and a plan image is fitted into it by its OWN ratio, centred — wide plans get margins above and below, tall ones at the sides. Migration (geometry_migration.py, pure and unit-tested) runs once at setup under the write lock. Nothing about a drawing changes: the old box is padded out to a square and every coordinate re-expressed against it — rooms as rects and polygons, openings and their lengths, decor, view_box, and the marker positions in the separate layout store. In render units it is a uniform scale plus an offset, so angles and proportions are exact. cell_cm is scaled for tall plans, because the grid pitch is a fraction of the width: without it a wall would measure less than it does. is now dropped by the schema rather than accepted — a stale tab sending it would be sending coordinates from the old normalisation too, and honouring the field would not make them right. The demo fixture was migrated with the same transform, so the smokes exercise the new geometry rather than a square-native fake; six of them needed their render-space helpers updated and one its click coordinates. Not released — dev only, per the owner's instruction.
149 lines
8.0 KiB
HTML
149 lines
8.0 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>House Plan demo</title>
|
|
<style>
|
|
:root{
|
|
--primary-color:#3ea6ff; --primary-text-color:#e1e1e1; --secondary-text-color:#9aa4ad;
|
|
--card-background-color:#1c2530; --ha-card-background:#1c2530;
|
|
--divider-color:#33404d; --text-primary-color:#fff; --mdc-icon-size:24px;
|
|
--header-height:0px;
|
|
}
|
|
html,body{margin:0;background:#0e1621;color:#e1e1e1;font-family:'Segoe UI',Roboto,Arial,sans-serif;}
|
|
#host{width:780px;margin:0 auto;padding:8px;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="host"></div>
|
|
<script src="/assets/icons.js"></script>
|
|
<script>
|
|
class HaIcon extends HTMLElement{
|
|
static get observedAttributes(){return ['icon'];}
|
|
connectedCallback(){this._r();}
|
|
attributeChangedCallback(){this._r();}
|
|
set icon(v){this.setAttribute('icon',v);}
|
|
get icon(){return this.getAttribute('icon');}
|
|
_r(){
|
|
const p=(window.__ICONS||{})[this.getAttribute('icon')]||'';
|
|
if(!this.shadowRoot) this.attachShadow({mode:'open'});
|
|
// Faithful to HA's ha-icon: host is display:block with a large inherited line-height,
|
|
// which pushes the (smaller) svg off-centre unless the consumer neutralises it.
|
|
this.shadowRoot.innerHTML='<style>:host{display:block;line-height:1.8;'+
|
|
'width:var(--mdc-icon-size,24px);height:var(--mdc-icon-size,24px);'+
|
|
'fill:currentColor;vertical-align:middle}'+
|
|
'svg{width:var(--mdc-icon-size,24px);height:var(--mdc-icon-size,24px);fill:currentColor;'+
|
|
'display:inline-block;vertical-align:baseline}</style>'+
|
|
'<svg viewBox="0 0 24 24"><path d="'+p+'"></path></svg>';
|
|
}
|
|
}
|
|
customElements.define('ha-icon',HaIcon);
|
|
class HaCard extends HTMLElement{
|
|
connectedCallback(){
|
|
if(this.shadowRoot)return;
|
|
this.attachShadow({mode:'open'});
|
|
this.shadowRoot.innerHTML='<style>:host{display:block;background:var(--ha-card-background,#1c2530);'+
|
|
'border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.4);overflow:visible;color:var(--primary-text-color)}</style><slot></slot>';
|
|
}
|
|
}
|
|
customElements.define('ha-card',HaCard);
|
|
</script>
|
|
<script type="module">
|
|
const CFG = {
|
|
spaces: [
|
|
{ id:'f1', title:'Ground floor', plan_url:'/assets/f1.svg', plan_aspect:1.25,
|
|
view_box:[0,0,1,1],
|
|
rooms:[
|
|
{id:'r1', name:'Living room', area:'living_room', poly:[[0.04,0.14],[0.55,0.14],[0.55,0.58],[0.04,0.58]]},
|
|
{id:'r2', name:'Kitchen', area:'kitchen', poly:[[0.55,0.14],[0.96,0.14],[0.96,0.46],[0.55,0.46]]},
|
|
{id:'r3', name:'Bedroom', area:'bedroom', poly:[[0.55,0.46],[0.96,0.46],[0.96,0.86],[0.55,0.86]]},
|
|
{id:'r4', name:'Hallway', area:'hallway', poly:[[0.04,0.58],[0.55,0.58],[0.55,0.86],[0.04,0.86]]}
|
|
], segments:[] },
|
|
{ id:'garden', title:'Garden', plan_url:'/assets/garden.svg', plan_aspect:1.4286,
|
|
view_box:[0,0,1,1],
|
|
rooms:[ {id:'g1', name:'Garden', area:'garden', poly:[[0.03,0.178],[0.97,0.178],[0.97,0.822],[0.03,0.822]]} ], segments:[] }
|
|
],
|
|
markers: [],
|
|
settings: {}
|
|
};
|
|
const LAYOUT = {
|
|
d_light1:{s:'f1',x:0.22,y:0.22}, d_lamp:{s:'f1',x:0.42,y:0.5}, d_tv:{s:'f1',x:0.13,y:0.5},
|
|
d_temp:{s:'f1',x:0.33,y:0.35}, d_kettle:{s:'f1',x:0.72,y:0.15}, d_leak:{s:'f1',x:0.88,y:0.34},
|
|
d_bedlight:{s:'f1',x:0.75,y:0.6}, d_window:{s:'f1',x:0.9,y:0.85}, d_lock:{s:'f1',x:0.1,y:0.88},
|
|
d_motion:{s:'f1',x:0.4,y:0.75}, d_mower:{s:'garden',x:0.25,y:0.35}, d_gate:{s:'garden',x:0.73,y:0.22}
|
|
};
|
|
const AREAS={living_room:{area_id:'living_room',name:'Living room'},kitchen:{area_id:'kitchen',name:'Kitchen'},
|
|
bedroom:{area_id:'bedroom',name:'Bedroom'},hallway:{area_id:'hallway',name:'Hallway'},garden:{area_id:'garden',name:'Garden'}};
|
|
function dev(id,name,model,area){return [id,{id,name,model,area_id:area,identifiers:[['demo',id]],entry_type:null,via_device_id:null}];}
|
|
const DEVICES=Object.fromEntries([
|
|
dev('d_light1','Ceiling light','Smart Bulb E27','living_room'),
|
|
dev('d_lamp','Floor lamp','Smart Bulb E14','living_room'),
|
|
dev('d_tv','TV','55U8HQ','living_room'),
|
|
dev('d_temp','Temperature sensor','TH01','living_room'),
|
|
dev('d_kettle','Kettle plug','Smart Plug','kitchen'),
|
|
dev('d_leak','Sink leak sensor','SJCGQ11LM','kitchen'),
|
|
dev('d_bedlight','Bedroom light','Smart Bulb E27','bedroom'),
|
|
dev('d_window','Window sensor','MCCGQ11LM','bedroom'),
|
|
dev('d_lock','Front door lock','Smart Lock S2','hallway'),
|
|
dev('d_motion','Motion sensor','RTCGQ11LM','hallway'),
|
|
dev('d_mower','Robot mower','Automower 305','garden'),
|
|
dev('d_gate','Gate','GDO-4','garden'),
|
|
]);
|
|
const ENTITIES={
|
|
'light.ceiling':{entity_id:'light.ceiling',device_id:'d_light1',platform:'demo'},
|
|
'light.floor_lamp':{entity_id:'light.floor_lamp',device_id:'d_lamp',platform:'demo'},
|
|
'media_player.tv':{entity_id:'media_player.tv',device_id:'d_tv',platform:'demo'},
|
|
'sensor.living_temp':{entity_id:'sensor.living_temp',device_id:'d_temp',platform:'demo'},
|
|
'switch.kettle':{entity_id:'switch.kettle',device_id:'d_kettle',platform:'demo'},
|
|
'binary_sensor.sink_leak':{entity_id:'binary_sensor.sink_leak',device_id:'d_leak',platform:'demo'},
|
|
'light.bedroom':{entity_id:'light.bedroom',device_id:'d_bedlight',platform:'demo'},
|
|
'binary_sensor.window':{entity_id:'binary_sensor.window',device_id:'d_window',platform:'demo'},
|
|
'lock.front_door':{entity_id:'lock.front_door',device_id:'d_lock',platform:'demo'},
|
|
'binary_sensor.hall_motion':{entity_id:'binary_sensor.hall_motion',device_id:'d_motion',platform:'demo'},
|
|
'vacuum.mower':{entity_id:'vacuum.mower',device_id:'d_mower',platform:'demo'},
|
|
'cover.gate':{entity_id:'cover.gate',device_id:'d_gate',platform:'demo'},
|
|
};
|
|
let STATES={
|
|
'light.ceiling':{entity_id:'light.ceiling',state:'on',attributes:{friendly_name:'Ceiling light'}},
|
|
'light.floor_lamp':{entity_id:'light.floor_lamp',state:'off',attributes:{friendly_name:'Floor lamp',linkquality:196}},
|
|
'media_player.tv':{entity_id:'media_player.tv',state:'playing',attributes:{friendly_name:'TV'}},
|
|
'sensor.living_temp':{entity_id:'sensor.living_temp',state:'22.4',attributes:{friendly_name:'Temperature',device_class:'temperature',unit_of_measurement:'°C',linkquality:154}},
|
|
'switch.kettle':{entity_id:'switch.kettle',state:'off',attributes:{friendly_name:'Kettle plug',linkquality:182}},
|
|
'binary_sensor.sink_leak':{entity_id:'binary_sensor.sink_leak',state:'off',attributes:{friendly_name:'Leak',device_class:'moisture',linkquality:117}},
|
|
'light.bedroom':{entity_id:'light.bedroom',state:'off',attributes:{friendly_name:'Bedroom light'}},
|
|
'binary_sensor.window':{entity_id:'binary_sensor.window',state:'on',attributes:{friendly_name:'Window',device_class:'window',linkquality:88}},
|
|
'lock.front_door':{entity_id:'lock.front_door',state:'locked',attributes:{friendly_name:'Front door',linkquality:143}},
|
|
'binary_sensor.hall_motion':{entity_id:'binary_sensor.hall_motion',state:'on',attributes:{friendly_name:'Motion',device_class:'motion',linkquality:64}},
|
|
'vacuum.mower':{entity_id:'vacuum.mower',state:'cleaning',attributes:{friendly_name:'Mower'}},
|
|
'cover.gate':{entity_id:'cover.gate',state:'closed',attributes:{friendly_name:'Gate'}},
|
|
};
|
|
function mkHass(){
|
|
return {
|
|
language:'en', locale:{language:'en'},
|
|
devices:DEVICES, entities:ENTITIES, areas:AREAS, states:STATES,
|
|
floors:{g:{floor_id:'g',name:'Ground floor',level:0},u:{floor_id:'u',name:'Upstairs',level:1}},
|
|
callWS:async (m)=>{
|
|
if(m.type==='houseplan/config/get')return{config:CFG,rev:1};
|
|
if(m.type==='houseplan/layout/get')return{layout:LAYOUT};
|
|
return {ok:true,rev:2};
|
|
},
|
|
callService:async (d,s,data)=>{
|
|
const eid=data.entity_id, st=STATES[eid];
|
|
if(st){const on=['on','playing','unlocked','open'].includes(st.state);
|
|
STATES={...STATES,[eid]:{...st,state:on?'off':'on'}};
|
|
card.hass=mkHass();}
|
|
},
|
|
connection:{subscribeEvents:async()=>()=>{}},
|
|
formatEntityState:(st)=>st.state,
|
|
};
|
|
}
|
|
await import('/assets/houseplan-card.js');
|
|
const card=document.createElement('houseplan-card');
|
|
card.setConfig({type:'custom:houseplan-card', title:'House Plan', icon_size:3.4});
|
|
document.getElementById('host').appendChild(card);
|
|
card.hass=mkHass();
|
|
window.__card=card;
|
|
window.__mkHass=mkHass;
|
|
</script>
|
|
</body></html>
|