From ed3c9b58960101b820640a85707f9e02b6ac9c56 Mon Sep 17 00:00:00 2001 From: xiaoshi <115949669+xiaoshi930@users.noreply.github.com> Date: Sat, 6 Dec 2025 21:54:27 +0800 Subject: [PATCH] Delete xiaoshi-device-ha-info-button.js --- xiaoshi-device-ha-info-button.js | 2107 ------------------------------ 1 file changed, 2107 deletions(-) delete mode 100644 xiaoshi-device-ha-info-button.js diff --git a/xiaoshi-device-ha-info-button.js b/xiaoshi-device-ha-info-button.js deleted file mode 100644 index 3dd226e..0000000 --- a/xiaoshi-device-ha-info-button.js +++ /dev/null @@ -1,2107 +0,0 @@ -import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module"; - -class XiaoshiHaInfoButtonEditor extends LitElement { - static get properties() { - return { - hass: { type: Object }, - config: { type: Object } - }; - } - - static get styles() { - return css` - .form { - display: flex; - flex-direction: column; - gap: 10px; - } - .form-group { - display: flex; - flex-direction: column; - gap: 5px; - } - label { - font-weight: bold; - } - select, input, textarea { - padding: 8px; - border: 1px solid #ddd; - border-radius: 4px; - } - textarea { - min-height: 80px; - resize: vertical; - } - .help-text { - font-size: 0.85em; - color: #666; - margin-top: 4px; - } - - /*button新元素 开始*/ - .checkbox-group { - display: flex; - align-items: center; - gap: 0; - margin: 0; - padding: 0; - } - - .checkbox-input { - margin: 0; - } - - .checkbox-label { - font-weight: normal; - margin: 0; - } - /*button新元素 结束*/ - `; - } - - render() { - if (!this.hass) return html``; - - return html` - - -
- - -
- -
- - -
- - -
- -
- -
- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - -
- - - -
- - - -
-
- - -
- -
- - -
- -
- -
如果勾选,将包含标记为跳过的版本更新
-
- -
- - -
- 支持通配符匹配,例如 *客厅* 会匹配所有包含"客厅"的设备 -
-
- -
- - -
- 支持通配符匹配,例如 sensor.* 会匹配所有以 sensor. 开头的实体 -
-
-
- - `; - } - - _entityChanged(e) { - - /*button新按钮方法 开始*/ - const { name, value, type, checked } = e.target; - - let finalValue; - // 处理复选框 - if (type === 'checkbox') { - finalValue = checked; - } else { - if (!value && name !== 'theme' && name !== 'button_width' && name !== 'button_height' && name !== 'button_font_size' && name !== 'button_icon_size' && name !== 'width' && name !== 'tap_action') return; - finalValue = value - } - - // 处理不同字段的默认值 - if (name === 'button_width') { - finalValue = value || '100%'; - } else if (name === 'button_height') { - finalValue = value || '24px'; - } else if (name === 'button_font_size') { - finalValue = value || '11px'; - } else if (name === 'button_icon_size') { - finalValue = value || '13px'; - } else if (name === 'width') { - finalValue = value || '100%'; - } else if (name === 'exclude_entities') { - // 将文本行转换为数组 - finalValue = value ? value.split('\n').filter(line => line.trim()).map(line => line.trim()) : []; - } else if (name === 'exclude_devices') { - // 将文本行转换为数组 - finalValue = value ? value.split('\n').filter(line => line.trim()).map(line => line.trim()) : []; - } else if (name === 'tap_action') { - // 处理 tap_action 的特殊逻辑 - if (value === 'tap_action') { - // 如果是弹出卡片,则不设置 tap_action,让组件使用默认逻辑 - finalValue = undefined; - } else { - finalValue = value; - } - } - /*button新按钮方法 结束*/ - - this.config = { - ...this.config, - [name]: finalValue - }; - - this.dispatchEvent(new CustomEvent('config-changed', { - detail: { config: this.config }, - bubbles: true, - composed: true - })); - } - - setConfig(config) { - this.config = config; - } -} -customElements.define('xiaoshi-ha-info-button-editor', XiaoshiHaInfoButtonEditor); - -export class XiaoshiHaInfoButton extends LitElement { - static get properties() { - return { - hass: Object, - config: Object, - _haUpdates: Array, - _otherUpdates: Array, - _offlineDevices: Array, - _offlineEntities: Array, - _loading: Boolean, - _refreshInterval: Number, - _dataLoaded: Boolean, //button新元素 - theme: { type: String } - }; - } - - static get styles() { - return css` - :host { - display: block; - width: var(--card-width, 100%); - } - - /*button新元素 开始*/ - .ha-info-status { - width: var(--button-width, 70px); - height: var(--button-height, 24px); - padding: 0; - margin: 0; - background: var(--bg-color, #fff); - color: var(--fg-color, #000); - border-radius: 10px; - font-size: var(--button-font-size, 11px); - font-weight: 500; - text-align: center; - box-sizing: border-box; - display: flex; - align-items: center; - justify-content: center; - gap: 0; - cursor: pointer; - transition: background-color 0.2s, transform 0.1s; - position: relative; - } - - .status-icon { - --mdc-icon-size: var(--button-icon-size, 13px); - color: var(--fg-color, #000); - margin-right: 3px; - } - - /* 角标模式样式 */ - .ha-info-status.badge-mode { - width: var(--button-width, 70px); - height: var(--button-height, 24px); - border-radius: 10px; - padding: 0; - margin: 0; - display: flex; - align-items: center; - justify-content: center; - } - - .ha-info-status.badge-mode .status-icon { - color: rgb(128, 128, 128); - transition: color 0.2s; - } - - .ha-info-status.badge-mode.has-warning .status-icon { - color: rgb(255, 0, 0); - } - - .badge-number { - position: absolute; - top: -6px; - right: -6px; - min-width: 12px; - height: 12px; - background: rgb(255, 0, 0); - color: rgb(255, 255, 255); - border-radius: 50%; - font-size: 8px; - font-weight: bold; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - box-sizing: border-box; - line-height: 1; - } - - /*button新元素 结束*/ - - ha-card { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - background: var(--bg-color, #fff); - border-radius: 12px; - } - - /*标题容器*/ - .card-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px; - background: var(--bg-color, #fff); - border-radius: 12px; - } - - /*标题红色圆点*/ - .offline-indicator { - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - margin-right: 8px; - } - - /*标题红色圆点动画*/ - @keyframes pulse { - 0% { opacity: 1; } - 50% { opacity: 0.5; } - 100% { opacity: 1; } - } - - /*标题*/ - .card-title { - font-size: 20px; - font-weight: 500; - color: var(--fg-color, #000); - height: 30px; - line-height: 30px; - display: flex; - align-items: center; - justify-content: center; - } - - /*标题统计数字*/ - .device-count { - color: var(--fg-color, #000); - border-radius: 8px; - font-size: 13px; - width: 30px; - height: 30px; - text-align: center; - line-height: 30px; - font-weight: bold; - padding: 0px; - } - - .device-count.non-zero { - background: rgb(255, 0, 0, 0.5); - } - - .device-count.zero { - background: rgb(0, 205, 0); - } - - /*标题刷新按钮*/ - .refresh-btn { - color: var(--fg-color, #fff); - border: none; - border-radius: 8px; - padding: 5px; - cursor: pointer; - font-size: 13px; - width: 50px; - height: 30px; - line-height: 30px; - text-align: center; - font-weight: bold; - padding: 0px; - } - - /*2级标题*/ - .section-divider { - margin: 0 0 8px 0; - padding: 8px 8px; - background: var(--bg-color, #fff); - font-weight: 500; - color: var(--fg-color, #000); - border-top: 1px solid rgb(150,150,150,0.5); - border-bottom: 1px solid rgb(150,150,150,0.5); - margin: 0 16px 0 16px; - - } - - /*2级标题字体*/ - .section-title { - display: flex; - align-items: center; - justify-content: space-between; - color: var(--fg-color, #000); - font-size: 13px; - } - - /*2级标题,统计数量字体*/ - .section-count { - background: rgb(255,0,0,0.5); - color: var(--fg-color, #000); - border-radius: 12px; - width: 15px; - height: 15px; - text-align: center; - line-height: 15px; - padding: 3px; - font-size: 12px; - font-weight: bold; - } - - /*设备、实体明细*/ - .device-item { - display: flex; - align-items: center; - padding: 0px; - border-bottom: 1px solid rgb(150,150,150,0.2); - margin: 0 32px 4px 32px; - padding: 4px 0 0 0; - } - - /*设备、实体明细背景*/ - .devices-list { - flex: 1; - overflow-y: auto; - min-height: 0; - padding: 4px 0; - } - - .device-icon { - margin-right: 12px; - color: var(--error-color); - } - - .device-info { - flex-grow: 1; - } - - .device-name { - font-weight: 500; - color: var(--fg-color, #000); - margin: 2px 0; - } - - .device-entity { - font-size: 10px; - color: var(--fg-color, #000); - font-family: monospace; - } - - .device-details { - font-size: 10px; - color: var(--fg-color, #000); - } - - .device-last-seen-update { - font-size: 10px; - color: var(--fg-color, #000); - padding: 5px; - background: rgb(255, 0, 0, 0.1); - border: 1px solid rgb(255, 0, 0, 0.3); - border-radius: 4px; - cursor: pointer; - white-space: nowrap; - transition: background-color 0.2s; - } - - .device-last-seen-update:hover { - background: rgb(255, 0, 0, 0.2); - } - - .device-last-seen { - font-size: 10px; - color: var(--fg-color, #000); - margin-left: auto; - } - - .no-devices { - text-align: center; - padding: 8px 0; - color: var(--fg-color, #000); - } - - .loading { - text-align: center; - padding: 10px 0px; - color: var(--fg-color, #000); - } - - /* HA版本信息样式 */ - .ha-version-info { - padding: 4px 0 4px 16px; - margin: 0 16px 0 30px; - display: grid; - grid-template-columns: auto auto auto; - gap: 4px; - align-items: center; - } - - .version-label { - font-size: 10px; - color: var(--fg-color, #000); - text-align: left; - } - - .current-version { - color: var(--fg-color, #000); - font-size: 10px; - text-align: left; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .current-version.outdated { - color: rgb(255,20,0); - } - - .latest-version { - color: var(--fg-color, #000); - font-size: 10px; - text-align: left; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - - - .warning-message { - color: #ff6b6b; - font-size: 10px; - font-style: italic; - } - - /* 备份信息样式 */ - .backup-label { - font-size: 10px; - color: var(--fg-color, #000); - text-align: left; - } - - .backup-time, .backup-relative { - color: var(--fg-color, #000); - font-size: 10px; - text-align: left; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .backup-separator { - grid-column: 1 / -1; - height: 1px; - background: rgb(150,150,150,0.2); - margin: 0px 0px; - } - - /* 备份信息独立容器 */ - .backup-info { - padding: 4px 0 4px 16px; - margin: 0 32px 8px 32px; - display: grid; - grid-template-columns: auto auto auto; - gap: 4px; - align-items: center; - border-bottom: 1px solid rgb(150,150,150,0.2); - } - `; - } - - constructor() { - super(); - this._haUpdates = []; - this._otherUpdates = []; - this._offlineDevices = []; - this._offlineEntities = []; - this._loading = false; - this._dataLoaded = false; //button新元素 - this._refreshInterval = null; - this.theme = 'on'; - } - - static getConfigElement() { - return document.createElement("xiaoshi-ha-info-button-editor"); - } - - connectedCallback() { - super.connectedCallback(); - this._loadUpdateData(); - this._loadOfflineDevices(); - - // 设置主题属性 - this.setAttribute('theme', this._evaluateTheme()); - - //button新元素 开始 - setTimeout(() => { - this._loadUpdateData(); - this._loadOfflineDevices(); - }, 50); - //button新元素 结束 - - // 每300秒刷新一次数据,减少频繁刷新 - this._refreshInterval = setInterval(() => { - this._loadUpdateData(); - this._loadOfflineDevices(); - }, 300000); - } - - _evaluateTheme() { - try { - if (!this.config || !this.config.theme) return 'on'; - if (typeof this.config.theme === 'function') { - return this.config.theme(); - } - if (typeof this.config.theme === 'string' && - (this.config.theme.includes('return') || this.config.theme.includes('=>'))) { - return (new Function(`return ${this.config.theme}`))(); - } - return this.config.theme; - } catch(e) { - console.error('计算主题时出错:', e); - return 'on'; - } - } - - disconnectedCallback() { - super.disconnectedCallback(); - if (this._refreshInterval) { - clearInterval(this._refreshInterval); - } - } - - async _loadOfflineDevices() { - if (!this.hass) return; - - // button新元素 开始 删除下面 - // this._loading = true; - // this.requestUpdate(); - // button新元素 介素 删除下面 - - try { - // 并行获取所有需要的数据 - const [devices, allEntityRegs] = await Promise.all([ - this.hass.callWS({ - type: 'config/device_registry/list' - }), - this.hass.callWS({ - type: 'config/entity_registry/list' - }) - ]); - - // 获取当前实体状态 - const entities = Object.values(this.hass.states); - const entityMap = {}; - entities.forEach(entity => { - entityMap[entity.entity_id] = entity; - }); - - // 按设备ID分组实体 - const entitiesByDevice = {}; - allEntityRegs.forEach(entity => { - if (entity.device_id) { - if (!entitiesByDevice[entity.device_id]) { - entitiesByDevice[entity.device_id] = []; - } - entitiesByDevice[entity.device_id].push(entity); - } - }); - - const offlineDevices = []; - - // 获取设备排除模式 - const excludeDevicePatterns = this.config.exclude_devices || []; - - // 记录被排除的设备ID集合 - const excludedDeviceIds = new Set(); - - // 并行检查所有设备 - const deviceChecks = devices.map(device => { - const deviceEntities = entitiesByDevice[device.id] || []; - return { - device, - deviceEntities, - isOffline: this._checkDeviceAvailabilitySync(device, deviceEntities, entityMap) - }; - }); - - // 过滤离线设备并构建数据 - deviceChecks.forEach(({ device, deviceEntities, isOffline }) => { - if (isOffline) { - const deviceName = device.name_by_user || device.name || `设备 ${device.id.slice(0, 8)}`; - - // 检查设备名称是否匹配排除模式 - if (this._matchesExcludePattern(deviceName, excludeDevicePatterns)) { - // 记录被排除的设备ID,以便后续排除其下属实体 - excludedDeviceIds.add(device.id); - return; // 跳过匹配排除模式的设备 - } - - // 再次确保设备有有效实体 - const validEntities = deviceEntities.filter(entityReg => { - const entity = entityMap[entityReg.entity_id]; - return entity && !entityReg.disabled_by; - }); - - // 只有当设备有有效实体时才添加到离线设备列表 - if (validEntities.length > 0) { - offlineDevices.push({ - device_id: device.id, - name: deviceName, - model: device.model, - manufacturer: device.manufacturer, - area_id: device.area_id, - entities: validEntities, // 使用有效实体而不是所有实体 - last_seen: this._getDeviceLastSeen(validEntities, entityMap), - icon: this._getDeviceIcon(device, validEntities) - }); - } - } - }); - - // 按最后看到时间排序 - offlineDevices.sort((a, b) => - new Date(b.last_seen || 0) - new Date(a.last_seen || 0) - ); - - // 获取离线设备的所有实体ID - const offlineDeviceEntityIds = new Set(); - offlineDevices.forEach(device => { - device.entities.forEach(entity => { - offlineDeviceEntityIds.add(entity.entity_id); - }); - }); - - // 获取排除模式 - const excludePatterns = this.config.exclude_entities || []; - - // 获取独立的离线实体(不属于离线设备的实体) - const offlineEntities = []; - allEntityRegs.forEach(entityReg => { - if (entityReg.disabled_by) return; // 跳过被禁用的实体 - - const entity = entityMap[entityReg.entity_id]; - if (!entity) return; - - // 检查是否匹配排除模式 - if (this._matchesExcludePattern(entityReg.entity_id, excludePatterns)) { - return; // 跳过匹配排除模式的实体 - } - - // 检查实体是否属于被排除的设备 - if (entityReg.device_id && excludedDeviceIds.has(entityReg.device_id)) { - return; // 跳过属于被排除设备的实体 - } - - // 检查实体是否离线 - const isEntityOffline = entity.state === 'unavailable' ; - - // 只处理离线且不属于离线设备的实体 - if (isEntityOffline && !offlineDeviceEntityIds.has(entityReg.entity_id)) { - offlineEntities.push({ - entity_id: entityReg.entity_id, - friendly_name: entity.attributes.friendly_name || entityReg.entity_id, - state: entity.state, - last_changed: entity.last_changed, - last_updated: entity.last_updated, - icon: entity.attributes.icon || this._getDefaultIcon(entityReg.entity_id), - device_class: entity.attributes.device_class, - unit_of_measurement: entity.attributes.unit_of_measurement, - device_id: entityReg.device_id, - platform: entityReg.platform - }); - } - }); - - // 按最后更新时间排序 - offlineEntities.sort((a, b) => - new Date(b.last_updated) - new Date(a.last_updated) - ); - - this._offlineDevices = offlineDevices; - this._offlineEntities = offlineEntities; - this._dataLoaded = true; //button新元素 - } catch (error) { - console.error('加载离线设备失败:', error); - this._offlineDevices = []; - this._dataLoaded = true; //button新元素 - } - - this._loading = false; - } - - _checkDeviceAvailabilitySync(device, deviceEntities, entityMap) { - if (!deviceEntities || deviceEntities.length === 0) { - return false; // 没有实体的设备不视为离线,直接排除 - } - - // 检查设备的可用性状态 - if (device.disabled_by) { - return false; // 被禁用的设备不算离线 - } - - // 过滤出有效的实体(未被禁用且在entityMap中存在) - const validEntities = deviceEntities.filter(entityReg => { - const entity = entityMap[entityReg.entity_id]; - return entity && !entityReg.disabled_by; - }); - - // 如果没有有效实体,则不视为离线设备,直接排除 - if (validEntities.length === 0) { - return false; - } - - let hasAvailableEntity = false; - let hasUnavailableEntity = false; - - for (const entityReg of validEntities) { - const entity = entityMap[entityReg.entity_id]; - - if (entity.state !== 'unavailable' ) { - hasAvailableEntity = true; - break; // 找到一个可用实体就可以停止检查 - } else { - hasUnavailableEntity = true; - } - } - - // 如果设备有有效实体但所有实体都不可用,则设备离线 - return hasUnavailableEntity && !hasAvailableEntity; - } - - _getDeviceLastSeen(deviceEntities, entityMap) { - let lastSeen = null; - - for (const entityReg of deviceEntities) { - const entity = entityMap[entityReg.entity_id]; - if (!entity) continue; - - const entityTime = new Date(entity.last_updated); - if (!lastSeen || entityTime > lastSeen) { - lastSeen = entityTime; - } - } - - return lastSeen; - } - - _getDeviceIcon(device, deviceEntities) { - // 优先使用设备图标 - if (device.icon) { - return device.icon; - } - - // 根据设备类型推断图标 - if (device.model) { - const model = device.model.toLowerCase(); - if (model.includes('light') || model.includes('bulb')) return 'mdi:lightbulb'; - if (model.includes('switch') || model.includes('plug')) return 'mdi:power'; - if (model.includes('sensor')) return 'mdi:eye'; - if (model.includes('camera')) return 'mdi:camera'; - if (model.includes('fan')) return 'mdi:fan'; - if (model.includes('tv')) return 'mdi:multimedia'; - if (model.includes('button')) return 'mdi:button-pointer'; - if (model.includes('thermostat') || model.includes('climate')) return 'mdi:thermostat'; - } - - // 根据制造商推断图标 - if (device.manufacturer) { - const manufacturer = device.manufacturer.toLowerCase(); - if (manufacturer.includes('xiaomi') || manufacturer.includes('aqara')) return 'mdi:home-automation'; - if (manufacturer.includes('philips')) return 'mdi:lightbulb'; - if (manufacturer.includes('tp-link')) return 'mdi:network'; - } - - // 根据第一个实体的类型推断图标 - if (deviceEntities && deviceEntities.length > 0) { - const firstEntityId = deviceEntities[0].entity_id; - return this._getDefaultIcon(firstEntityId); - } - - return 'mdi:device-hub'; - } - - _getDefaultIcon(entityId) { - if (entityId.startsWith('light.')) return 'mdi:lightbulb'; - if (entityId.startsWith('switch.')) return 'mdi:power'; - if (entityId.startsWith('sensor.')) return 'mdi:eye'; - if (entityId.startsWith('binary_sensor.')) return 'mdi:eye'; - if (entityId.startsWith('device_tracker.')) return 'mdi:cellphone'; - if (entityId.startsWith('media_player.')) return 'mdi:speaker'; - if (entityId.startsWith('climate.')) return 'mdi:thermostat'; - if (entityId.startsWith('cover.')) return 'mdi:window-shutter'; - if (entityId.startsWith('weather.')) return 'mdi:weather-cloudy'; - if (entityId.startsWith('input_select.')) return 'mdi:form-select'; - if (entityId.startsWith('select.')) return 'mdi:form-select'; - if (entityId.startsWith('input_text.')) return 'mdi:form-textbox'; - if (entityId.startsWith('text.')) return 'mdi:form-textbox'; - if (entityId.startsWith('button.')) return 'mdi:button-pointer'; - if (entityId.startsWith('event.')) return 'mdi:gesture-tap-button'; - if (entityId.startsWith('device_tracker.')) return 'mdi:lan-connect'; - if (entityId.startsWith('notify.')) return 'mdi:message'; - return 'mdi:help-circle'; - } - - _formatLastSeen(timestamp) { - if (!timestamp) return '未知'; - - const date = new Date(timestamp); - const now = new Date(); - const diffMs = now - date; - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffMins < 1) return '刚刚'; - if (diffMins < 60) return `${diffMins}分钟前`; - if (diffHours < 24) return `${diffHours}小时前`; - if (diffDays < 7) return `${diffDays}天前`; - - return date.toLocaleDateString('zh-CN'); - } - - _handleRefresh() { - this._handleClick(); - this._loadOfflineDevices(); - } - - _handleDeviceClick(device) { - // 点击设备时跳转到设备详情页 - this._handleClick(); - if (device.device_id) { - // 先关闭当前弹窗/界面 - this._closeCurrentDialog(); - - // 延迟执行导航,确保当前界面已关闭 - setTimeout(() => { - const deviceUrl = `/config/devices/device/${device.device_id}`; - - // 尝试在Home Assistant环境中导航 - try { - // 在Home Assistant环境中导航 - window.history.pushState(null, '', deviceUrl); - window.dispatchEvent(new CustomEvent('location-changed')); - } catch (e) { - // 如果导航失败,在新标签页中打开,确保在最上层 - window.open(deviceUrl, '_blank', 'noopener,noreferrer'); - } - }, 300); // 短暂延迟确保当前界面已关闭 - } - } - - _handleEntityClick(entity) { - this._handleClick(); - // 点击实体时打开实体详情页 - if (entity.entity_id) { - // 使用您建议的第一种方式 - const evt = new Event('hass-more-info', { composed: true }); - evt.detail = { entityId: entity.entity_id }; - this.dispatchEvent(evt); - } - } - - _handleClick(){ - if (navigator.vibrate) { - navigator.vibrate(50); - } - else if (navigator.webkitVibrate) { - navigator.webkitVibrate(50); - } - else { - } - } - - _closeCurrentDialog() { - // 查找并关闭当前可能的弹窗或对话框 - const dialogs = document.querySelectorAll('ha-dialog, .mdc-dialog, paper-dialog, vaadin-dialog'); - dialogs.forEach(dialog => { - if (dialog && dialog.open) { - dialog.close(); - } - }); - - // 尝试关闭更多UI的弹窗 - const moreUIs = document.querySelectorAll('ha-more-info-dialog, .ha-more-info-dialog'); - moreUIs.forEach(ui => { - if (ui && ui.close) { - ui.close(); - } - }); - - // 如果是在卡片详情页面,尝试返回上一页 - if (window.history.length > 1) { - window.history.back(); - } - } - - _matchesExcludePattern(entityId, patterns) { - if (!patterns || patterns.length === 0) { - return false; - } - - for (const pattern of patterns) { - if (this._matchPattern(entityId, pattern)) { - return true; - } - } - return false; - } - - _matchPattern(str, pattern) { - // 将通配符模式转换为正则表达式 - const regexPattern = pattern - .replace(/\./g, '\\.') // 转义点号 - .replace(/\*/g, '.*'); // 将 * 转换为 .* - - const regex = new RegExp(`^${regexPattern}$`, 'i'); // 不区分大小写 - return regex.test(str); - } - async _loadUpdateData() { - if (!this.hass) return; - - this._loading = true; - this.requestUpdate(); - - try { - const haUpdates = []; - const otherUpdates = []; - - // 获取update.开头的实体更新信息 - try { - const entities = Object.values(this.hass.states); - const skipUpdates = this.config.skip_updates !== false; // 默认为true - - entities.forEach(entity => { - // 筛选以update.开头的实体 - if (entity.entity_id.startsWith('update.') && entity.state !== 'unavailable') { - const attributes = entity.attributes; - - // 检查是否有更新可用 - if (attributes.in_progress === false && - attributes.latest_version && - attributes.installed_version && - attributes.latest_version !== attributes.installed_version) { - - // 如果不跳过更新,检查skipped_version属性 - if (!skipUpdates) { - const skippedVersion = attributes.skipped_version; - // 如果skipped_version不为null且等于latest_version,则跳过此更新 - if (skippedVersion !== null && skippedVersion === attributes.latest_version) { - return; // 跳过此更新 - } - } - - // 新增规则:如果skipped_version为null情况下,当latest_version !== installed_version时, - // 且实体状态为off时,有可能是安装的版本比latest_version还高,这种不算更新的实体 - if (attributes.skipped_version === null && entity.state === 'off') { - return; // 跳过此更新 - } - - const updateData = { - name: attributes.friendly_name || entity.entity_id.replace('update.', ''), - current_version: attributes.installed_version, - latest_version: attributes.latest_version, - update_type: 'entity_update', - icon: attributes.icon || 'mdi:update', - entity_id: entity.entity_id, - title: attributes.title || '', - release_url: attributes.release_url || '', - entity_picture: attributes.entity_picture || '', - skipped_version: attributes.skipped_version || null - }; - - // 检查是否为home_assistant开头的实体 - if (entity.entity_id.includes('home_assistant') || - entity.entity_id.includes('hacs')) { - haUpdates.push(updateData); - } else { - otherUpdates.push(updateData); - } - } - } - }); - } catch (error) { - console.warn('获取update实体更新信息失败:', error); - } - - this._haUpdates = haUpdates; - this._otherUpdates = otherUpdates; - } catch (error) { - console.error('加载更新信息失败:', error); - this._haUpdates = []; - this._otherUpdates = []; - } - - this._loading = false; - } - - _handleRefresh() { - this._handleClick(); - this._loadUpdateData(); - this._loadOfflineDevices(); - } - - _handleUpdateClick(update) { - // 点击更新项时弹出实体详情 - this._handleClick(); - - // 如果有entity_id,弹出实体详情 - if (update.entity_id) { - this.dispatchEvent(new CustomEvent('hass-more-info', { - detail: { entityId: update.entity_id }, - bubbles: true, - composed: true - })); - } else { - // 对于没有entity_id的更新项,可以显示一个提示信息 - // 可选:显示一个简单的提示 - if (update.update_type === 'version') { - alert(`${update.name}\n当前版本: ${update.current_version}\n最新版本: ${update.latest_version}\n\n请点击右侧的"立即更新"按钮进行更新`); - } - } - } - - _handleConfirmUpdate(update, event) { - event.stopPropagation(); // 阻止事件冒泡 - event.preventDefault(); // 阻止默认行为 - this._handleClick(); - - // 弹出确认对话框 - const confirmed = confirm(`确认要更新 ${update.name} 吗?\n当前版本: ${update.current_version}\n最新版本: ${update.latest_version}`); - - if (confirmed) { - this._executeUpdate(update); - // 延迟3秒后刷新数据,给更新操作足够时间完成 - setTimeout(() => { - this._loadUpdateData(); - }, 1000); - } - - } - - _executeUpdate(update) { - // 根据更新类型执行不同的更新逻辑 - if (update.update_type === 'version') { - if (update.name.includes('Core')) { - this._callUpdateService('homeassistant', 'core', 'update'); - } else if (update.name.includes('Supervisor')) { - this._callUpdateService('hassio', 'supervisor', 'update'); - } else if (update.name.includes('OS')) { - this._callUpdateService('hassio', 'os', 'update'); - } - } else if (update.update_type.startsWith('hacs')) { - // HACS更新逻辑 - // 可以通过调用HACS服务来更新 - this.hass.callService('hacs', 'download', { - repository: update.name.replace('HACS - ', '') - }); - } else if (update.update_type === 'integration') { - // 集成更新逻辑 - } else if (update.update_type === 'entity_update') { - // 实体更新逻辑 - this.hass.callService('update', 'install', { - entity_id: update.entity_id - }); - } - } - - _callUpdateService(domain, service, action) { - try { - this.hass.callService(domain, service, { - [action]: true - }); - } catch (error) { - console.error(`调用更新服务失败: ${domain}.${service}`, error); - } - } - - _isNewerVersion(latest, current) { - if (!latest || !current) return false; - - const latestParts = latest.split('.').map(Number); - const currentParts = current.split('.').map(Number); - - for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) { - const latestPart = latestParts[i] || 0; - const currentPart = currentParts[i] || 0; - - if (latestPart > currentPart) return true; - if (latestPart < currentPart) return false; - } - - return false; - } - - _getHacsIcon(category) { - const iconMap = { - 'integration': 'mdi:puzzle', - 'plugin': 'mdi:card-multiple', - 'theme': 'mdi:palette', - 'python_script': 'mdi:language-python', - 'netdaemon': 'mdi:code-braces', - 'appdaemon': 'mdi:application' - }; - - return iconMap[category] || 'mdi:download'; - } - - _formatDateTime(dateString) { - if (!dateString || dateString === 'unknown' || dateString === '未知') { - return '无'; - } - - try { - // 解析ISO时间字符串,Date对象会自动处理时区转换 - const date = new Date(dateString); - if (isNaN(date.getTime())) { - return '无'; - } - - // 使用toLocaleString直接格式化为东八区时间 - return date.toLocaleString('zh-CN', { - timeZone: 'Asia/Shanghai', - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - hour12: false - }).replace(/\//g, '-'); - } catch (error) { - console.warn('时间格式化失败:', dateString, error); - return '无'; - } - } - - _getRelativeTime(dateString, isFuture = false) { - if (!dateString || dateString === 'unknown' || dateString === '未知') { - return '无'; - } - - try { - const date = new Date(dateString); - if (isNaN(date.getTime())) { - return '无'; - } - - const now = new Date(); - const diffMs = isFuture ? date.getTime() - now.getTime() : now.getTime() - date.getTime(); - const diffHours = Math.abs(Math.floor(diffMs / (1000 * 60 * 60))); - - if (diffHours >= 24) { - const diffDays = Math.floor(diffHours / 24); - return `${diffDays}天${isFuture ? '后' : '前'}`; - } else { - return `${diffHours}小时${isFuture ? '后' : '前'}`; - } - } catch (error) { - console.warn('相对时间计算失败:', dateString, error); - return '无'; - } - } - - _renderHAVersionInfo() { - if (!this.hass) return html``; - - const versionElements = []; - - // OS版本信息 - 只有当update.home_assistant_operating_system_update存在时才显示 - const osEntity = this.hass.states['update.home_assistant_operating_system_update']; - if (osEntity) { - const current = osEntity.attributes.installed_version || '未知'; - const latest = osEntity.attributes.latest_version || '未知'; - const osCurrentVersionClass = (current !== '未知' && latest !== '未知' && current !== latest) ? 'outdated' : ''; - versionElements.push(html` -
OS
-
当前版本:${current}
-
最新版本:${latest}
- `); - } - - // Core版本信息 - const coreEntity = this.hass.states['update.home_assistant_core_update']; - if (coreEntity) { - const current = coreEntity.attributes.installed_version || '未知'; - const latest = coreEntity.attributes.latest_version || '未知'; - const coreCurrentVersionClass = (current !== '未知' && latest !== '未知' && current !== latest) ? 'outdated' : ''; - versionElements.push(html` -
Core
-
当前版本:${current}
-
最新版本:${latest}
- `); - } - - // Supervisor版本信息 - const supervisorEntity = this.hass.states['update.home_assistant_supervisor_update']; - if (supervisorEntity) { - const current = supervisorEntity.attributes.installed_version || '未知'; - const latest = supervisorEntity.attributes.latest_version || '未知'; - const supervisorCurrentVersionClass = (current !== '未知' && latest !== '未知' && current !== latest) ? 'outdated' : ''; - versionElements.push(html` -
Supervisor
-
当前版本:${current}
-
最新版本:${latest}
- `); - } - - return html`${versionElements}`; - } - - _renderBackupInfo() { - if (!this.hass) return html``; - - const backupElements = []; - - // 上次备份信息 - const lastBackupEntity = this.hass.states['sensor.backup_last_successful_automatic_backup']; - if (lastBackupEntity) { - const lastBackupTime = this._formatDateTime(lastBackupEntity.state); - const lastBackupRelative = this._getRelativeTime(lastBackupEntity.state, false); - const lastBackupCombined = lastBackupRelative !== '无' ? `${lastBackupTime}(${lastBackupRelative})` : lastBackupTime; - - backupElements.push(html` -
HA上次备份
-
${lastBackupCombined}
- `); - } - - // 下次备份信息 - const nextBackupEntity = this.hass.states['sensor.backup_next_scheduled_automatic_backup']; - if (nextBackupEntity) { - const nextBackupTime = this._formatDateTime(nextBackupEntity.state); - const nextBackupRelative = this._getRelativeTime(nextBackupEntity.state, true); - const nextBackupCombined = nextBackupRelative !== '无' ? `${nextBackupTime}(${nextBackupRelative})` : nextBackupTime; - - backupElements.push(html` -
HA下次备份
-
${nextBackupCombined}
- `); - } - - return html`${backupElements}`; - } - - - - /*button新元素 开始*/ - _handleButtonClick() { - const tapAction = this.config.tap_action; - - if (!tapAction || tapAction !== 'none') { - // 默认 tap_action 行为:弹出垂直堆叠卡片 - const excludedParams = ['type', 'button_height', 'button_width', 'button_font_size', 'button_icon_size', 'show_preview', 'tap_action']; - - // 构建垂直堆叠卡片的内容 - const cards = []; - - // 1. 添加HA卡片 - const hainfoCardConfig = {}; - Object.keys(this.config).forEach(key => { - if (!excludedParams.includes(key) && key !== 'other_cards' && key !== 'no_preview') { - hainfoCardConfig[key] = this.config[key]; - } - }); - - cards.push({ - type: 'custom:xiaoshi-ha-info-card', - ...hainfoCardConfig - }); - - // 2. 添加附加卡片 - if (this.config.other_cards && this.config.other_cards.trim()) { - try { - const additionalCardsConfig = this._parseYamlCards(this.config.other_cards); - - // 为每个附加卡片传递 theme 值 - const cardsWithTheme = additionalCardsConfig.map(card => { - // 如果卡片没有 theme 配置,则从当前卡片配置中传递 - if (!card.theme && this.config.theme) { - return { - ...card, - theme: this.config.theme - }; - } - return card; - }); - - cards.push(...cardsWithTheme); - } catch (error) { - console.error('解析附加卡片配置失败:', error); - } - } - - // 创建垂直堆叠卡片 - const popupContent = { - type: 'vertical-stack', - cards: cards - }; - - const popupStyle = this.config.popup_style || ` - --mdc-theme-surface: rgb(0,0,0,0); - --dialog-backdrop-filter: blur(10px) brightness(1); - `; - - if (window.browser_mod) { - window.browser_mod.service('popup', { - style: popupStyle, - content: popupContent - }); - } else { - console.warn('browser_mod not available, cannot show popup'); - } - } - this._handleClick(); - } - - _parseYamlCards(yamlString) { - try { - const lines = yamlString.split('\n'); - const cards = []; - let currentCard = null; - let indentStack = []; - let contextStack = []; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const trimmed = line.trim(); - - if (!trimmed || trimmed.startsWith('#')) continue; - - const indentLevel = line.length - line.trimStart().length; - if (trimmed.startsWith('- type')) { - if (currentCard) { - cards.push(currentCard); - currentCard = null; - indentStack = []; - contextStack = []; - } - const content = trimmed.substring(1).trim(); - if (content.includes(':')) { - const [key, ...valueParts] = content.split(':'); - const value = valueParts.join(':').trim(); - currentCard = {}; - this._setNestedValue(currentCard, key.trim(), this._parseValue(value)); - } else { - currentCard = { type: content }; - } - - indentStack = [indentLevel]; - contextStack = [currentCard]; - } else if (currentCard && trimmed.startsWith('-')) { - while (indentStack.length > 1 && indentLevel <= indentStack[indentStack.length - 1]) { - indentStack.pop(); - contextStack.pop(); - } - - let currentContext = contextStack[contextStack.length - 1]; - const itemValue = trimmed.substring(1).trim(); - - if (!Array.isArray(currentContext)) { - if (contextStack.length > 1) { - const parentContext = contextStack[contextStack.length - 2]; - for (let key in parentContext) { - if (parentContext[key] === currentContext) { - parentContext[key] = []; - contextStack[contextStack.length - 1] = parentContext[key]; - currentContext = parentContext[key]; - break; - } - } - } - } - if (Array.isArray(currentContext)) { - if (itemValue.includes(':')) { - const [key, ...valueParts] = itemValue.split(':'); - const value = valueParts.join(':').trim(); - const obj = {}; - obj[key.trim()] = this._parseValue(value); - currentContext.push(obj); - } else { - currentContext.push(this._parseValue(itemValue)); - } - } - } else if (currentCard && trimmed.includes(':')) { - const [key, ...valueParts] = trimmed.split(':'); - const value = valueParts.join(':').trim(); - const keyName = key.trim(); - - while (indentStack.length > 1 && indentLevel <= indentStack[indentStack.length - 1]) { - indentStack.pop(); - contextStack.pop(); - } - - const currentContext = contextStack[contextStack.length - 1]; - - if (value) { - this._setNestedValue(currentContext, keyName, this._parseValue(value)); - } else { - let nextLine = null, nextIndent = null; - for (let j = i + 1; j < lines.length; j++) { - const nextTrimmed = lines[j].trim(); - if (nextTrimmed && !nextTrimmed.startsWith('#')) { - nextLine = nextTrimmed; - nextIndent = lines[j].length - lines[j].trimStart().length; - break; - } - } - - currentContext[keyName] = (nextLine && nextLine.startsWith('-') && nextIndent > indentLevel) - ? [] : (currentContext[keyName] || {}); - - indentStack.push(indentLevel); - contextStack.push(currentContext[keyName]); - } - } - } - - if (currentCard) cards.push(currentCard); - - return cards; - } catch (error) { - console.error('YAML解析错误:', error); - return []; - } - } - - _parseValue(value) { - if (!value) return ''; - - // 移除引号 - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { - return value.slice(1, -1); - } - - // 尝试解析为数字 - if (!isNaN(value) && value.trim() !== '') { - return Number(value); - } - - // 尝试解析为布尔值 - if (value === 'true') return true; - if (value === 'false') return false; - if (value === 'null') return null; - - // 返回字符串 - return value; - } - - _setNestedValue(obj, path, value) { - // 支持嵌套路径,如 "styles.card" - const keys = path.split('.'); - let current = obj; - - for (let i = 0; i < keys.length - 1; i++) { - const key = keys[i]; - if (!current[key] || typeof current[key] !== 'object') { - current[key] = {}; - } - current = current[key]; - } - - current[keys[keys.length - 1]] = value; - } - - /*button新元素 结束*/ - - render() { - if (!this.hass) { - return html`
等待Home Assistant连接...
`; - } - // 获取主题和颜色 - const theme = this._evaluateTheme(); - const fgColor = theme === 'on' ? 'rgb(0, 0, 0)' : 'rgb(255, 255, 255)'; - const bgColor = theme === 'on' ? 'rgb(255, 255, 255)' : 'rgb(50, 50, 50)'; - const warningCount =this._haUpdates.length + this._otherUpdates.length + this._offlineDevices.length + this._offlineEntities.length; - - /*button新元素 前9行和最后1行开始*/ - const showPreview = this.config.no_preview === true; - - // 获取新参数 - const badgeMode = this.config.badge_mode === true; - const transparentBg = this.config.transparent_bg === true; - const hideIcon = this.config.hide_icon === true; - const hideColon = this.config.hide_colon === true; - const hideZero = this.config.hide_zero === true; - const autoHide = this.config.auto_hide === true; - const buttonText = this.config.button_text || 'HA'; - const buttonIcon = this.config.button_icon || 'mdi:home-assistant'; - - // 设置背景颜色 - const buttonBgColor = transparentBg ? 'transparent' : bgColor; - - // 检查是否需要自动隐藏(只有数据加载完成且数量为0时才考虑隐藏) - const shouldAutoHide = this._dataLoaded && autoHide && warningCount === 0; - - // 如果需要自动隐藏,返回空div - if (shouldAutoHide) { - return html`
`; - } - - // 渲染按钮 - let buttonHtml; - if (!this._dataLoaded) { - if (badgeMode) { - // 角标模式:只显示图标,数量为0时不显示角标 - buttonHtml = html` -
- -
- `; - } else { - // 普通模式 - // 构建显示文本 - let displayText = buttonText; - - // 根据hide_colon参数决定是否显示冒号 - if (!hideColon) { - displayText += ':'; - } else { - displayText += ' '; - } - - // 根据hide_zero参数决定是否显示0值 - if (!hideZero) { - displayText += ' 0'; - } else { - // 隐藏0值时使用CSS空格占位符,保持布局稳定 - displayText += '\u2002'; // 两个en空格,大约等于数字"0"的宽度 - } - - buttonHtml = html` -
- ${!hideIcon ? html`` : ''} - ${displayText} -
- `; - } - } else { - // 数据加载完成后 - if (badgeMode) { - // 角标模式:只显示图标,根据数量显示角标 - const hasWarning = warningCount > 0; - buttonHtml = html` -
- - ${hasWarning ? html`
${warningCount}
` : ''} -
- `; - } else { - // 普通模式:显示文本和数量 - const textColor = warningCount === 0 ? fgColor : 'rgb(255, 0, 0)'; - - // 构建显示文本 - let displayText = buttonText; - - // 根据hide_colon参数决定是否显示冒号 - if (!hideColon) { - displayText += ':'; - } else { - displayText += ' '; - } - - // 根据hide_zero参数和实际数量决定是否显示数量 - if (hideZero && warningCount === 0) { - // 隐藏0值时使用CSS空格占位符,保持布局稳定 - displayText += '\u2002'; // 两个en空格,大约等于数字"0"的宽度 - } else { - displayText += ` ${warningCount}`; - } - - buttonHtml = html` -
- ${!hideIcon ? html`` : ''} - ${displayText} -
- `; - } - } - - return html` - ${buttonHtml} - ${showPreview ? html` -
- -
- - -
-
- - HA信息监控 -
-
- - ${warningCount} - - -
-
- - -
-
- • HA版本信息 -
-
-
- ${this._renderHAVersionInfo()} -
- -
- ${this._loading ? - html`
HA版本信息加载中...
` : - - (this._haUpdates.length === 0 && this._otherUpdates.length === 0) ? - html`
✅ 所有组件都是最新版本
` : - html` - ${this._haUpdates.length > 0 ? html` -
-
- • HA版本更新 - ${this._haUpdates.length} -
-
- ${this._haUpdates.map(update => html` -
this._handleEntityClick(update)}> -
- -
-
-
${update.name}
-
- 当前版本: ${update.current_version} → 最新版本: ${update.latest_version} - ${update.skipped_version ? html`
已跳过版本: ${update.skipped_version}` : ''} -
-
-
this._handleConfirmUpdate(update, e)}> - 立即更新 -
-
- `)}\n ` : ''} - - ${this._otherUpdates.length > 0 ? html` -
-
- • HACS更新 - ${this._otherUpdates.length} -
-
- ${this._otherUpdates.map(update => html` -
this._handleEntityClick(update)}> -
- -
-
-
${update.name}
-
- 当前版本: ${update.current_version} → 最新版本: ${update.latest_version} - ${update.skipped_version ? html`
已跳过版本: ${update.skipped_version}` : ''} -
-
-
this._handleConfirmUpdate(update, e)}> - 立即更新 -
-
- `)}\n ` : ''} - ` - } - - ${this._loading ? - html`
设备和实体加载中...
` : - - (this._offlineDevices.length === 0 && this._offlineEntities.length === 0) ? - html`
✅ 所有设备和实体都在线
` : - html` - ${this._offlineDevices.length > 0 ? html` -
-
- • 离线设备 - ${this._offlineDevices.length} -
-
- ${this._offlineDevices.map(device => html` -
this._handleDeviceClick(device)}> -
- -
-
-
${device.name}
-
- ${device.manufacturer && device.model ? - `${device.manufacturer} ${device.model}` : - device.manufacturer || device.model || '未知设备'} - ${device.entities ? `• ${device.entities.length} 个实体` : ''} -
-
-
- ${this._formatLastSeen(device.last_seen)} -
-
- `)}\n ` : ''} - - ${this._offlineEntities.length > 0 ? html` -
-
- • 离线实体 - ${this._offlineEntities.length} -
-
- ${this._offlineEntities.map(entity => html` -
this._handleEntityClick(entity)}> -
- -
-
-
${entity.friendly_name}
-
- ${entity.entity_id} - ${entity.platform ? `• ${entity.platform}` : ''} - ${entity.unit_of_measurement ? `• ${entity.unit_of_measurement}` : ''} -
-
-
- ${this._formatLastSeen(entity.last_updated)} -
-
- `)}\n ` : ''} - ` - } -
- - -
-
- • 备份信息 -
-
-
- ${this._renderBackupInfo()} -
- -
- ` : html``} - `; - /*button新元素 结束*/ - } - - setConfig(config) { - /*button新元素 开始*/ - // 不设置默认值,只有明确配置时才添加 no_preview - this.config = { - ...config - }; - if (config.button_width) { - this.style.setProperty('--button-width', config.button_width); - } else { - this.style.setProperty('--button-width', '70px'); - } - - // 设置按钮高度(只控制 ha-info-status) - if (config.button_height) { - this.style.setProperty('--button-height', config.button_height); - } else { - this.style.setProperty('--button-height', '24px'); - } - - // 设置按钮文字大小(只控制 ha-info-status) - if (config.button_font_size) { - this.style.setProperty('--button-font-size', config.button_font_size); - } else { - this.style.setProperty('--button-font-size', '11px'); - } - - // 设置按钮图标大小(只控制 ha-info-status) - if (config.button_icon_size) { - this.style.setProperty('--button-icon-size', config.button_icon_size); - } else { - this.style.setProperty('--button-icon-size', '13px'); - } - - // 设置卡片宽度(控制原来的 UI) - if (config.width) { - this.style.setProperty('--card-width', config.width); - } else { - this.style.setProperty('--card-width', '100%'); - } - /*button新元素 结束*/ - // 设置主题 - if (config.theme) { - this.setAttribute('theme', config.theme); - } - } - - getCardSize() { - // 根据更新项数量动态计算卡片大小 - const baseSize = 4; - const haSize = Math.max(0, Math.min(this._haUpdates.length, 6)); - const otherSize = Math.max(0, Math.min(this._otherUpdates.length, 8)); - const deviceSize = Math.max(0, Math.min(this._offlineDevices.length, 6)); - const entitySize = Math.max(0, Math.min(this._offlineEntities.length, 8)); - return baseSize + haSize + otherSize + deviceSize + entitySize; - } -} -customElements.define('xiaoshi-ha-info-button', XiaoshiHaInfoButton); - - - -