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