diff --git a/xiaoshi-device-balance-card.js b/xiaoshi-device-balance-card.js
new file mode 100644
index 0000000..e302a69
--- /dev/null
+++ b/xiaoshi-device-balance-card.js
@@ -0,0 +1,856 @@
+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;
+ }
+
+ .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;
+ }
+
+ .attribute-config {
+ margin-top: 4px;
+ }
+
+ .attribute-input {
+ width: 100%;
+ padding: 4px 8px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 12px;
+ box-sizing: border-box;
+ }
+
+ .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`
+
+
+ `;
+ }
+
+ _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 }];
+ }
+
+ 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();
+ }
+
+ // 点击外部关闭下拉列表
+ 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-right: 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;
+ }
+
+ balanceData.push({
+ entity_id: entityId,
+ friendly_name: attributes.friendly_name || entityId,
+ value: value,
+ unit: unit,
+ icon: attributes.icon || 'mdi:help-circle'
+ });
+ }
+
+ 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._loading ?
+ html`
加载中...
` :
+
+ this._oilPriceData.length === 0 ?
+ html`
请配置余额实体
` :
+ html`
+ ${this._oilPriceData.map(balanceData => {
+ const warningThreshold = this.config.warning || 20;
+ const numericValue = parseFloat(balanceData.value);
+ const isWarning = !isNaN(numericValue) && numericValue < warningThreshold;
+
+ 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);