feat: HA smart home control improvements

- stats_cache: swap interesting domains to controllable-only (remove sensors/binary_sensors)
- stats_cache: filter pre-release/camera/HA-addon switches from entity list
- index.html: add visual toggle switch buttons per entity
- index.html: fix scene activation (was returning early; now calls scene/turn_on)
- index.html: remove 8-per-domain cap on entity display
- index.html: add domain icons and unavailable state handling
- index.html: alarm panel arm/disarm toggle logic
This commit is contained in:
2026-05-31 05:05:44 +00:00
parent ca3ae31826
commit 499adadc6d
2 changed files with 71 additions and 16 deletions
+51 -13
View File
@@ -493,16 +493,36 @@ body::after{
/* HA DEVICES ────────────────────────────────────────────────────────── */
.ha-entity{
display:flex;align-items:center;justify-content:space-between;
padding:5px 0;border-bottom:1px solid rgba(0,212,255,0.06);
display:flex;align-items:center;gap:6px;
padding:4px 2px;border-bottom:1px solid rgba(0,212,255,0.06);
font-family:var(--font-mono);font-size:0.72rem;
cursor:pointer;transition:background 0.15s;border-radius:4px;
transition:background 0.15s;border-radius:4px;
}
.ha-entity:hover{background:rgba(0,212,255,0.06)}
.ha-name{color:var(--text);flex:1}
.ha-state{font-weight:600}
.ha-name{color:var(--text);flex:1;cursor:pointer}
.ha-state{font-weight:600;font-size:0.65rem;min-width:32px;text-align:right}
.ha-state.on{color:var(--green)}
.ha-state.off{color:var(--text-dim)}
.ha-state.unavailable{color:var(--text-dim);opacity:0.4}
/* toggle switch */
.ha-toggle{
position:relative;width:32px;height:16px;flex-shrink:0;cursor:pointer;
}
.ha-toggle input{opacity:0;width:0;height:0;position:absolute}
.ha-slider{
position:absolute;inset:0;border-radius:8px;
background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.15);
transition:background 0.2s,border-color 0.2s;
}
.ha-slider::before{
content:'';position:absolute;left:2px;top:2px;
width:10px;height:10px;border-radius:50%;
background:var(--text-dim);transition:transform 0.2s,background 0.2s;
}
.ha-toggle input:checked + .ha-slider{background:rgba(0,255,100,0.25);border-color:var(--green)}
.ha-toggle input:checked + .ha-slider::before{transform:translateX(16px);background:var(--green)}
.ha-toggle.scene .ha-slider{background:rgba(0,212,255,0.15);border-color:var(--cyan)}
.ha-toggle.scene .ha-slider::before{background:var(--cyan)}
/* ALERTS BADGE ─────────────────────────────────────────────────────── */
.alert-item{
@@ -1396,19 +1416,31 @@ async function loadHA() {
return;
}
const domainIcon = {light:'💡',switch:'🔌',scene:'🎬',media_player:'📺',
alarm_control_panel:'🔒',lawn_mower:'🌿',water_heater:'🌡',fan:'💨',
lock:'🔑',cover:'🪟',climate:'❄',input_boolean:'⚙'};
let html = '';
for (const [domain, items] of Object.entries(entities)) {
if (!items.length) continue;
html += `<div style="font-family:var(--font-display);font-size:0.55rem;letter-spacing:2px;color:var(--text-dim);margin:8px 0 4px">${domain.toUpperCase()}</div>`;
html += items.slice(0,8).map(e => {
const isOn = ['on','home','open','locked','playing'].includes(e.state);
const icon = domainIcon[domain] || '•';
html += `<div style="font-family:var(--font-display);font-size:0.55rem;letter-spacing:2px;color:var(--text-dim);margin:8px 0 4px">${icon} ${domain.toUpperCase()}</div>`;
html += items.map(e => {
const isOn = ['on','home','open','locked','playing','mowing','armed_home','armed_away','armed_night'].includes(e.state);
const isScene = domain === 'scene';
const unavail = e.state === 'unavailable';
const ctxKey = 'ha_' + e.entity_id.replace(/[^a-z0-9]/gi,'_');
_panelCtx[ctxKey] = {type:'ha', label:e.name,
entity_id:e.entity_id, name:e.name, state:e.state, domain:domain};
const stateLabel = unavail ? 'N/A' : (isScene ? 'RUN' : e.state.toUpperCase());
const stateClass = unavail ? 'unavailable' : (isOn ? 'on' : 'off');
const toggleEl = isScene
? `<button class="ha-toggle scene" onclick="toggleHA('${e.entity_id}','${domain}','${e.state}')" title="Activate scene" style="cursor:pointer;background:rgba(0,212,255,0.1);border:1px solid var(--cyan);border-radius:4px;color:var(--cyan);font-size:0.6rem;padding:1px 5px;font-family:var(--font-mono)">▶</button>`
: `<label class="ha-toggle${unavail?' disabled':''}"><input type="checkbox"${isOn?' checked':''}${unavail?' disabled':''} onchange="toggleHA('${e.entity_id}','${domain}','${e.state}')"><span class="ha-slider"></span></label>`;
return `<div class="ha-entity">
<span class="ha-name" onclick="toggleHA('${e.entity_id}','${domain}','${e.state}')">${e.name}</span>
<button class="ha-ask-btn" onclick="selectContext('${ctxKey}')" data-ctx-key="${ctxKey}" title="Ask Jarvis about this">ASK</button>
<span class="ha-state ${isOn?'on':'off'}" onclick="toggleHA('${e.entity_id}','${domain}','${e.state}')">${e.state.toUpperCase()}</span>
<span class="ha-name" onclick="selectContext('${ctxKey}')">${e.name}</span>
<button class="ha-ask-btn" onclick="selectContext('${ctxKey}')" data-ctx-key="${ctxKey}" title="Ask JARVIS">ASK</button>
<span class="ha-state ${stateClass}">${stateLabel}</span>
${toggleEl}
</div>`;
}).join('');
}
@@ -1416,8 +1448,14 @@ async function loadHA() {
}
async function toggleHA(entityId, domain, currentState) {
const service = currentState==='on'?'turn_off':'turn_on';
if (domain==='scene') { return; }
let service;
if (domain === 'scene') {
service = 'turn_on';
} else if (domain === 'alarm_control_panel') {
service = currentState === 'disarmed' ? 'alarm_arm_away' : 'alarm_disarm';
} else {
service = currentState === 'on' ? 'turn_off' : 'turn_on';
}
try {
await api('ha/service', 'POST', {domain, service, entity_id: entityId});
setTimeout(loadHA, 1500);