From 721b6a4ba293928d8917b28773268805262376da Mon Sep 17 00:00:00 2001 From: xiaoshi <115949669+xiaoshi930@users.noreply.github.com> Date: Sat, 29 Nov 2025 23:55:37 +0800 Subject: [PATCH] Delete xiaoshi-device-consumables-button2.js --- xiaoshi-device-consumables-button2.js | 2407 ------------------------- 1 file changed, 2407 deletions(-) delete mode 100644 xiaoshi-device-consumables-button2.js diff --git a/xiaoshi-device-consumables-button2.js b/xiaoshi-device-consumables-button2.js deleted file mode 100644 index 97091bd..0000000 --- a/xiaoshi-device-consumables-button2.js +++ /dev/null @@ -1,2407 +0,0 @@ -import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module"; - -class XiaoshiConsumablesButtonEditor extends LitElement { - static get properties() { - return { - hass: { type: Object }, - config: { type: Object }, - _searchTerm: { type: String }, - _filteredEntities: { type: Array }, - _showEntityList: { type: Boolean } - }; - } - - static get styles() { - return css` - .form { - display: flex; - flex-direction: column; - gap: 10px; - min-height: 500px; - } - .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; - } - - .entity-selector { - position: relative; - } - - .entity-search-input { - width: 100%; - padding: 8px; - border: 1px solid #ddd; - border-radius: 4px; - box-sizing: border-box; - } - - .entity-dropdown { - position: absolute; - top: 100%; - left: 0; - right: 0; - height: 300px; - overflow-y: auto; - background: white; - border: 1px solid #ddd; - border-radius: 4px; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - z-index: 1000; - margin-top: 2px; - } - - .entity-option { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - border-bottom: 1px solid #eee; - } - - .entity-option:hover { - background: #f5f5f5; - } - - .entity-option.selected { - background: #e3f2fd; - } - - .entity-info { - display: flex; - align-items: center; - gap: 8px; - flex: 1; - justify-content: space-between; - } - - .entity-details { - flex: 1; - } - - .entity-name { - font-weight: 500; - font-size: 14px; - color: #000; - } - - .entity-id { - font-size: 12px; - color: #000; - font-family: monospace; - } - - .check-icon { - color: #4CAF50; - } - - .no-results { - padding: 12px; - text-align: center; - color: #666; - font-style: italic; - } - - .selected-entities { - margin-top: 8px; - } - - .selected-label { - font-size: 12px; - font-weight: bold; - margin-bottom: 4px; - color: #333; - } - - .selected-entity-config { - margin-bottom: 8px; - border: 1px solid #ddd; - border-radius: 4px; - padding: 8px; - background: #f9f9f9; - } - - .selected-entity { - display: flex; - align-items: center; - gap: 4px; - margin-bottom: 8px; - font-size: 12px; - color: #000; - justify-content: space-between; - } - - .attribute-config { - margin-top: 4px; - display: flex; - flex-direction: column; - gap: 4px; - } - - .attribute-input { - width: 100%; - padding: 4px 8px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 12px; - box-sizing: border-box; - } - - .override-config { - display: flex; - align-items: center; - gap: 4px; - margin-top: 2px; - } - - .override-checkbox { - margin-right: 4px; - } - - .override-input { - flex: 1; - padding: 2px 6px; - border: 1px solid #ddd; - border-radius: 3px; - font-size: 11px; - box-sizing: border-box; - } - - .override-label { - font-size: 11px; - color: #666; - white-space: nowrap; - } - - .remove-btn { - background: none; - border: none; - cursor: pointer; - padding: 0; - display: flex; - align-items: center; - color: #666; - margin-left: auto; - } - - .remove-btn:hover { - color: #f44336; - } - - /*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` -
- - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - -
- - - - -
- -
- - -
- - - -
- - -
- -
- - -
- -
- - -
- 全局预警条件:当任一实体满足此条件时,该实体显示为红色预警状态
- 优先级:明细预警 > 全局预警 > 无预警
- 预警基于换算后的结果进行判断(如果配置了换算) -
-
- -
- - -
- -
- - -
- -
- -
- - ${this._showEntityList ? html` -
- ${this._filteredEntities.map(entity => html` -
this._toggleEntity(entity.entity_id)} - > -
-
-
${entity.attributes.friendly_name || entity.entity_id}
-
${entity.entity_id}
-
- -
- ${this.config.entities && this.config.entities.some(e => e.entity_id === entity.entity_id) ? - html`` : ''} -
- `)} - ${this._filteredEntities.length === 0 ? html` -
未找到匹配的实体
- ` : ''} -
- ` : ''} -
-
- ${this.config.entities && this.config.entities.length > 0 ? html` -
已选择的实体:
- ${this.config.entities.map((entityConfig, index) => { - const entity = this.hass.states[entityConfig.entity_id]; - return html` -
-
- ${entity?.attributes.friendly_name || entityConfig.entity_id} - - -
-
- this._updateEntityAttribute(index, e.target.value)} - .value=${entityConfig.attribute || ''} - placeholder="留空使用实体状态,或输入属性名" - class="attribute-input" - /> - -
- this._updateEntityOverride(index, 'icon', e.target.checked)} - .checked=${entityConfig.overrides?.icon !== undefined} - /> - 图标: - this._updateEntityOverrideValue(index, 'icon', e.target.value)} - .value=${entityConfig.overrides?.icon || ''} - placeholder="mdi:icon-name" - ?disabled=${entityConfig.overrides?.icon === undefined} - /> -
- -
- this._updateEntityOverride(index, 'name', e.target.checked)} - .checked=${entityConfig.overrides?.name !== undefined} - /> - 名称: - this._updateEntityOverrideValue(index, 'name', e.target.value)} - .value=${entityConfig.overrides?.name || ''} - placeholder="自定义名称" - ?disabled=${entityConfig.overrides?.name === undefined} - /> -
- -
- this._updateEntityOverride(index, 'unit_of_measurement', e.target.checked)} - .checked=${entityConfig.overrides?.unit_of_measurement !== undefined} - /> - 单位: - this._updateEntityOverrideValue(index, 'unit_of_measurement', e.target.value)} - .value=${entityConfig.overrides?.unit_of_measurement || ''} - placeholder="自定义单位" - ?disabled=${entityConfig.overrides?.unit_of_measurement === undefined} - /> -
- -
- this._updateEntityOverride(index, 'warning', e.target.checked)} - .checked=${entityConfig.overrides?.warning !== undefined} - /> - 预警: - this._updateEntityOverrideValue(index, 'warning', e.target.value)} - .value=${entityConfig.overrides?.warning || ''} - placeholder='>10, <=5, ==on,=="hello world"' - ?disabled=${entityConfig.overrides?.warning === undefined} - /> -
- -
- this._updateEntityOverride(index, 'conversion', e.target.checked)} - .checked=${entityConfig.overrides?.conversion !== undefined} - /> - 换算: - this._updateEntityOverrideValue(index, 'conversion', e.target.value)} - .value=${entityConfig.overrides?.conversion || ''} - placeholder="+10, -10, *1.5, /2" - ?disabled=${entityConfig.overrides?.conversion === undefined} - /> -
- -
- 预警:针对单个实体的预警条件,优先级高于全局预警
- 换算:对原始数值进行数学运算,支持 +10, -10, *1.5, /2 等格式
-
-
-
- `; - })} - ` : ''} -
-
- 搜索并选择要显示的设备耗材实体,支持多选。每个实体可以配置:
- • 特殊实体显示:binary_sensor(off→正常,on→缺少), event(unknown→正常,其他→低电量)
- • 属性名:留空使用实体状态,或输入属性名
- • 名称重定义:勾选后可自定义显示名称
- • 图标重定义:勾选后可自定义图标(如 mdi:phone)
- • 单位重定义:勾选后可自定义单位(如 元、$、kWh 等)
- • 预警条件:勾选后设置预警条件,支持 >10, >=10, <10, <=10, ==10, ==on, ==off, =="hello world" 等
- • 换算:对数值进行数学运算,支持 +10, -10, *1.5, /2 等
- • 未勾选重定义时,将使用实体的原始属性值 -
-
-
- - `; - } - - _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 || '16vw'; - } else if (name === 'button_height') { - finalValue = value || '2.8vh'; - } else if (name === 'button_font_size') { - finalValue = value || '1.25vh'; - } else if (name === 'button_icon_size') { - finalValue = value || '18px'; - } else if (name === 'width') { - finalValue = value || '100%'; - } else if (name === 'tap_action') { - // 处理tap_action YAML配置 - finalValue = value || ''; - // 只保存原始YAML,不保存解析后的对象到配置中 - // 解析后的对象将在setConfig中处理 - } - /*button新按钮方法 结束*/ - - this.config = { - ...this.config, - [name]: finalValue - }; - - this.dispatchEvent(new CustomEvent('config-changed', { - detail: { config: this.config }, - bubbles: true, - composed: true - })); - } - - // 简单的YAML卡片解析函数 - _parseYamlCards(yamlText) { - try { - // 这里是一个简化的解析器,实际使用中建议使用js-yaml库 - // 假设用户输入的是这样的格式: - // cards: - // - type: entities - // entities: - // - entity: sun.sun - - // 简单解析:提取cards数组 - const lines = yamlText.split('\n'); - const cards = []; - let currentCard = null; - let indentLevel = 0; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const trimmed = line.trim(); - - if (trimmed.startsWith('cards:')) { - continue; // 跳过cards行 - } - - if (trimmed.startsWith('- type:')) { - if (currentCard) { - cards.push(currentCard); - } - currentCard = { - type: trimmed.replace('- type:', '').trim() - }; - } else if (currentCard && trimmed && !trimmed.startsWith('#')) { - const match = line.match(/^(\s*)([^:]+):\s*(.*)$/); - if (match) { - const [, spaces, key, value] = match; - if (key.trim() !== 'type') { - if (!currentCard.properties) { - currentCard.properties = {}; - } - currentCard.properties[key.trim()] = value ? value.trim() : ''; - } - } - } - } - - if (currentCard) { - cards.push(currentCard); - } - - return { cards: cards }; - } catch (error) { - console.error('YAML解析错误:', error); - return null; - } - } - - // 解析tap_action YAML配置 - 支持简化的配置格式 - _parseTapActionYaml(yamlText) { - try { - - - // 检查是否是简化格式(只包含 type 和 cards,没有action字段) - const hasAction = yamlText.includes('action:'); - const hasTypeAndCards = yamlText.includes('type:') && yamlText.includes('cards:'); - - console.log('弹窗调试: hasAction =', hasAction, 'hasTypeAndCards =', hasTypeAndCards); - - if (!hasAction && hasTypeAndCards) { - console.log('弹窗调试: 检测到简化格式'); - - // 提取type值 - const typeMatch = yamlText.match(/type:\s*(.+)$/m); - const stackType = typeMatch ? typeMatch[1].trim() : 'vertical-stack'; - console.log('弹窗调试: 解析到的type =', stackType); - - // 解析cards部分 - 使用完整的YAML解析逻辑 - const cardsMatch = yamlText.match(/cards:\s*\n((?:\s*-.+\n?)*)/); - let cards = []; - - if (cardsMatch) { - const cardsText = cardsMatch[1]; - const cardLines = cardsText.split('\n').filter(line => line.trim()); - let currentCard = null; - let cardIndent = 0; - - for (let i = 0; i < cardLines.length; i++) { - const line = cardLines[i]; - const trimmed = line.trim(); - const indent = line.match(/^(\s*)/)[1].length; - - if (trimmed.startsWith('- type:')) { - // 保存上一个卡片(如果有) - if (currentCard) { - cards.push(currentCard); - } - - // 开始新卡片 - const cardType = trimmed.replace('- type:', '').trim(); - currentCard = { type: cardType }; - cardIndent = indent; - - // 如果是耗材卡片,自动添加配置参数 - if (cardType === 'custom:xiaoshi-consumables-card') { - const excludedParams = ['type', 'button_height', 'button_width', 'button_font_size', 'button_icon_size', 'show_preview', 'tap_action']; - Object.keys(this.config).forEach(key => { - if (!excludedParams.includes(key)) { - currentCard[key] = this.config[key]; - } - }); - console.log('弹窗调试: 为耗材卡片添加参数:', currentCard); - } - } else if (currentCard && indent > cardIndent && trimmed && !trimmed.startsWith('#')) { - // 解析卡片属性 - const match = trimmed.match(/^([^:]+):\s*(.*)$/); - if (match) { - const [, key, value] = match; - // 只排除 type 字段,其他所有字段都传递 - if (key.trim() !== 'type') { - // 处理特殊值类型 - let parsedValue = value ? value.trim() : ''; - - // 处理布尔值 - if (parsedValue === 'true') parsedValue = true; - else if (parsedValue === 'false') parsedValue = false; - // 处理数字 - else if (!isNaN(parsedValue) && parsedValue !== '') parsedValue = Number(parsedValue); - - currentCard[key.trim()] = parsedValue; - } - } - } - } - - // 保存最后一个卡片 - if (currentCard) { - cards.push(currentCard); - } - } - - // 如果没有耗材卡片,自动添加一个 - const hasConsumablesCard = cards.some(card => card.type === 'custom:xiaoshi-consumables-card'); - if (!hasConsumablesCard) { - const excludedParams = ['type', 'button_height', 'button_width', 'button_font_size', 'button_icon_size', 'show_preview', 'tap_action']; - const consumablesCard = { - type: 'custom:xiaoshi-consumables-card' - }; - - Object.keys(this.config).forEach(key => { - if (!excludedParams.includes(key)) { - consumablesCard[key] = this.config[key]; - } - }); - - cards.unshift(consumablesCard); - console.log('弹窗调试: 自动添加耗材卡片:', consumablesCard); - } - - // 构建完整样式,包括用户配置和自动添加的宽度 - const fullStyle = this._buildFullPopupStyle(); - - const result = { - action: 'fire-dom-event', - browser_mod: { - service: 'browser_mod.popup', - data: { - style: fullStyle, - content: { - type: stackType, - cards: cards - } - } - } - }; - - console.log('弹窗调试: 简化格式解析结果:', JSON.stringify(result, null, 2)); - return result; - } - - // 原有的完整格式解析逻辑 - const lines = yamlText.split('\n'); - const config = {}; - const stack = [config]; - const pathStack = []; - let multilineValue = null; - let multilineKey = null; - let multilineIndent = 0; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const trimmed = line.trim(); - - - - if (!trimmed || trimmed.startsWith('#')) continue; - - // 处理多行字符串 - if (multilineValue !== null) { - const currentIndent = line.match(/^(\s*)/)[1].length; - if (currentIndent > multilineIndent) { - multilineValue.push(line); - continue; - } else { - // 多行字符串结束 - const current = stack[stack.length - 1]; - current[multilineKey] = multilineValue.join('\n'); - multilineValue = null; - multilineKey = null; - } - } - - // 检查是否是多行字符串开始 - if (trimmed === '|') { - // 获取上一行的键 - for (let j = i - 1; j >= 0; j--) { - const prevLine = lines[j]; - const prevTrimmed = prevLine.trim(); - if (prevTrimmed && prevTrimmed.includes(':')) { - const colonIndex = prevTrimmed.indexOf(':'); - multilineKey = prevTrimmed.substring(0, colonIndex).trim(); - break; - } - } - if (multilineKey) { - multilineValue = []; - multilineIndent = line.match(/^(\s*)/)[1].length; - - continue; - } - } - - // 计算缩进级别 - const indent = line.match(/^(\s*)/)[1].length; - const level = Math.floor(indent / 2); - - // 调整栈到正确的层级 - while (stack.length > level + 1) { - stack.pop(); - pathStack.pop(); - } - - const current = stack[stack.length - 1]; - - if (trimmed.startsWith('- type:')) { - // 处理数组项 - const cardType = trimmed.replace('- type:', '').trim(); - const card = { type: cardType }; - - // 如果是耗材卡片,自动添加配置参数 - if (cardType === 'custom:xiaoshi-consumables-card') { - const excludedParams = ['type', 'button_height', 'button_width', 'button_font_size', 'button_icon_size', 'show_preview', 'tap_action']; - Object.keys(this.config).forEach(key => { - if (!excludedParams.includes(key)) { - card[key] = this.config[key]; - } - }); - console.log('弹窗调试: 为耗材卡片添加参数:', card); - } - - if (!Array.isArray(current)) { - // 如果当前不是数组,需要找到父对象并创建数组 - const parentKey = pathStack[pathStack.length - 1]; - const parent = stack[stack.length - 2]; - if (parent && parentKey) { - parent[parentKey] = []; - stack[stack.length - 1] = parent[parentKey]; - } else { - // 如果找不到父对象,创建一个新数组 - const newArray = [card]; - stack[stack.length - 1] = newArray; - stack.push(card); - pathStack.push('card'); - continue; - } - } - - // 确保 current 是数组后再 push - if (Array.isArray(current)) { - current.push(card); - stack.push(card); - pathStack.push('card'); - } - } else if (trimmed.startsWith('- ')) { - // 处理普通数组项(如 - sensor.fuel_price_shaanxi) - const itemValue = trimmed.substring(2).trim(); - - if (!Array.isArray(current)) { - // 如果当前不是数组,需要找到父对象并创建数组 - const parentKey = pathStack[pathStack.length - 1]; - const parent = stack[stack.length - 2]; - if (parent && parentKey) { - parent[parentKey] = []; - stack[stack.length - 1] = parent[parentKey]; - } else { - console.error('弹窗调试: 无法找到父数组'); - continue; - } - } - - // 确保 current 是数组后再 push - if (Array.isArray(current)) { - current.push(itemValue); - } - } else if (trimmed.startsWith('- ')) { - // 处理数组项(非type开头的) - const itemValue = trimmed.substring(2).trim(); - - // 确保当前上下文是数组 - if (Array.isArray(current)) { - current.push(itemValue); - - } else { - // 如果当前不是数组,查找最近的数组 - for (let j = stack.length - 1; j >= 0; j--) { - const obj = stack[j]; - for (const key in obj) { - if (Array.isArray(obj[key])) { - obj[key].push(itemValue); - - break; - } - } - break; - } - } - } else if (trimmed.includes(':')) { - const colonIndex = trimmed.indexOf(':'); - const key = trimmed.substring(0, colonIndex).trim(); - const value = trimmed.substring(colonIndex + 1).trim(); - - if (value && value !== '|') { - // 有值的键值对 - current[key] = value; - - } else if (value !== '|') { - // 没有值的键,需要判断是创建对象还是数组 - // 检查下一行是否以" - "开头(表示是数组项) - const nextLineIndex = i + 1; - const shouldCreateArray = nextLineIndex < lines.length && - lines[nextLineIndex].trim().startsWith('- '); - - if (shouldCreateArray) { - current[key] = []; - - } else { - current[key] = {}; - - } - stack.push(current[key]); - pathStack.push(key); - } - } - } - - // 处理最后的多行字符串 - if (multilineValue !== null && multilineKey !== null) { - const current = stack[stack.length - 1]; - current[multilineKey] = multilineValue.join('\n'); - } - - // 确保有耗材卡片 - if (config.browser_mod && config.browser_mod.data && config.browser_mod.data.content && config.browser_mod.data.content.cards) { - const cards = config.browser_mod.data.content.cards; - const hasConsumablesCard = cards.some(card => card.type === 'custom:xiaoshi-consumables-card'); - - if (!hasConsumablesCard) { - const excludedParams = ['type', 'button_height', 'button_width', 'button_font_size', 'button_icon_size', 'show_preview', 'tap_action']; - const consumablesCard = { - type: 'custom:xiaoshi-consumables-card' - }; - - Object.keys(this.config).forEach(key => { - if (!excludedParams.includes(key)) { - consumablesCard[key] = this.config[key]; - } - }); - - cards.unshift(consumablesCard); - console.log('弹窗调试: 自动添加耗材卡片到完整格式:', consumablesCard); - } - } - - - return config; - } catch (error) { - console.error('tap_action YAML解析错误:', error); - - return null; - } - } - - // 处理单行YAML - _processYamlLine(line, config, currentPath) { - const trimmed = line.trim(); - const indentMatch = line.match(/^(\s*)/); - const indent = indentMatch ? indentMatch[1].length : 0; - - if (trimmed.includes(':')) { - const colonIndex = trimmed.indexOf(':'); - const key = trimmed.substring(0, colonIndex).trim(); - const value = trimmed.substring(colonIndex + 1).trim(); - - // 根据缩进确定路径深度(每2个空格为一级) - const expectedDepth = Math.floor(indent / 2); - currentPath = currentPath.slice(0, expectedDepth); - currentPath.push(key); - - if (value) { - this._setNestedValue(config, currentPath, value); - currentPath = currentPath.slice(0, -1); - } - } else if (trimmed.startsWith('- type:')) { - // 处理数组项 - const expectedDepth = Math.floor((indent - 2) / 2); - currentPath = currentPath.slice(0, expectedDepth); - - // 确保父路径存在且是数组 - if (currentPath.length === 0) { - console.error('数组项没有父级路径'); - return; - } - - const parentPath = currentPath.slice(0, -1); - const arrayKey = currentPath[currentPath.length - 1]; - - let parent = config; - for (const pathKey of parentPath) { - if (!parent[pathKey]) parent[pathKey] = {}; - parent = parent[pathKey]; - } - - if (!parent[arrayKey]) parent[arrayKey] = []; - - const cardType = trimmed.replace('- type:', '').trim(); - parent[arrayKey].push({ type: cardType }); - } - } - - // 设置嵌套值 - _setNestedValue(obj, path, value) { - let current = obj; - for (let i = 0; i < path.length - 1; i++) { - if (!current[path[i]]) { - current[path[i]] = {}; - } - current = current[path[i]]; - } - current[path[path.length - 1]] = value; - } - - - - _onEntitySearch(e) { - const searchTerm = e.target.value.toLowerCase(); - this._searchTerm = searchTerm; - this._showEntityList = true; - - if (!this.hass) return; - - const allEntities = Object.values(this.hass.states); - - this._filteredEntities = allEntities.filter(entity => { - const entityId = entity.entity_id.toLowerCase(); - const friendlyName = (entity.attributes.friendly_name || '').toLowerCase(); - - return entityId.includes(searchTerm) || friendlyName.includes(searchTerm); - }).slice(0, 50); - - this.requestUpdate(); - } - - _toggleEntity(entityId) { - const currentEntities = this.config.entities || []; - let newEntities; - - if (currentEntities.some(e => e.entity_id === entityId)) { - newEntities = currentEntities.filter(e => e.entity_id !== entityId); - } else { - newEntities = [...currentEntities, { - entity_id: entityId, - overrides: undefined - }]; - } - - this.config = { - ...this.config, - entities: newEntities - }; - - this.dispatchEvent(new CustomEvent('config-changed', { - detail: { config: this.config }, - bubbles: true, - composed: true - })); - - this.requestUpdate(); - } - - _removeEntity(index) { - const currentEntities = this.config.entities || []; - const newEntities = currentEntities.filter((_, i) => i !== index); - - this.config = { - ...this.config, - entities: newEntities - }; - - this.dispatchEvent(new CustomEvent('config-changed', { - detail: { config: this.config }, - bubbles: true, - composed: true - })); - - this.requestUpdate(); - } - - _updateEntityAttribute(index, attributeValue) { - const currentEntities = this.config.entities || []; - const newEntities = [...currentEntities]; - - if (newEntities[index]) { - const trimmedValue = attributeValue.trim(); - if (trimmedValue === '') { - // 如果属性为空,则从配置中移除 attribute 字段 - const { attribute, ...entityWithoutAttribute } = newEntities[index]; - newEntities[index] = entityWithoutAttribute; - } else { - // 如果属性不为空,则设置属性值 - newEntities[index] = { - ...newEntities[index], - attribute: trimmedValue - }; - } - } - - this.config = { - ...this.config, - entities: newEntities - }; - - this.dispatchEvent(new CustomEvent('config-changed', { - detail: { config: this.config }, - bubbles: true, - composed: true - })); - - this.requestUpdate(); - } - - _updateEntityOverride(index, overrideType, enabled) { - const currentEntities = this.config.entities || []; - const newEntities = [...currentEntities]; - - if (newEntities[index]) { - const overrides = { ...newEntities[index].overrides }; - - if (enabled) { - overrides[overrideType] = ''; - } else { - delete overrides[overrideType]; - } - - newEntities[index] = { - ...newEntities[index], - overrides: Object.keys(overrides).length > 0 ? overrides : undefined - }; - } - - this.config = { - ...this.config, - entities: newEntities - }; - - this.dispatchEvent(new CustomEvent('config-changed', { - detail: { config: this.config }, - bubbles: true, - composed: true - })); - - this.requestUpdate(); - } - - _updateEntityOverrideValue(index, overrideType, value) { - const currentEntities = this.config.entities || []; - const newEntities = [...currentEntities]; - - if (newEntities[index] && newEntities[index].overrides && newEntities[index].overrides[overrideType] !== undefined) { - const overrides = { ...newEntities[index].overrides }; - overrides[overrideType] = value.trim(); - - newEntities[index] = { - ...newEntities[index], - overrides: overrides - }; - } - - this.config = { - ...this.config, - entities: newEntities - }; - - this.dispatchEvent(new CustomEvent('config-changed', { - detail: { config: this.config }, - bubbles: true, - composed: true - })); - - this.requestUpdate(); - } - - firstUpdated() { - document.addEventListener('click', (e) => { - if (!e.target.closest('.entity-selector')) { - this._showEntityList = false; - this.requestUpdate(); - } - }); - } - - constructor() { - super(); - this._searchTerm = ''; - this._filteredEntities = []; - this._showEntityList = false; - } - - setConfig(config) { - this.config = config; - - // 如果有tap_action配置,自动解析为内部使用的_tap_action_config - // 不保存到配置中,只作为内部属性使用 - if (config.tap_action && config.tap_action.trim() && config.tap_action !== 'none') { - try { - this._tap_action_config = this._parseTapActionYaml(config.tap_action); - } catch (error) { - console.error('tap_action解析失败:', error); - this._tap_action_config = null; - } - } else { - this._tap_action_config = null; - } - } -} -customElements.define('xiaoshi-consumables-button-editor', XiaoshiConsumablesButtonEditor); - -class XiaoshiConsumablesButton extends LitElement { - static get properties() { - return { - hass: Object, - config: Object, - _oilPriceData: Array, - _loading: Boolean, - _refreshInterval: Number, - theme: { type: String } - }; - } - - static get styles() { - return css` - :host { - display: block; - width: var(--card-width, 100%); - } - - /*button新元素 开始*/ - .consumables-status { - width: var(--button-width, 16vw); - height: var(--button-height, 2.8vh); - padding: 0; - margin: 0; - background: var(--bg-color, #fff); - color: var(--fg-color, #000); - border-radius: 10px; - font-size: var(--button-font-size, 14px); - 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; - } - - .status-icon { - --mdc-icon-size: var(--button-icon-size, 18px); - color: var(--fg-color, #000); - } - /*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: rgba(255, 0, 0, 0.7); - color: #fff; - } - - .device-count.zero { - background: rgba(0, 205, 0, 0.7); - color: #fff; - } - - /*标题刷新按钮*/ - .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; - justify-content: space-between; - margin: 0px 16px; - padding: 0; - border-bottom: 1px solid rgb(150,150,150,0.5); - cursor: pointer; - transition: background-color 0.2s; - min-height: 30px; - max-height: 30px; - } - - .device-item:first-child { - border-top: 1px solid rgb(150,150,150,0.5); - } - - .device-item:hover { - background-color: rgba(150,150,150,0.1); - } - - /*设备、实体明细背景*/ - .devices-list { - flex: 1; - overflow-y: auto; - min-height: 0; - padding: 0 0 8px 0; - } - - /*2列布局容器*/ - .devices-grid { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 0 15px; - padding: 0px 16px; - width: 100%; - box-sizing: border-box; - overflow: hidden; - } - - /*强制每列等宽*/ - .devices-grid > * { - min-width: 0; - width: 100%; - box-sizing: border-box; - overflow: hidden; - } - - /*2列布局中的设备项*/ - .devices-grid .device-item { - margin: 0.5px 0; - padding: 0; - background: var(--bg-color, #fff); - display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; - transition: background-color 0.2s; - min-height: 30px; - max-height: 30px; - border-bottom: none; - border-right: none; - border-left: none; - width: 100%; - max-width: 100%; - box-sizing: border-box; - overflow: hidden; - border-bottom: 1px solid rgb(150,150,150,0.5); - } - - .devices-grid .device-item:hover { - background-color: rgba(150,150,150,0.1); - } - - /*2列布局中的第一行顶部边框*/ - .devices-grid .device-item:nth-child(1), - .devices-grid .device-item:nth-child(2) { - border-top: 1px solid rgb(150,150,150,0.5); - } - - /*1列布局保持原有样式*/ - .devices-list.single-column { - padding: 0 0 8px 0; - } - - .device-left { - display: flex; - align-items: center; - flex: 1; - min-width: 0; - overflow: hidden; - } - - .device-icon { - margin-right: 8px; - color: var(--fg-color, #000); - flex-shrink: 0; - font-size: 10px; - width: 12px; - height: 12px; - display: flex; - align-items: center; - justify-content: center; - } - - .device-name { - color: var(--fg-color, #000); - font-size: 11px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; - min-width: 0; - } - - .device-value { - color: var(--fg-color, #000); - font-size: 11px; - flex-shrink: 0; - font-weight: bold; - max-width: 45%; - text-align: right; - overflow: hidden; - white-space: nowrap; - } - - .device-value.warning { - color: #F44336; - } - - .device-unit { - font-size: 11px; - color: var(--fg-color, #000); - margin-left: 0.5px; - font-weight: bold; - white-space: nowrap; - flex-shrink: 0; - } - - .device-unit.warning { - color: #F44336; - } - - .no-devices { - text-align: center; - padding: 10px 0; - color: var(--fg-color, #000); - } - - .loading { - text-align: center; - padding: 10px 0; - color: var(--fg-color, #000); - } - `; - } - - constructor() { - super(); - this._oilPriceData = []; - this._loading = false; - this._refreshInterval = null; - this.theme = 'on'; - } - - static getConfigElement() { - return document.createElement("xiaoshi-consumables-button-editor"); - } - - connectedCallback() { - super.connectedCallback(); - this._loadOilPriceData(); - - // 设置主题属性 - this.setAttribute('theme', this._evaluateTheme()); - - // 每300秒刷新一次数据,减少频繁刷新 - this._refreshInterval = setInterval(() => { - this._loadOilPriceData(); - }, 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 _loadOilPriceData() { - if (!this.hass) return; - - this._loading = true; - this.requestUpdate(); - - try { - const entities = this.config.entities || []; - const consumablesData = []; - - for (const entityConfig of entities) { - const entityId = entityConfig.entity_id; - const attributeName = entityConfig.attribute; - const entity = this.hass.states[entityId]; - if (!entity) continue; - - const attributes = entity.attributes; - let value = entity.state; - let unit = '元'; - - // 如果指定了属性,则使用属性值 - if (attributeName && attributes[attributeName] !== undefined) { - value = attributes[attributeName]; - } - - // 特殊实体类型的数值显示逻辑 - if (!attributeName) { - // binary_sensor 实体:off显示正常,on显示缺少 - if (entityId.startsWith('binary_sensor.')) { - if (value === 'off') { - value = '正常'; - } else if (value === 'on') { - value = '缺少'; - } - } - // event 实体:unknown显示正常,非unknown或不可用时显示低电量 - else if (entityId.startsWith('event.')) { - if (value === 'unknown') { - value = '正常'; - } else if (value !== 'unknown' && value !== 'unavailable') { - value = '低电量'; - } - } - } - - // 尝试从属性中获取单位 - if (attributes.unit_of_measurement) { - unit = attributes.unit_of_measurement; - } else { - // 如果实体没有单位,则不显示单位 - unit = ''; - } - - // 应用属性重定义 - let friendlyName = attributes.friendly_name || entityId; - let icon = attributes.icon || 'mdi:help-circle'; - let warningThreshold = undefined; - let conversion = undefined; - - // 应用用户自定义的重定义 - if (entityConfig.overrides) { - if (entityConfig.overrides.name !== undefined && entityConfig.overrides.name !== '') { - friendlyName = entityConfig.overrides.name; - } - if (entityConfig.overrides.icon !== undefined && entityConfig.overrides.icon !== '') { - icon = entityConfig.overrides.icon; - } - if (entityConfig.overrides.unit_of_measurement !== undefined && entityConfig.overrides.unit_of_measurement !== '') { - unit = entityConfig.overrides.unit_of_measurement; - } - if (entityConfig.overrides.warning !== undefined && entityConfig.overrides.warning !== '') { - warningThreshold = entityConfig.overrides.warning; // 保持原始字符串 - } - if (entityConfig.overrides.conversion !== undefined && entityConfig.overrides.conversion !== '') { - conversion = entityConfig.overrides.conversion; // 换算表达式 - } - } - - // 应用换算(只对数值进行换算,不对文本状态进行换算) - let originalValue = value; - if (conversion && !isNaN(parseFloat(value))) { - value = this._applyConversion(value, conversion); - } else if (conversion && isNaN(parseFloat(value))) { - } - - consumablesData.push({ - entity_id: entityId, - friendly_name: friendlyName, - value: value, - original_value: originalValue, - unit: unit, - icon: icon, - warning_threshold: warningThreshold, - conversion: conversion - }); - } - - this._oilPriceData = consumablesData; - } catch (error) { - console.error('加载设备耗材数据失败:', error); - this._oilPriceData = []; - } - - this._loading = false; - } - - _handleRefresh() { - this._handleClick(); - this._loadOilPriceData(); - } - - _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); - } - } - /*button新元素 开始*/ - - _handleClick(){ - if (navigator.vibrate) { - navigator.vibrate(50); - } - else if (navigator.webkitVibrate) { - navigator.webkitVibrate(50); - } - else { - } - } - - // 获取默认弹出样式 - _getDefaultPopupStyle() { - return '--mdc-theme-surface: rgba(0,0,0,0)\n--dialog-backdrop-filter: blur(10px) brightness(1);'; - } - - // 构建完整的弹出样式 - _buildFullPopupStyle() { - const baseStyle = this._getDefaultPopupStyle(); - const popupMinWidth = this.config.width || '100%'; - return baseStyle + `\n--popup-min-width: ${popupMinWidth}`; - } - - _handleButtonClick() { - // 默认 tap_action 行为:弹出耗材卡片 - const excludedParams = ['type', 'button_height', 'button_width', 'button_font_size', 'button_icon_size', 'show_preview', 'tap_action']; - const cardConfig = {}; - - Object.keys(this.config).forEach(key => { - if (!excludedParams.includes(key)) { - cardConfig[key] = this.config[key]; - } - }); - - - - // 检查是否配置了自定义的 tap_action - if (this._tap_action_config) { - - // 使用用户配置的自定义tap_action - const actionConfig = this._tap_action_config; - - // 如果是简化格式(没有action字段),需要包装成browser_mod格式 - if (!actionConfig.action && actionConfig.type) { - - - // 创建新的配置对象,避免循环引用 - const originalContent = { ...actionConfig }; - - // 清空原对象并重新赋值 - Object.keys(actionConfig).forEach(key => delete actionConfig[key]); - - // 设置新的browser_mod格式 - actionConfig.action = 'fire-dom-event'; - actionConfig.browser_mod = { - service: 'popup', - data: { - content: originalContent, - style: this._buildFullPopupStyle() - } - }; - - - } - - // 如果配置中有content且是vertical-stack,则插入耗材卡片 - if (actionConfig.browser_mod && actionConfig.browser_mod.data && - actionConfig.browser_mod.data.content) { - - let content = actionConfig.browser_mod.data.content; - - - // 如果content是字符串,尝试解析为JSON - if (typeof content === 'string') { - try { - content = JSON.parse(content); - - } catch (e) { - console.error('解析content失败:', e); - content = {}; - } - } - - // 如果是vertical-stack且有cards数组,插入耗材卡片 - if (content.type === 'vertical-stack' && Array.isArray(content.cards)) { - // 先移除已存在的耗材卡片,避免重复插入 - content.cards = content.cards.filter(card => - card.type !== 'custom:xiaoshi-consumables-card' - ); - - const consumablesCard = { - type: 'custom:xiaoshi-consumables-card', - ...cardConfig - }; - - content.cards.unshift(consumablesCard); - actionConfig.browser_mod.data.content = content; - } - } - - // 执行配置的动作 - if (actionConfig.action === 'fire-dom-event' && window.browser_mod) { - - - // 使用用户配置的完整content - try { - - const popupData = { - ...actionConfig.browser_mod.data - }; - - // 构建完整的样式,包括用户配置和自动添加的宽度 - const fullStyle = this._buildFullPopupStyle(); - - // 如果没有style,使用构建的完整style - if (!popupData.style && !popupData['--mdc-theme-surface']) { - popupData.style = fullStyle; - } else if (popupData.style) { - // 如果已有style,追加宽度配置 - popupData.style += `\n--popup-min-width: ${this.config.width || '100%'}`; - } - - window.browser_mod.service('popup', popupData); - } catch (error) { - console.error('用户配置弹窗失败,使用备用方案:', error); - - // 备用方案:使用默认耗材卡片 - const consumablesContent = { - type: 'custom:xiaoshi-consumables-card', - ...cardConfig - }; - - window.browser_mod.service('popup', { - style: this._buildFullPopupStyle(), - content: consumablesContent - }); - } - } else { - - } - } else { - - // 默认行为:只显示耗材卡片 - const popupStyle = this._buildFullPopupStyle(); - - if (window.browser_mod) { - // 使用之前工作的简单方式 - const simplePopupContent = { - type: 'custom:xiaoshi-consumables-card', - ...cardConfig - }; - - window.browser_mod.service('popup', { - style: popupStyle, - content: simplePopupContent - }); - } - } - this._handleClick(); - } - - // 备选的弹出方案 - _showDefaultPopup(content) { - try { - const event = new Event('hass-more-info', { - composed: true, - bubbles: true - }); - event.detail = { - entityId: 'none', - content: content - }; - this.dispatchEvent(event); - } catch (error) { - console.error('Failed to show default popup:', error); - } - } - /*button新元素 结束*/ - - _renderDeviceItem(consumablesData) { - let isWarning = false; - - // 特殊实体类型的默认预警逻辑 - if (consumablesData.entity_id.startsWith('binary_sensor.') && !consumablesData.warning_threshold) { - // binary_sensor: "缺少"状态时预警 - isWarning = consumablesData.value === '缺少'; - } else if (consumablesData.entity_id.startsWith('event.') && !consumablesData.warning_threshold) { - // event: "低电量"状态时预警 - isWarning = consumablesData.value === '低电量'; - } else { - // 使用配置的预警条件 - if (consumablesData.warning_threshold && consumablesData.warning_threshold.trim() !== '') { - isWarning = this._evaluateWarningCondition(consumablesData.value, consumablesData.warning_threshold); - } else { - if (this.config.global_warning && this.config.global_warning.trim() !== '') { - isWarning = this._evaluateWarningCondition(consumablesData.value, this.config.global_warning); - } - } - } - - return html` -
this._handleEntityClick(consumablesData)}> -
- -
${consumablesData.friendly_name}
-
-
- ${consumablesData.value} - ${consumablesData.unit} -
-
- `; - } - - _applyConversion(value, conversion) { - if (!conversion || !value) return value; - - try { - // 提取数值部分 - const numericValue = parseFloat(value); - if (isNaN(numericValue)) { - console.warn(`无法将值 "${value}" 转换为数字进行换算`); - return value; - } - - // 解析换算表达式 - const match = conversion.match(/^([+\-*/])(\d+(?:\.\d+)?)$/); - if (!match) { - console.warn(`无效的换算表达式: "${conversion}",支持的格式: +10, -10, *1.5, /2`); - return value; - } - - const operator = match[1]; - const operand = parseFloat(match[2]); - - let result; - switch (operator) { - case '+': - result = numericValue + operand; - break; - case '-': - result = numericValue - operand; - break; - case '*': - result = numericValue * operand; - break; - case '/': - result = numericValue / operand; - break; - default: - return value; - } - - // 返回结果,保留适当的小数位数 - return Number.isInteger(result) ? result.toString() : result.toFixed(2).toString(); - - } catch (error) { - console.error(`换算时出错: ${error.message}`); - return value; - } - } - - _evaluateWarningCondition(value, condition) { - if (!condition) return false; - - const match = condition.match(/^(>=|<=|>|<|==|!=)\s*(.+)$/); - if (!match) return false; - - const operator = match[1]; - let compareValue = match[2].trim(); - - if ((compareValue.startsWith('"') && compareValue.endsWith('"')) || - (compareValue.startsWith("'") && compareValue.endsWith("'"))) { - compareValue = compareValue.slice(1, -1); - } - - const numericValue = parseFloat(value); - const numericCompare = parseFloat(compareValue); - - if (!isNaN(numericValue) && !isNaN(numericCompare)) { - switch (operator) { - case '>': return numericValue > numericCompare; - case '>=': return numericValue >= numericCompare; - case '<': return numericValue < numericCompare; - case '<=': return numericValue <= numericCompare; - case '==': return numericValue === numericCompare; - case '!=': return numericValue !== numericCompare; - } - } - - const stringValue = String(value); - const stringCompare = compareValue; - - switch (operator) { - case '==': return stringValue === stringCompare; - case '!=': return stringValue !== stringCompare; - case '>': return stringValue > stringCompare; - case '>=': return stringValue >= stringCompare; - case '<': return stringValue < stringCompare; - case '<=': return stringValue <= stringCompare; - } - - return false; - } - - // 解析tap_action YAML配置 - _parseTapActionYaml(yamlText) { - try { - - const lines = yamlText.split('\n'); - const config = {}; - const stack = [config]; - const pathStack = []; - let multilineValue = null; - let multilineKey = null; - let multilineIndent = 0; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const trimmed = line.trim(); - - - - if (!trimmed || trimmed.startsWith('#')) continue; - - // 处理多行字符串 - if (multilineValue !== null) { - const currentIndent = line.match(/^(\s*)/)[1].length; - if (currentIndent > multilineIndent) { - multilineValue.push(line); - continue; - } else { - // 多行字符串结束 - const current = stack[stack.length - 1]; - current[multilineKey] = multilineValue.join('\n'); - multilineValue = null; - multilineKey = null; - } - } - - // 检查是否是多行字符串开始 - if (trimmed === '|') { - // 获取上一行的键 - for (let j = i - 1; j >= 0; j--) { - const prevLine = lines[j]; - const prevTrimmed = prevLine.trim(); - if (prevTrimmed && prevTrimmed.includes(':')) { - const colonIndex = prevTrimmed.indexOf(':'); - multilineKey = prevTrimmed.substring(0, colonIndex).trim(); - break; - } - } - if (multilineKey) { - multilineValue = []; - multilineIndent = line.match(/^(\s*)/)[1].length; - - continue; - } - } - - // 计算缩进级别 - const indent = line.match(/^(\s*)/)[1].length; - const level = Math.floor(indent / 2); - - // 调整栈到正确的层级 - while (stack.length > level + 1) { - stack.pop(); - pathStack.pop(); - } - - const current = stack[stack.length - 1]; - - if (trimmed.startsWith('- type:')) { - // 处理数组项 - const cardType = trimmed.replace('- type:', '').trim(); - const card = { type: cardType }; - - // 找到父对象和键名 - let parent = null; - let parentKey = null; - - // 从当前栈中找到合适的父对象 - for (let j = stack.length - 1; j >= 0; j--) { - const obj = stack[j]; - const keys = Object.keys(obj); - for (const key of keys) { - if (Array.isArray(obj[key])) { - parent = obj; - parentKey = key; - break; - } - } - if (parent) break; - } - - // 如果没找到父数组,检查pathStack中的最后一个键是否对应数组 - if (!parent && pathStack.length > 0) { - const lastKey = pathStack[pathStack.length - 1]; - const potentialParent = stack[stack.length - 2]; - if (potentialParent && potentialParent[lastKey]) { - if (Array.isArray(potentialParent[lastKey])) { - parent = potentialParent; - parentKey = lastKey; - } else if (typeof potentialParent[lastKey] === 'object' && Object.keys(potentialParent[lastKey]).length === 0) { - // 将空对象转换为数组 - potentialParent[lastKey] = []; - parent = potentialParent; - parentKey = lastKey; - } - } - } - - if (parent && parentKey) { - // 将卡片添加到现有数组 - parent[parentKey].push(card); - stack.push(card); - pathStack.push('card'); - - } else { - // 创建新数组或处理特殊情况 - const current = stack[stack.length - 1]; - if (Array.isArray(current)) { - current.push(card); - stack.push(card); - pathStack.push('card'); - - } else { - // 查找最近的cards键 - let foundCards = false; - for (let j = stack.length - 1; j >= 0; j--) { - const obj = stack[j]; - if (obj.cards !== undefined) { - if (!Array.isArray(obj.cards)) { - obj.cards = []; - } - obj.cards.push(card); - stack.push(card); - pathStack.push('card'); - foundCards = true; - - break; - } - } - if (!foundCards) { - - config.cards = [card]; - stack.push(card); - pathStack.push('card'); - } - } - } - } else if (trimmed.startsWith('- ')) { - // 处理数组项(非type开头的) - const itemValue = trimmed.substring(2).trim(); - - // 确保当前上下文是数组 - if (Array.isArray(current)) { - current.push(itemValue); - - } else { - // 如果当前不是数组,查找最近的数组 - for (let j = stack.length - 1; j >= 0; j--) { - const obj = stack[j]; - for (const key in obj) { - if (Array.isArray(obj[key])) { - obj[key].push(itemValue); - - break; - } - } - break; - } - } - } else if (trimmed.includes(':')) { - const colonIndex = trimmed.indexOf(':'); - const key = trimmed.substring(0, colonIndex).trim(); - const value = trimmed.substring(colonIndex + 1).trim(); - - if (value && value !== '|') { - // 有值的键值对 - current[key] = value; - - } else if (value !== '|') { - // 没有值的键,需要判断是创建对象还是数组 - // 检查下一行是否以" - "开头(表示是数组项) - const nextLineIndex = i + 1; - const shouldCreateArray = nextLineIndex < lines.length && - lines[nextLineIndex].trim().startsWith('- '); - - if (shouldCreateArray) { - current[key] = []; - - } else { - current[key] = {}; - - } - stack.push(current[key]); - pathStack.push(key); - } - } - } - - // 处理最后的多行字符串 - if (multilineValue !== null && multilineKey !== null) { - const current = stack[stack.length - 1]; - current[multilineKey] = multilineValue.join('\n'); - } - - - return config; - } catch (error) { - console.error('tap_action YAML解析错误:', error); - - return null; - } - } - - - 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._oilPriceData.filter(consumablesData => { - let isWarning = false; - - // 对于 binary_sensor 和 event,使用默认预警逻辑 - if (consumablesData.entity_id.startsWith('binary_sensor.') && !consumablesData.warning_threshold) { - // binary_sensor: "缺少"状态时预警 - isWarning = consumablesData.value === '缺少'; - } else if (consumablesData.entity_id.startsWith('event.') && !consumablesData.warning_threshold) { - // event: "低电量"状态时预警 - isWarning = consumablesData.value === '低电量'; - } else { - // 使用配置的预警条件 - if (consumablesData.warning_threshold && consumablesData.warning_threshold.trim() !== '') { - isWarning = this._evaluateWarningCondition(consumablesData.value, consumablesData.warning_threshold); - } else { - if (this.config.global_warning && this.config.global_warning.trim() !== '') { - isWarning = this._evaluateWarningCondition(consumablesData.value, this.config.global_warning); - } - } - } - - return isWarning; - }).length; - - /*button新元素 前9行和最后1行开始*/ - const showPreview = this.config.show_preview !== false; - - return html` -
- - 耗材: ${warningCount === 0 ? 0 : warningCount} -
- ${showPreview ? html` -
- -
- - -
-
- - ${this.config.name || '耗材信息统计'} -
-
- ${warningCount} -
-
- - ${this._loading ? - html`
加载中...
` : - - this._oilPriceData.length === 0 ? - html`
请配置耗材实体
` : - this.config.columns === '1' ? html` -
- ${this._oilPriceData.map(consumablesData => this._renderDeviceItem(consumablesData))} -
- ` : html` -
- ${this._oilPriceData.map(consumablesData => this._renderDeviceItem(consumablesData))} -
- ` - } -
- ` : html``} - `; - /*button新元素 结束*/ - } - - setConfig(config) { - this.config = config; - - // 如果有tap_action配置,自动解析为内部使用的_tap_action_config - // 不保存到配置中,只作为内部属性使用 - if (config.tap_action && config.tap_action.trim() && config.tap_action !== 'none') { - try { - this._tap_action_config = this._parseTapActionYaml(config.tap_action); - } catch (error) { - console.error('tap_action解析失败:', error); - this._tap_action_config = null; - } - } else { - this._tap_action_config = null; - } - - /*button新元素 开始*/ - if (config.button_width) { - this.style.setProperty('--button-width', config.button_width); - } else { - this.style.setProperty('--button-width', '16vw'); - } - - // 设置按钮高度(只控制 consumables-status) - if (config.button_height) { - this.style.setProperty('--button-height', config.button_height); - } else { - this.style.setProperty('--button-height', '2.8vh'); - } - - // 设置按钮文字大小(只控制 consumables-status) - if (config.button_font_size) { - this.style.setProperty('--button-font-size', config.button_font_size); - } else { - this.style.setProperty('--button-font-size', '14px'); - } - - // 设置按钮图标大小(只控制 consumables-status) - if (config.button_icon_size) { - this.style.setProperty('--button-icon-size', config.button_icon_size); - } else { - this.style.setProperty('--button-icon-size', '18px'); - } - - // 设置卡片宽度(控制原来的 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 = 3; - const entitySize = Math.max(0, Math.min(this._oilPriceData.length * 2, 10)); - return baseSize + entitySize; - } -} -customElements.define('xiaoshi-consumables-button', XiaoshiConsumablesButton);