From 499adadc6dcd860be2893d74368386df232a971e Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Sun, 31 May 2026 05:05:44 +0000 Subject: [PATCH] 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 --- api/endpoints/stats_cache.php | 23 +++++++++++-- public_html/index.html | 64 ++++++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 16 deletions(-) diff --git a/api/endpoints/stats_cache.php b/api/endpoints/stats_cache.php index f8c37b4..deb85f4 100644 --- a/api/endpoints/stats_cache.php +++ b/api/endpoints/stats_cache.php @@ -133,13 +133,30 @@ if (HA_TOKEN !== 'YOUR_HA_TOKEN_HERE' && strpos(HA_URL, '10.48.200.X') === false $states = $statesRaw ? json_decode($statesRaw, true) : []; $config = $configRaw ? json_decode($configRaw, true) : []; - $interesting = ['light','switch','sensor','climate','binary_sensor','cover', - 'media_player','camera','alarm_control_panel','lock','fan','input_boolean']; + // Controllable domains only — skip read-only sensors to keep list manageable + $interesting = ['light','switch','scene','media_player','alarm_control_panel', + 'lawn_mower','water_heater','fan','lock','cover','climate','input_boolean']; + // Switches that are HA internals / camera settings, not physical devices + $skipKeywords = ['pre_release','_record','_ftp_','_push_','_hub_ringtone', + '_siren_on','_email_on','_manual_record','_infrared_', + 'do_not_disturb','matter_server','zerotier','mariadb', + 'spotify_connect','file_editor','ssh_web','uptime_kuma', + 'folding_home','music_assistant','get_hacs','mealie', + 'mosquitto','social_to','esphome_device','motion_detection', + 'front_yard_record','down_hill_record','camera1_record', + 'back_yard_record','nvr_','assist_microphone','cec_scanner', + 'kiosker','hacs_pre','adguard']; $grouped = []; foreach (($states ?? []) as $entity) { $domain = explode('.', $entity['entity_id'])[0]; if (!in_array($domain, $interesting)) continue; - if (strpos($entity['entity_id'], 'adguard') !== false) continue; + if ($domain === 'switch') { + $skip = false; + foreach ($skipKeywords as $kw) { + if (strpos($entity['entity_id'], $kw) !== false) { $skip = true; break; } + } + if ($skip) continue; + } if (!isset($grouped[$domain])) $grouped[$domain] = []; $grouped[$domain][] = [ 'entity_id' => $entity['entity_id'], diff --git a/public_html/index.html b/public_html/index.html index 3cf74b3..796a750 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -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 += `
${domain.toUpperCase()}
`; - html += items.slice(0,8).map(e => { - const isOn = ['on','home','open','locked','playing'].includes(e.state); + const icon = domainIcon[domain] || '•'; + html += `
${icon} ${domain.toUpperCase()}
`; + 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 + ? `` + : ``; return `
- ${e.name} - - ${e.state.toUpperCase()} + ${e.name} + + ${stateLabel} + ${toggleEl}
`; }).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);