import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module"; class XiaoshiBalanceCardEditor 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; cursor: pointer; 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; } `; } 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, '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, '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, '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="预警值" min="0" step="0.01" ?disabled=${entityConfig.overrides?.warning === undefined} />
`; })} ` : ''}
搜索并选择要显示的设备余额实体,支持多选。每个实体可以配置:
• 属性名:留空使用实体状态,或输入属性名
• 名称重定义:勾选后可自定义显示名称
• 图标重定义:勾选后可自定义图标(如 mdi:phone)
• 单位重定义:勾选后可自定义单位(如 元、$、kWh 等)
• 预警值:勾选后设置预警值,低于此值显示红色
• 未勾选重定义时,将使用实体的原始属性值
`; } _entityChanged(e) { const { name, value } = e.target; if (!value && name !== 'theme' && name !== 'width' ) return; let finalValue = value; // 处理不同字段的默认值 if (name === 'width') { finalValue = value || '100%'; } this.config = { ...this.config, [name]: finalValue }; this.dispatchEvent(new CustomEvent('config-changed', { detail: { config: this.config }, bubbles: true, composed: true })); } _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, attribute: null, 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]) { newEntities[index] = { ...newEntities[index], attribute: attributeValue.trim() || null }; } 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; } } customElements.define('xiaoshi-balance-card-editor', XiaoshiBalanceCardEditor); class XiaoshiBalanceCard 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%); } ha-card { width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--bg-color, #fff); border-radius: 12px; } /*标题容器*/ .card-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; background: var(--bg-color, #fff); border-radius: 12px; } /*标题红色圆点*/ .offline-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; } /*标题红色圆点动画*/ @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } /*标题*/ .card-title { font-size: 20px; font-weight: 500; color: var(--fg-color, #000); height: 30px; line-height: 30px; display: flex; align-items: center; justify-content: center; } /*标题统计数字*/ .device-count { color: var(--fg-color, #000); border-radius: 8px; font-size: 13px; width: 30px; height: 30px; text-align: center; line-height: 30px; font-weight: bold; padding: 0px; } .device-count.non-zero { background: rgb(2, 250, 250, 0.5); } .device-count.zero { background: rgb(0, 205, 0); } /*标题刷新按钮*/ .refresh-btn { color: var(--fg-color, #fff); border: none; border-radius: 8px; padding: 5px; cursor: pointer; font-size: 13px; width: 50px; height: 30px; line-height: 30px; text-align: center; font-weight: bold; padding: 0px; } /*2级标题*/ .section-divider { margin: 0 0 8px 0; padding: 8px 8px; background: var(--bg-color, #fff); font-weight: 500; color: var(--fg-color, #000); border-top: 1px solid rgb(150,150,150,0.5); border-bottom: 1px solid rgb(150,150,150,0.5); margin: 0 16px 0 16px; } /*2级标题字体*/ .section-title { display: flex; align-items: center; justify-content: space-between; color: var(--fg-color, #000); font-size: 13px; } /*2级标题,统计数量字体*/ .section-count { background: rgb(255,0,0,0.5); color: var(--fg-color, #000); border-radius: 12px; width: 15px; height: 15px; text-align: center; line-height: 15px; padding: 3px; font-size: 12px; font-weight: bold; } /*设备、实体明细*/ .device-item { display: flex; align-items: center; justify-content: space-between; margin: 0px 16px; padding: 8px 0; border-bottom: 1px solid rgb(150,150,150,0.5); cursor: pointer; transition: background-color 0.2s; } .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; } .device-left { display: flex; align-items: center; flex: 1; min-width: 0; } .device-icon { margin-left: 12px; color: var(--fg-color, #000); flex-shrink: 0; } .device-name { color: var(--fg-color, #000); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .device-value { color: var(--fg-color, #000); font-size: 12px; margin-left: auto; flex-shrink: 0; font-weight: bold; } .device-value.warning { color: #F44336; } .device-unit { font-size: 12px; color: var(--fg-color, #000); margin-left: 4px; font-weight: bold; } .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-balance-card-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 balanceData = []; 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 (attributes.unit_of_measurement) { unit = attributes.unit_of_measurement; } // 应用属性重定义 let friendlyName = attributes.friendly_name || entityId; let icon = attributes.icon || 'mdi:help-circle'; let warningThreshold = 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 = parseFloat(entityConfig.overrides.warning); } } balanceData.push({ entity_id: entityId, friendly_name: friendlyName, value: value, unit: unit, icon: icon, warning_threshold: warningThreshold }); } this._oilPriceData = balanceData; } 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); } } 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)'; return html`
${this.config.name || '电话余额信息'}
${this._loading ? html`
加载中...
` : this._oilPriceData.length === 0 ? html`
请配置余额实体
` : html` ${this._oilPriceData.map(balanceData => { const numericValue = parseFloat(balanceData.value); const isWarning = balanceData.warning_threshold !== undefined && !isNaN(numericValue) && numericValue < balanceData.warning_threshold; return html`
this._handleEntityClick(balanceData)}>
${balanceData.friendly_name}
${balanceData.value} ${balanceData.unit}
`; })} ` }
`; } setConfig(config) { this.config = config; // 设置CSS变量来控制卡片的宽度和高度 if (config.width) { this.style.setProperty('--card-width', config.width); } // 设置主题 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-balance-card', XiaoshiBalanceCard);