From b6bcca62788d85cbac5dbe0b86541693b2558766 Mon Sep 17 00:00:00 2001 From: Stream Date: Wed, 15 Apr 2026 15:00:01 +0300 Subject: [PATCH] auto-sync: 2026-04-15 15:00:01 --- .../dashboard/availability.yaml | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 tasks/ha-availability-dashboard/dashboard/availability.yaml diff --git a/tasks/ha-availability-dashboard/dashboard/availability.yaml b/tasks/ha-availability-dashboard/dashboard/availability.yaml new file mode 100644 index 0000000..4fc37b9 --- /dev/null +++ b/tasks/ha-availability-dashboard/dashboard/availability.yaml @@ -0,0 +1,270 @@ +title: Доступность устройств +views: + - title: Доступность устройств + path: availability + cards: + # ═══ Переключатель периода ═══ + - type: entities + entities: + - entity: input_select.avail_period + name: Период + card_mod: + style: | + ha-card { + padding: 8px 16px !important; + margin-bottom: 4px !important; + } + + # ═══ Кнопка обновить + статус ═══ + - type: entities + entities: + - entity: sensor.avail_calc_progress + name: Статус расчёта + - type: button + name: Принудительный пересчёт + action_name: Обновить + tap_action: + action: call-service + service: input_select.select_option + target: + entity_id: input_select.avail_period + data: + option: "7d" + card_mod: + style: | + ha-card { + padding: 8px 16px !important; + margin-bottom: 4px !important; + } + + # ═══ Сводная карточка ═══ + - type: custom:button-card + entity: sensor.avail_calc_progress + show_name: false + show_state: false + show_icon: false + custom_fields: + card: | + [[[ + const devs = Object.entries(hass.states) + .filter(([k]) => k.startsWith('sensor.avail_') && !k.startsWith('sensor.avail_area_') && !k.startsWith('sensor.avail_calc_')) + .map(([k,v]) => ({ id: k, pct: parseFloat(v.state), attrs: v.attributes })); + + if (!devs.length) return '
Нет данных
'; + + const avg = (devs.reduce((s,d) => s + d.pct, 0) / devs.length).toFixed(1); + const problems = devs.filter(d => d.pct < 95).sort((a,b) => a.pct - b.pct); + const worse = devs.filter(d => d.attrs.trend === 'down').length; + const better = devs.filter(d => d.attrs.trend === 'up').length; + + const avgClr = avg < 90 ? 'var(--error-color)' : avg < 95 ? 'var(--warning-color)' : avg < 99 ? '#f0c040' : 'var(--success-color)'; + + let h = `
`; + + // Средний % + h += `
Средняя доступность
`; + h += `
`; + h += `
`; + h += `
`; + h += `
`; + h += `
${avg}%
`; + h += `
`; + + // Проблемные + if (problems.length) { + h += `
🔴 Проблемных (<95%): ${problems.length}
`; + problems.forEach(p => { + const ic = {switch:'🔌',light:'💡',binary_sensor:'🔘',sensor:'🌡️',climate:'❄️',cover:'🪟',lock:'🔒',media_player:'📺',fan:'🌀',vacuum:'🤖',device_tracker:'📍',humidifier:'💧',water_heater:'🚿',siren:'🚨',button:'🔔'}[p.attrs.entity_id?.split('.')[0]] || '📟'; + const pc = p.pct < 90 ? 'var(--error-color)' : 'var(--warning-color)'; + const ti = p.attrs.trend === 'down' ? '📉' : p.attrs.trend === 'up' ? '📈' : '➡️'; + h += `
`; + h += `${ic}${p.attrs.friendly_name || p.id}`; + h += `${p.pct}%${ti}`; + h += `
`; + }); + } + + // Тренды + h += `
`; + h += `📉 Хуже: ${worse}📈 Лучше: ${better}`; + h += `
`; + h += `
`; + return h; + ]]] + styles: + card: + - padding: 0 + - background: var(--card-background-color) + custom_fields: + card: + - padding: 0 + - margin: 0 + + # ═══ Группы по комнатам ═══ + # Каждая комната — отдельная collapsible секция с auto-entities + # Фильтр по атрибуту area. Сортировка: проблемные комнаты первые. + + - type: custom:auto-entities + card: + type: vertical-stack + filter: + include: + - entity_id: "sensor.avail_area_*" + exclude: [] + sort: + method: state + numeric: true + card_param: cards + card_template: + type: conditional + conditions: &area_cond + - entity: "[[entity.entity_id]]" + state_not: "0" # always show + row: {} # placeholder + # We can't easily do nested auto-entities per area in Lovelace. + # Instead, render each area as a section header + its devices. + + # ═══ Альтернативный подход: все устройства одной auto-entities ═══ + # Группировка по area — визуальная через JavaScript в button-card + + - type: custom:auto-entities + card: + type: vertical-stack + filter: + include: + - entity_id: "sensor.avail_area_*" + options: + type: custom:button-card + entity: "[[entity.entity_id]]" + show_name: false + show_state: false + show_icon: false + tap_action: + action: none + custom_fields: + area: | + [[[ + const pct = parseFloat(entity.state); + const a = entity.attributes; + const clr = pct < 90 ? 'var(--error-color)' : pct < 95 ? 'var(--warning-color)' : pct < 99 ? '#f0c040' : 'var(--success-color)'; + const iconMap = {'спальня':'🛏️','кухня':'🍳','ванная':'🚿','гостиная':'🛋️','прихожая':'🚪','дом':'🏠','улица':'🌳','гараж':'🚗','подвал':'🏚️','балкон':'🌅','кабинет':'💼','детская':'🧸'}; + const name = a.friendly_name || ''; + const ic = iconMap[name.toLowerCase()] || '📍'; + let h = `
`; + h += `${ic}`; + h += `${name}`; + h += `${pct}%`; + h += `${a.device_count||'?'} устр.`; + if (a.problem_count > 0) { + h += `${a.problem_count}`; + } + h += `
`; + h += `
`; + return h; + ]]] + styles: + card: + - padding: 4px 16px + - background: transparent + - box-shadow: none + custom_fields: + area: + - padding: 0 + - entity_id: "sensor.avail_*" + options: + type: custom:button-card + entity: "[[entity.entity_id]]" + show_name: false + show_state: false + show_icon: false + tap_action: + action: more-info + custom_fields: + dev: | + [[[ + const pct = parseFloat(entity.state); + const a = entity.attributes; + const clr = pct < 90 ? 'var(--error-color)' : pct < 95 ? 'var(--warning-color)' : pct < 99 ? '#f0c040' : 'var(--success-color)'; + const ic = {switch:'🔌',light:'💡',binary_sensor:'🔘',sensor:'🌡️',climate:'❄️',cover:'🪟',lock:'🔒',media_player:'📺',fan:'🌀',vacuum:'🤖',device_tracker:'📍',humidifier:'💧',water_heater:'🚿',siren:'🚨',button:'🔔'}[a.entity_id?.split('.')[0]] || '📟'; + const ti = a.trend === 'down' ? '📉' : a.trend === 'up' ? '📈' : '➡️'; + + let downInfo = ''; + if (a.down_count > 0) { + const mh = Math.floor((a.max_downtime_minutes||0)/60); + const mm = (a.max_downtime_minutes||0) % 60; + downInfo = `${a.down_count} пад., макс ${mh > 0 ? mh+'ч ':''}${mm}мин`; + } + + // Sparkline SVG + let spark = ''; + if (a.sparkline && a.sparkline.length > 1) { + const pts = a.sparkline; + const mn = Math.min(...pts), mx = Math.max(...pts), rng = mx-mn||1; + const w=120, h=20; + const svgPts = pts.map((v,i) => `${(i/(pts.length-1))*w},${h-((v-mn)/rng)*h}`).join(' '); + spark = ``; + } + + // Desktop + let o = `
`; + o += `
`; + o += `${ic}`; + o += `${a.friendly_name||a.entity_id}`; + o += `${pct}%`; + o += `${ti}`; + o += `
`; + o += `
`; + o += `
`; + o += `
`; + o += `
`; + o += `
`; + if (downInfo || spark) { + o += `
`; + o += `${downInfo}`; + o += spark; + o += `
`; + } + o += `
`; + + // Mobile + o += ``; + + return o; + ]]] + styles: + card: + - padding: 6px 16px + - background: transparent + - box-shadow: none + custom_fields: + dev: + - padding: 0 + card_mod: + style: | + ha-card { padding: 6px 16px !important; box-shadow: none !important; background: transparent !important; } + .spark-d { display: block; } + .av-desk { display: block; } + .av-mob { display: none !important; } + @media (max-width: 768px) { + .spark-d { display: none !important; } + .av-desk { display: none !important; } + .av-mob { display: block !important; } + } + exclude: + - entity_id: "sensor.avail_calc_*" + sort: + method: attribute + attribute: area + secondary_sort: + method: state + numeric: true + reverse: true + card: + type: vertical-stack + show_empty: true