diff --git a/xiaoshi-device-consumables-button2.js b/xiaoshi-device-consumables-button2.js new file mode 100644 index 0000000..ac6e7fb --- /dev/null +++ b/xiaoshi-device-consumables-button2.js @@ -0,0 +1,2397 @@ +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: 9px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + min-width: 0; + } + + .device-value { + color: var(--fg-color, #000); + font-size: 9px; + flex-shrink: 0; + font-weight: bold; + max-width: 45%; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .device-value.warning { + color: #F44336; + } + + .device-unit { + font-size: 9px; + 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._loadOilPriceData(); + navigator.vibrate(50); + } + + _handleEntityClick(entity) { + navigator.vibrate(50); + // 点击实体时打开实体详情页 + if (entity.entity_id) { + const evt = new Event('hass-more-info', { composed: true }); + evt.detail = { entityId: entity.entity_id }; + this.dispatchEvent(evt); + } + } + /*button新元素 开始*/ + + // 获取默认弹出样式 + _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 + }); + } + } + navigator.vibrate(50); + } + + // 备选的弹出方案 + _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);