Compare commits

..

93 Commits

Author SHA1 Message Date
xiaoshi
e15cdfccaa Update xiaoshi-pad-card.js 2026-01-01 15:12:37 +08:00
xiaoshi
c0f45717d4 Update xiaoshi-pad-grid-card.js 2026-01-01 15:11:40 +08:00
xiaoshi
d57f69d4b9 Update xiaoshi-device-ha-info-card.js 2026-01-01 15:11:03 +08:00
xiaoshi
1ba54782c0 Update xiaoshi-device-ha-info-button.js 2026-01-01 15:10:42 +08:00
xiaoshi
240ce4e4ef Update xiaoshi-pad-card.js 2025-12-27 14:01:30 +08:00
xiaoshi
9d0fbaead4 Update xiaoshi-device-todo-button.js 2025-12-27 14:01:17 +08:00
xiaoshi
b357b69fa5 Update xiaoshi-device-consumables-button.js 2025-12-27 14:00:40 +08:00
xiaoshi
0d61619f8d Update xiaoshi-device-balance-button.js 2025-12-27 14:00:21 +08:00
xiaoshi
953e414a61 Update xiaoshi-device-ha-info-button.js 2025-12-27 14:00:00 +08:00
xiaoshi
8974f122b6 Update xiaoshi-device-ha-info-button.js 2025-12-27 13:59:27 +08:00
xiaoshi
6060663a24 Update xiaoshi-device-consumables-button.js 2025-12-27 13:59:08 +08:00
xiaoshi
22e7325a06 Update xiaoshi-device-balance-button.js 2025-12-27 13:58:47 +08:00
xiaoshi
67f13a23ca Update xiaoshi-pad-card.js 2025-12-27 13:08:52 +08:00
xiaoshi
56e85c834f Update xiaoshi-device-todo-button.js 2025-12-27 13:08:15 +08:00
xiaoshi
367a47d33b Update xiaoshi-device-ha-info-button.js 2025-12-27 13:07:54 +08:00
xiaoshi
6501aeead7 Update xiaoshi-device-consumables-button.js 2025-12-27 13:07:29 +08:00
xiaoshi
c93735be82 Update xiaoshi-device-balance-button.js 2025-12-27 13:06:56 +08:00
xiaoshi
740bac1c63 Update xiaoshi-device-todo-button.js 2025-12-20 01:48:48 +08:00
xiaoshi
85d413baa1 Update data refresh interval to 3 seconds
Changed the data refresh interval from 300 seconds to 3 seconds.
2025-12-20 01:48:21 +08:00
xiaoshi
128d24328b Update xiaoshi-device-consumables-button.js 2025-12-20 01:47:57 +08:00
xiaoshi
de64a44599 Update xiaoshi-device-balance-button.js 2025-12-20 01:47:29 +08:00
xiaoshi
cf3e37da9e Update xiaoshi-pad-card.js 2025-12-13 22:49:01 +08:00
xiaoshi
cfc9618e87 Update xiaoshi-device-todo-button.js 2025-12-13 22:48:34 +08:00
xiaoshi
fe22c85d3b Update xiaoshi-device-ha-info-button.js 2025-12-13 22:48:20 +08:00
xiaoshi
13bcd1a702 Update xiaoshi-device-consumables-button.js 2025-12-13 22:48:03 +08:00
xiaoshi
1fb2f8dcb7 Update xiaoshi-device-balance-button.js 2025-12-13 22:47:43 +08:00
xiaoshi
6abbc0e917 Update xiaoshi-pad-card.js 2025-12-06 21:55:16 +08:00
xiaoshi
a0d123a998 Add files via upload 2025-12-06 21:54:51 +08:00
xiaoshi
8014a664b4 Delete xiaoshi-device-todo-button.js 2025-12-06 21:54:35 +08:00
xiaoshi
ed3c9b5896 Delete xiaoshi-device-ha-info-button.js 2025-12-06 21:54:27 +08:00
xiaoshi
b2eccc66cf Delete xiaoshi-device-consumables-button.js 2025-12-06 21:54:19 +08:00
xiaoshi
08242a4758 Delete xiaoshi-device-balance-button.js 2025-12-06 21:54:11 +08:00
xiaoshi
513a906231 Update xiaoshi-pad-card.js 2025-12-06 20:31:27 +08:00
xiaoshi
bcc4585278 Add files via upload 2025-12-06 20:31:12 +08:00
xiaoshi
b5598387a1 Delete xiaoshi-device-todo-button.js 2025-12-06 20:30:55 +08:00
xiaoshi
fbdece70f9 Delete xiaoshi-device-ha-info-button.js 2025-12-06 20:30:47 +08:00
xiaoshi
85c7123854 Delete xiaoshi-device-consumables-button.js 2025-12-06 20:30:39 +08:00
xiaoshi
daae901d1b Delete xiaoshi-device-balance-button.js 2025-12-06 20:30:31 +08:00
xiaoshi
b221bacb78 Update xiaoshi-pad-card.js 2025-12-06 20:16:56 +08:00
xiaoshi
7b7f0c632d Update xiaoshi-device-todo-button.js 2025-12-06 20:16:06 +08:00
xiaoshi
1523dc76d3 Update xiaoshi-device-ha-info-button.js 2025-12-06 20:15:52 +08:00
xiaoshi
20eae200c1 Update xiaoshi-device-consumables-button.js 2025-12-06 20:15:39 +08:00
xiaoshi
763b1cc977 Update xiaoshi-pad-card.js 2025-12-06 19:44:27 +08:00
xiaoshi
3c54c6315e Add files via upload 2025-12-06 19:44:01 +08:00
xiaoshi
74c53128c8 Update xiaoshi-pad-card.js 2025-12-06 19:43:41 +08:00
xiaoshi
9973d7b614 Delete xiaoshi-device-update-card.js 2025-12-06 19:43:19 +08:00
xiaoshi
a4d4fba8a1 Delete xiaoshi-device-todo-card.js 2025-12-06 19:43:11 +08:00
xiaoshi
27d567803b Delete xiaoshi-device-offline-card.js 2025-12-06 19:43:01 +08:00
xiaoshi
b2bd56026c Delete xiaoshi-device-ha-info-card.js 2025-12-06 19:42:54 +08:00
xiaoshi
00b7fe4842 Delete xiaoshi-device-consumables-card.js 2025-12-06 19:42:47 +08:00
xiaoshi
8f96c49931 Delete xiaoshi-device-consumables-button.js 2025-12-06 19:42:38 +08:00
xiaoshi
2087327b69 Delete xiaoshi-device-balance-card.js 2025-12-06 19:42:21 +08:00
xiaoshi
7f415eba9a Update xiaoshi-pad-card.js 2025-12-01 10:11:58 +08:00
xiaoshi
4b7da24066 Update xiaoshi-device-consumables-button.js 2025-11-29 23:55:48 +08:00
xiaoshi
721b6a4ba2 Delete xiaoshi-device-consumables-button2.js 2025-11-29 23:55:37 +08:00
xiaoshi
b615d3dd7a Update xiaoshi-device-todo-card.js 2025-11-29 20:08:41 +08:00
xiaoshi
6f58598f54 Update xiaoshi-device-update-card.js 2025-11-29 00:07:04 +08:00
xiaoshi
f71e51029c Update xiaoshi-device-ha-info-card.js 2025-11-29 00:05:21 +08:00
xiaoshi
48f0c23456 Update xiaoshi-device-update-card.js 2025-11-28 23:20:14 +08:00
xiaoshi
910365f8b8 Update xiaoshi-device-ha-info-card.js 2025-11-28 23:20:04 +08:00
xiaoshi
81b40138f5 Update xiaoshi-pad-card.js 2025-11-28 21:50:16 +08:00
xiaoshi
4783085a99 Update xiaoshi-device-update-card.js 2025-11-28 21:49:39 +08:00
xiaoshi
737b174953 Update xiaoshi-device-todo-card.js 2025-11-28 21:49:28 +08:00
xiaoshi
d49426da73 Update xiaoshi-device-offline-card.js 2025-11-28 21:49:16 +08:00
xiaoshi
3bf33ef0cb Update xiaoshi-device-ha-info-card.js 2025-11-28 21:49:02 +08:00
xiaoshi
2c3c3fd42d Update xiaoshi-device-consumables-card.js 2025-11-28 21:48:49 +08:00
xiaoshi
4fe21a2c74 Update xiaoshi-device-consumables-button2.js 2025-11-28 21:48:35 +08:00
xiaoshi
3900897f15 Update xiaoshi-device-consumables-button.js 2025-11-28 21:48:19 +08:00
xiaoshi
2170f5423c Update xiaoshi-device-balance-card.js 2025-11-28 21:48:06 +08:00
xiaoshi
82e71cf38d Update xiaoshi-device-consumables-button.js 2025-11-28 20:49:17 +08:00
xiaoshi
c79052b1e1 Update xiaoshi-device-consumables-button2.js 2025-11-28 20:49:01 +08:00
xiaoshi
5ef261877c Update xiaoshi-device-consumables-card.js 2025-11-28 20:48:46 +08:00
xiaoshi
da16f27cf2 Update xiaoshi-pad-card.js 2025-11-28 12:15:39 +08:00
xiaoshi
df6a678043 Update xiaoshi-device-ha-info-card.js 2025-11-28 12:15:25 +08:00
xiaoshi
82930f4e41 Update xiaoshi-device-update-card.js 2025-11-28 12:15:06 +08:00
xiaoshi
4a2ffb628d Create xiaoshi-device-consumables-button2.js 2025-11-28 00:31:54 +08:00
xiaoshi
3d5f6028c2 Update xiaoshi-device-todo-card.js 2025-11-27 21:10:40 +08:00
xiaoshi
765b0a1367 Update README.md 2025-11-27 15:50:13 +08:00
xiaoshi
df66608d7f Update xiaoshi-pad-card.js 2025-11-27 15:47:56 +08:00
xiaoshi
6ed3bca2cc Create xiaoshi-device-consumables-button.js 2025-11-27 15:47:20 +08:00
xiaoshi
f1d0b52bd7 Update xiaoshi-pad-card.js 2025-11-27 15:46:51 +08:00
xiaoshi
528eaf1cd1 Create xiaoshi-device-ha-info-card.js 2025-11-27 15:46:02 +08:00
xiaoshi
59268a91f8 Update xiaoshi-device-consumables-card.js 2025-11-27 15:45:29 +08:00
xiaoshi
f768186379 Update xiaoshi-device-balance-card.js 2025-11-26 14:21:37 +08:00
xiaoshi
2371475eca Update xiaoshi-device-consumables-card.js 2025-11-26 14:18:03 +08:00
xiaoshi
befca5ece5 Update xiaoshi-pad-card.js 2025-11-26 14:13:44 +08:00
xiaoshi
24848db29b Update xiaoshi-device-consumables-card.js 2025-11-26 14:13:25 +08:00
xiaoshi
be232dc056 Update README.md 2025-11-26 13:06:57 +08:00
xiaoshi
96fb548262 Update README.md 2025-11-26 13:06:09 +08:00
xiaoshi
16831e5102 Update xiaoshi-pad-card.js 2025-11-26 13:04:30 +08:00
xiaoshi
b05f7de548 Update xiaoshi-device-consumables-card.js 2025-11-26 13:04:02 +08:00
xiaoshi
d8d4389549 Update xiaoshi-device-update-card.js 2025-11-26 12:07:35 +08:00
xiaoshi
bca500c945 Update xiaoshi-device-balance-card.js 2025-11-26 11:56:44 +08:00
12 changed files with 9063 additions and 1032 deletions

View File

@@ -24,19 +24,12 @@ max: 80 # 当前地区最大值
mode: 湿度 # 【温度】或者【湿度】
~~~
## 功能2HA版本更新卡(手机平板端通用)
## 功能2HA信息卡(手机平板端通用)
**引用示例**
~~~
type: custom:xiaoshi-update-card
width: 100%
theme: on
~~~
## 功能3HA离线设备卡(手机平板端通用)
**引用示例**
~~~
type: custom:xiaoshi-offline-card
width: 100%
skip_updates: false #是否包含已跳过的更新
theme: on
exclude_devices:
- *设备*
@@ -44,7 +37,7 @@ exclude_entities:
- *shiti*
~~~
## 功能4:电话信息余额卡(手机平板端通用)
## 功能3:电话信息余额卡(手机平板端通用)
**引用示例**
~~~
type: custom:xiaoshi-balance-card
@@ -68,7 +61,7 @@ entities:
warning: "99"
~~~
## 功能5:待办事项卡(手机平板端通用)
## 功能4:待办事项卡(手机平板端通用)
**引用示例**
~~~
type: custom:xiaoshi-todo-card
@@ -78,3 +71,29 @@ entities:
- todo.kuai_di
- todo.ji_shi_ben
~~~
## 功能5耗材信息卡片(手机平板端通用)
**引用示例**
~~~
type: custom:xiaoshi-consumables-card
width: 100%
global_warning: <8
columns: "2"
entities:
- entity_id: input_text.aaa
overrides:
name: 奥斯卡德拉萨达实打实实打实
unit_of_measurement: "%"
warning: <10
conversion: "*2"
icon: ""
- entity_id: input_text.aaa1
- entity_id: input_text.aaa2
- entity_id: input_text.aaa3
- entity_id: input_text.aaa4
- entity_id: input_text.aaa5
- entity_id: input_text.aaa6
- entity_id: input_text.aaa7
~~~

File diff suppressed because it is too large Load Diff

View File

@@ -245,9 +245,26 @@ class XiaoshiBalanceCardEditor extends LitElement {
name="global_warning"
placeholder="如: >10, <=5, ==on, ==off, =='hello world'"
/>
<div class="help-text">
全局预警条件:当任一实体满足此条件时,该实体显示为红色预警状态<br>
优先级:明细预警 > 全局预警 > 无预警
</div>
<div class="form-group">
<label>预警颜色:设置预警状态下的显示颜色</label>
<div style="display: flex; gap: 8px; align-items: center;">
<input
type="color"
@change=${this._entityChanged}
.value=${this.config.warning_color || '#f44336'}
name="warning_color"
style="width: 50px; height: 34px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;"
/>
<input
type="text"
@change=${this._entityChanged}
.value=${this.config.warning_color || '#f44336'}
name="warning_color"
placeholder="默认:#f44336"
style="flex: 1;"
/>
</div>
</div>
@@ -466,11 +483,12 @@ class XiaoshiBalanceCardEditor extends LitElement {
newEntities = currentEntities.filter(e => e.entity_id !== entityId);
} else {
// 添加实体
newEntities = [...currentEntities, {
entity_id: entityId,
attribute: null,
const newEntity = {
entity_id: entityId,
overrides: undefined
}];
};
// 只有在明确指定属性时才添加 attribute 字段
newEntities = [...currentEntities, newEntity];
}
this.config = {
@@ -510,10 +528,17 @@ class XiaoshiBalanceCardEditor extends LitElement {
const newEntities = [...currentEntities];
if (newEntities[index]) {
newEntities[index] = {
...newEntities[index],
attribute: attributeValue.trim() || null
};
const updatedEntity = { ...newEntities[index] };
if (attributeValue.trim()) {
// 只有当属性值不为空时才设置 attribute 字段
updatedEntity.attribute = attributeValue.trim();
} else {
// 如果属性值为空,则移除 attribute 字段
delete updatedEntity.attribute;
}
newEntities[index] = updatedEntity;
}
this.config = {
@@ -816,7 +841,7 @@ class XiaoshiBalanceCard extends LitElement {
}
.device-value.warning {
color: #F44336;
color: var(--warning-color, #F44336);
}
.device-unit {
@@ -827,7 +852,7 @@ class XiaoshiBalanceCard extends LitElement {
}
.device-unit.warning {
color: #F44336;
color: var(--warning-color, #F44336);
}
.no-devices {
@@ -967,12 +992,12 @@ class XiaoshiBalanceCard extends LitElement {
}
_handleRefresh() {
this._handleClick();
this._loadOilPriceData();
navigator.vibrate(50);
}
_handleEntityClick(entity) {
navigator.vibrate(50);
this._handleClick();
// 点击实体时打开实体详情页
if (entity.entity_id) {
const evt = new Event('hass-more-info', { composed: true });
@@ -981,6 +1006,17 @@ class XiaoshiBalanceCard extends LitElement {
}
}
_handleClick(){
if (navigator.vibrate) {
navigator.vibrate(50);
}
else if (navigator.webkitVibrate) {
navigator.webkitVibrate(50);
}
else {
}
}
_evaluateWarningCondition(value, condition) {
if (!condition) return false;
@@ -1040,7 +1076,7 @@ class XiaoshiBalanceCard extends LitElement {
const bgColor = theme === 'on' ? 'rgb(255, 255, 255)' : 'rgb(50, 50, 50)';
return html`
<ha-card style="--fg-color: ${fgColor}; --bg-color: ${bgColor};">
<ha-card style="--fg-color: ${fgColor}; --bg-color: ${bgColor}; --warning-color: ${this.config.warning_color || '#f44336'};">
<div class="card-header">
<div class="card-title">
<span class="offline-indicator" style="background: rgb(0,222,220); animation: pulse 2s infinite"></span>
@@ -1061,13 +1097,11 @@ class XiaoshiBalanceCard extends LitElement {
// 首先检查明细预警,如果存在且满足条件,直接设为预警状态
if (balanceData.warning_threshold && balanceData.warning_threshold.trim() !== '') {
isWarning = this._evaluateWarningCondition(balanceData.value, balanceData.warning_threshold);
console.log(`明细预警 - 实体: ${balanceData.friendly_name}, 值: "${balanceData.value}", 条件: "${balanceData.warning_threshold}", 预警: ${isWarning}`);
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);
console.log(`全局预警 - 实体: ${balanceData.friendly_name}, 值: "${balanceData.value}", 条件: "${this.config.global_warning}", 预警: ${isWarning}`);
}
}
@@ -1099,7 +1133,13 @@ class XiaoshiBalanceCard extends LitElement {
this.style.setProperty('--card-width', config.width);
}
// 设置主题
// 设置预警颜色
if (config.warning_color) {
this.style.setProperty('--warning-color', config.warning_color);
} else {
this.style.setProperty('--warning-color', '#f44336');
}
if (config.theme) {
this.setAttribute('theme', config.theme);
}

File diff suppressed because it is too large Load Diff

View File

@@ -74,7 +74,6 @@ class XiaoshiConsumablesCardEditor extends LitElement {
align-items: center;
justify-content: space-between;
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #eee;
}
@@ -247,7 +246,8 @@ class XiaoshiConsumablesCardEditor extends LitElement {
/>
<div class="help-text">
全局预警条件:当任一实体满足此条件时,该实体显示为红色预警状态<br>
优先级:明细预警 > 全局预警 > 无预警
优先级:明细预警 > 全局预警 > 无预警<br>
预警基于换算后的结果进行判断(如果配置了换算)
</div>
</div>
@@ -400,10 +400,33 @@ class XiaoshiConsumablesCardEditor extends LitElement {
class="override-input"
@change=${(e) => this._updateEntityOverrideValue(index, 'warning', e.target.value)}
.value=${entityConfig.overrides?.warning || ''}
placeholder="如: >10, <=5, ==on, ==off, =='hello world'"
placeholder='>10, <=5, ==on,=="hello world"'
?disabled=${entityConfig.overrides?.warning === undefined}
/>
</div>
<div class="override-config">
<input
type="checkbox"
class="override-checkbox"
@change=${(e) => this._updateEntityOverride(index, 'conversion', e.target.checked)}
.checked=${entityConfig.overrides?.conversion !== undefined}
/>
<span class="override-label">换算:</span>
<input
type="text"
class="override-input"
@change=${(e) => this._updateEntityOverrideValue(index, 'conversion', e.target.value)}
.value=${entityConfig.overrides?.conversion || ''}
placeholder="+10, -10, *1.5, /2"
?disabled=${entityConfig.overrides?.conversion === undefined}
/>
</div>
<div class="help-text">
<strong>预警:</strong>针对单个实体的预警条件,优先级高于全局预警<br>
<strong>换算:</strong>对原始数值进行数学运算,支持 +10, -10, *1.5, /2 等格式<br>
</div>
</div>
</div>
`;
@@ -417,7 +440,7 @@ class XiaoshiConsumablesCardEditor extends LitElement {
• 图标重定义:勾选后可自定义图标(如 mdi:phone<br>
• 单位重定义:勾选后可自定义单位(如 元、$、kWh 等)<br>
• 预警条件:勾选后设置预警条件,支持 >10, >=10, <10, <=10, ==10, ==on, ==off, =="hello world" 等<br>
未勾选重定义时,将使用实体的原始属性值
换算:对数值进行数学运算,支持 +10, -10, *1.5, /2 等<br>
</div>
</div>
</div>
@@ -426,6 +449,8 @@ class XiaoshiConsumablesCardEditor extends LitElement {
}
_entityChanged(e) {
const { name, value } = e.target;
if (!value && name !== 'theme' && name !== 'width' ) return;
@@ -476,7 +501,6 @@ class XiaoshiConsumablesCardEditor extends LitElement {
} else {
newEntities = [...currentEntities, {
entity_id: entityId,
attribute: null,
overrides: undefined
}];
}
@@ -518,10 +542,18 @@ class XiaoshiConsumablesCardEditor extends LitElement {
const newEntities = [...currentEntities];
if (newEntities[index]) {
newEntities[index] = {
...newEntities[index],
attribute: attributeValue.trim() || null
};
const trimmedValue = attributeValue.trim();
if (trimmedValue === '') {
// 如果属性为空,则从配置中移除 attribute 字段
const { attribute, ...entityWithoutAttribute } = newEntities[index];
newEntities[index] = entityWithoutAttribute;
} else {
// 如果属性不为空,则设置属性值
newEntities[index] = {
...newEntities[index],
attribute: trimmedValue
};
}
}
this.config = {
@@ -705,7 +737,6 @@ class XiaoshiConsumablesCard extends LitElement {
.device-count.non-zero {
background: rgba(255, 0, 0, 0.7);
color: #fff;
animation: pulse 2s infinite;
}
.device-count.zero {
@@ -771,11 +802,12 @@ class XiaoshiConsumablesCard extends LitElement {
align-items: center;
justify-content: space-between;
margin: 0px 16px;
padding: 6px 0;
padding: 0;
border-bottom: 1px solid rgb(150,150,150,0.5);
cursor: pointer;
transition: background-color 0.2s;
min-height: 32px;
min-height: 30px;
max-height: 30px;
}
.device-item:first-child {
@@ -798,8 +830,8 @@ class XiaoshiConsumablesCard extends LitElement {
.devices-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 1px;
padding: 0 16px;
gap: 0 15px;
padding: 0px 16px;
width: 100%;
box-sizing: border-box;
overflow: hidden;
@@ -815,15 +847,16 @@ class XiaoshiConsumablesCard extends LitElement {
/*2列布局中的设备项*/
.devices-grid .device-item {
margin: 0;
padding: 4px 12px;
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: 24px;
min-height: 30px;
max-height: 30px;
border-bottom: none;
border-right: none;
border-left: none;
@@ -831,6 +864,7 @@ class XiaoshiConsumablesCard extends LitElement {
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
border-bottom: 1px solid rgb(150,150,150,0.5);
}
.devices-grid .device-item:hover {
@@ -860,7 +894,7 @@ class XiaoshiConsumablesCard extends LitElement {
margin-right: 8px;
color: var(--fg-color, #000);
flex-shrink: 0;
font-size: 10px;
font-size: 11px;
width: 12px;
height: 12px;
display: flex;
@@ -870,7 +904,7 @@ class XiaoshiConsumablesCard extends LitElement {
.device-name {
color: var(--fg-color, #000);
font-size: 9px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -880,13 +914,12 @@ class XiaoshiConsumablesCard extends LitElement {
.device-value {
color: var(--fg-color, #000);
font-size: 9px;
font-size: 11px;
flex-shrink: 0;
font-weight: bold;
max-width: 45%;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -895,9 +928,9 @@ class XiaoshiConsumablesCard extends LitElement {
}
.device-unit {
font-size: 8px;
font-size: 11px;
color: var(--fg-color, #000);
margin-left: 2px;
margin-left: 0.5px;
font-weight: bold;
white-space: nowrap;
flex-shrink: 0;
@@ -995,6 +1028,26 @@ class XiaoshiConsumablesCard extends LitElement {
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;
@@ -1007,6 +1060,7 @@ class XiaoshiConsumablesCard extends LitElement {
let friendlyName = attributes.friendly_name || entityId;
let icon = attributes.icon || 'mdi:help-circle';
let warningThreshold = undefined;
let conversion = undefined;
// 应用用户自定义的重定义
if (entityConfig.overrides) {
@@ -1022,15 +1076,27 @@ class XiaoshiConsumablesCard extends LitElement {
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
warning_threshold: warningThreshold,
conversion: conversion
});
}
@@ -1044,12 +1110,12 @@ class XiaoshiConsumablesCard extends LitElement {
}
_handleRefresh() {
this._handleClick();
this._loadOilPriceData();
navigator.vibrate(50);
}
_handleEntityClick(entity) {
navigator.vibrate(50);
this._handleClick();
// 点击实体时打开实体详情页
if (entity.entity_id) {
const evt = new Event('hass-more-info', { composed: true });
@@ -1058,16 +1124,35 @@ class XiaoshiConsumablesCard extends LitElement {
}
}
_handleClick(){
if (navigator.vibrate) {
navigator.vibrate(50);
}
else if (navigator.webkitVibrate) {
navigator.webkitVibrate(50);
}
else {
}
}
_renderDeviceItem(consumablesData) {
let isWarning = false;
if (consumablesData.warning_threshold && consumablesData.warning_threshold.trim() !== '') {
isWarning = this._evaluateWarningCondition(consumablesData.value, consumablesData.warning_threshold);
console.log(`明细预警 - 实体: ${consumablesData.friendly_name}, 值: "${consumablesData.value}", 条件: "${consumablesData.warning_threshold}", 预警: ${isWarning}`);
// 特殊实体类型的默认预警逻辑
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 (this.config.global_warning && this.config.global_warning.trim() !== '') {
isWarning = this._evaluateWarningCondition(consumablesData.value, this.config.global_warning);
console.log(`全局预警 - 实体: ${consumablesData.friendly_name}, 值: "${consumablesData.value}", 条件: "${this.config.global_warning}", 预警: ${isWarning}`);
// 使用配置的预警条件
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);
}
}
}
@@ -1085,6 +1170,54 @@ class XiaoshiConsumablesCard extends LitElement {
`;
}
_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;
@@ -1128,7 +1261,6 @@ class XiaoshiConsumablesCard extends LitElement {
return false;
}
render() {
if (!this.hass) {
return html`<div class="loading">等待Home Assistant连接...</div>`;
@@ -1141,11 +1273,21 @@ class XiaoshiConsumablesCard extends LitElement {
const warningCount = this._oilPriceData.filter(consumablesData => {
let isWarning = false;
if (consumablesData.warning_threshold && consumablesData.warning_threshold.trim() !== '') {
isWarning = this._evaluateWarningCondition(consumablesData.value, consumablesData.warning_threshold);
// 对于 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 (this.config.global_warning && this.config.global_warning.trim() !== '') {
isWarning = this._evaluateWarningCondition(consumablesData.value, this.config.global_warning);
// 使用配置的预警条件
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);
}
}
}
@@ -1156,7 +1298,7 @@ class XiaoshiConsumablesCard extends LitElement {
<ha-card style="--fg-color: ${fgColor}; --bg-color: ${bgColor};">
<div class="card-header">
<div class="card-title">
<span class="offline-indicator" style="background: rgb(0,222,220); animation: pulse 2s infinite"></span>
<span class="offline-indicator" style="background: ${warningCount === 0 ? 'rgb(0,255,0)' : 'rgb(255,0,0)'}; animation: pulse 2s infinite"></span>
${this.config.name || '耗材信息统计'}
</div>
<div class="device-count ${warningCount > 0 ? 'non-zero' : 'zero'}">

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module";
class XiaoshiOfflineCardEditor extends LitElement {
class XiaoshiHaInfoCardEditor extends LitElement {
static get properties() {
return {
hass: { type: Object },
@@ -69,7 +69,20 @@ class XiaoshiOfflineCardEditor extends LitElement {
</div>
<div class="form-group">
<label>排除设备每行一个设备名称支持通配符(*)</label>
<label>
<input
type="checkbox"
@change=${this._entityChanged}
.checked=${this.config.skip_updates !== false}
name="skip_updates"
/>
包含已跳过的更新
</label>
<div class="help-text">如果勾选将包含标记为跳过的版本更新</div>
</div>
<div class="form-group">
<label>排除离线设备每行一个设备名称支持通配符(*)</label>
<textarea
@change=${this._entityChanged}
.value=${this.config.exclude_devices ? this.config.exclude_devices.join('\n') : ''}
@@ -82,7 +95,7 @@ class XiaoshiOfflineCardEditor extends LitElement {
</div>
<div class="form-group">
<label>过滤实体每行一个实体ID支持通配符(*)</label>
<label>排除离线实体每行一个实体ID支持通配符(*)</label>
<textarea
@change=${this._entityChanged}
.value=${this.config.exclude_entities ? this.config.exclude_entities.join('\n') : ''}
@@ -99,10 +112,17 @@ class XiaoshiOfflineCardEditor extends LitElement {
}
_entityChanged(e) {
const { name, value } = e.target;
if (!value && name !== 'theme' && name !== 'width' && name !== 'exclude_entities' && name !== 'exclude_devices' ) return;
const { name, value, type, checked } = e.target;
let finalValue = value;
let finalValue;
// 处理复选框
if (type === 'checkbox') {
finalValue = checked;
} else {
if (!value && name !== 'theme' && name !== 'width') return;
finalValue = value;
}
// 处理不同字段的默认值
if (name === 'width') {
@@ -131,13 +151,15 @@ class XiaoshiOfflineCardEditor extends LitElement {
this.config = config;
}
}
customElements.define('xiaoshi-offline-card-editor', XiaoshiOfflineCardEditor);
customElements.define('xiaoshi-ha-info-card-editor', XiaoshiHaInfoCardEditor);
export class XiaoshiOfflineCard extends LitElement {
class XiaoshiHaInfoCard extends LitElement {
static get properties() {
return {
hass: Object,
config: Object,
_haUpdates: Array,
_otherUpdates: Array,
_offlineDevices: Array,
_offlineEntities: Array,
_loading: Boolean,
@@ -169,7 +191,6 @@ export class XiaoshiOfflineCard extends LitElement {
align-items: center;
padding: 16px;
background: var(--bg-color, #fff);
border-radius: 12px;
}
@@ -199,7 +220,6 @@ export class XiaoshiOfflineCard extends LitElement {
display: flex;
align-items: center;
justify-content: center;
}
/*标题统计数字*/
@@ -281,7 +301,8 @@ export class XiaoshiOfflineCard extends LitElement {
align-items: center;
padding: 0px;
border-bottom: 1px solid rgb(150,150,150,0.2);
margin: 0 32px 0px 32px;
margin: 0 32px 4px 32px;
padding: 4px 0 0 0;
}
/*设备、实体明细背景*/
@@ -289,7 +310,7 @@ export class XiaoshiOfflineCard extends LitElement {
flex: 1;
overflow-y: auto;
min-height: 0;
padding: 0 0 8px 0;
padding: 4px 0;
}
.device-icon {
@@ -304,7 +325,7 @@ export class XiaoshiOfflineCard extends LitElement {
.device-name {
font-weight: 500;
color: var(--fg-color, #000);
padding: 6px 0 0 0;
margin: 2px 0;
}
.device-entity {
@@ -318,6 +339,22 @@ export class XiaoshiOfflineCard extends LitElement {
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);
@@ -326,20 +363,102 @@ export class XiaoshiOfflineCard extends LitElement {
.no-devices {
text-align: center;
padding: 8px 0 0 0;
padding: 8px 0;
color: var(--fg-color, #000);
}
.loading {
text-align: center;
padding: 0px;
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;
@@ -348,11 +467,12 @@ export class XiaoshiOfflineCard extends LitElement {
}
static getConfigElement() {
return document.createElement("xiaoshi-offline-card-editor");
return document.createElement("xiaoshi-ha-info-card-editor");
}
connectedCallback() {
super.connectedCallback();
this._loadUpdateData();
this._loadOfflineDevices();
// 设置主题属性
@@ -360,6 +480,7 @@ export class XiaoshiOfflineCard extends LitElement {
// 每300秒刷新一次数据减少频繁刷新
this._refreshInterval = setInterval(() => {
this._loadUpdateData();
this._loadOfflineDevices();
}, 300000);
}
@@ -645,6 +766,14 @@ export class XiaoshiOfflineCard extends LitElement {
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';
}
@@ -667,13 +796,13 @@ export class XiaoshiOfflineCard extends LitElement {
}
_handleRefresh() {
this._handleClick();
this._loadOfflineDevices();
navigator.vibrate(50);
}
_handleDeviceClick(device) {
navigator.vibrate(50);
// 点击设备时跳转到设备详情页
this._handleClick();
if (device.device_id) {
// 先关闭当前弹窗/界面
this._closeCurrentDialog();
@@ -696,7 +825,7 @@ export class XiaoshiOfflineCard extends LitElement {
}
_handleEntityClick(entity) {
navigator.vibrate(50);
this._handleClick();
// 点击实体时打开实体详情页
if (entity.entity_id) {
// 使用您建议的第一种方式
@@ -705,7 +834,18 @@ export class XiaoshiOfflineCard extends LitElement {
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');
@@ -751,6 +891,330 @@ export class XiaoshiOfflineCard extends LitElement {
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`
<div class="version-label">OS</div>
<div class="current-version ${osCurrentVersionClass}">当前版本${current}</div>
<div class="latest-version">最新版本${latest}</div>
`);
}
// 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`
<div class="version-label">Core</div>
<div class="current-version ${coreCurrentVersionClass}">当前版本${current}</div>
<div class="latest-version">最新版本${latest}</div>
`);
}
// 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`
<div class="version-label">Supervisor</div>
<div class="current-version ${supervisorCurrentVersionClass}">当前版本${current}</div>
<div class="latest-version">最新版本${latest}</div>
`);
}
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`
<div class="backup-label">HA上次备份</div>
<div class="backup-time" style="grid-column: 2 / -1;">${lastBackupCombined}</div>
`);
}
// 下次备份信息
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`
<div class="backup-label">HA下次备份</div>
<div class="backup-time" style="grid-column: 2 / -1;">${nextBackupCombined}</div>
`);
}
return html`${backupElements}`;
}
render() {
if (!this.hass) {
@@ -760,27 +1224,96 @@ export class XiaoshiOfflineCard extends LitElement {
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;
return html`
<ha-card style="--fg-color: ${fgColor}; --bg-color: ${bgColor};">
<div class="card-header">
<div class="card-title">
<span class="offline-indicator" style="background: ${this._offlineDevices.length + this._offlineEntities.length === 0 ? 'rgb(0,205,0)' : 'rgb(255,20,0)'}; animation: ${this._offlineDevices.length + this._offlineEntities.length === 0 ? 'none' : 'pulse 1s infinite'}"></span>
HA离线设备监控
</div>
<div style="display: flex; align-items: center; gap: 8px; ">
<span class="device-count ${this._offlineDevices.length + this._offlineEntities.length > 0 ? 'non-zero' : 'zero'}">
${this._offlineDevices.length + this._offlineEntities.length}
</span>
<button class="refresh-btn" style="background: ${this._offlineDevices.length + this._offlineEntities.length > 0 ? 'rgb(255,0,0,0.5)' : 'rgb(0,205,0)'}" @click=${this._handleRefresh}>
<span class="offline-indicator" style="background: ${warningCount=== 0 ? 'rgb(0,205,0)' : 'rgb(255,20,0)'}; animation: ${warningCount=== 0 ? 'none' : 'pulse 1s infinite'}"></span>
HA信息监控
</div>
<div style="display: flex; align-items: center; gap: 8px; ">
<span class="device-count ${warningCount> 0 ? 'non-zero' : 'zero'}">
${warningCount}
</span>
<button class="refresh-btn" style="background: ${warningCount> 0 ? 'rgb(255,0,0,0.5)' : 'rgb(0,205,0)'}" @click=${this._handleRefresh}>
刷新
</button>
</div>
</div>
<!-- HA版本信息 -->
<div class="section-divider">
<div class="section-title">
<span> HA版本信息</span>
</div>
</div>
<div class="ha-version-info">
${this._renderHAVersionInfo()}
</div>
<div class="devices-list">
${this._loading ?
html`<div class="loading">加载中...</div>` :
html`<div class="loading">HA版本信息加载中...</div>` :
(this._haUpdates.length === 0 && this._otherUpdates.length === 0) ?
html`<div class="no-devices">✅ 所有组件都是最新版本</div>` :
html`
${this._haUpdates.length > 0 ? html`
<div class="section-divider">
<div class="section-title">
<span> HA版本更新</span>
<span class="section-count ${this._haUpdates.length > 0 ? 'non-zero' : 'zero'}">${this._haUpdates.length}</span>
</div>
</div>
${this._haUpdates.map(update => html`
<div class="device-item" @click=${() => this._handleEntityClick(update)}>
<div class="device-icon">
<ha-icon icon="${update.icon}"></ha-icon>
</div>
<div class="device-info">
<div class="device-name">${update.name}</div>
<div class="device-details">
当前版本: ${update.current_version} 最新版本: ${update.latest_version}
${update.skipped_version ? html`<br><span style="color: #ff9800;">已跳过版本: ${update.skipped_version}</span>` : ''}
</div>
</div>
<div class="device-last-seen-update" @click=${(e) => this._handleConfirmUpdate(update, e)}>
立即更新
</div>
</div>
`)}\n ` : ''}
${this._otherUpdates.length > 0 ? html`
<div class="section-divider">
<div class="section-title">
<span> HACS更新</span>
<span class="section-count ${this._otherUpdates.length > 0 ? 'non-zero' : 'zero'}">${this._otherUpdates.length}</span>
</div>
</div>
${this._otherUpdates.map(update => html`
<div class="device-item" @click=${() => this._handleEntityClick(update)}>
<div class="device-icon">
<ha-icon icon="${update.icon}"></ha-icon>
</div>
<div class="device-info">
<div class="device-name">${update.name}</div>
<div class="device-details">
当前版本: ${update.current_version} 最新版本: ${update.latest_version}
${update.skipped_version ? html`<br><span style="color: #ff9800;">已跳过版本: ${update.skipped_version}</span>` : ''}
</div>
</div>
<div class="device-last-seen-update" @click=${(e) => this._handleConfirmUpdate(update, e)}>
立即更新
</div>
</div>
`)}\n ` : ''}
`
}
${this._loading ?
html`<div class="loading">设备和实体加载中...</div>` :
(this._offlineDevices.length === 0 && this._offlineEntities.length === 0) ?
html`<div class="no-devices">✅ 所有设备和实体都在线</div>` :
@@ -840,6 +1373,17 @@ export class XiaoshiOfflineCard extends LitElement {
`
}
</div>
<!-- 备份信息 -->
<div class="section-divider">
<div class="section-title">
<span> 备份信息</span>
</div>
</div>
<div class="backup-info">
${this._renderBackupInfo()}
</div>
</ha-card>
`;
}
@@ -859,14 +1403,16 @@ export class XiaoshiOfflineCard extends LitElement {
}
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 + deviceSize + entitySize;
return baseSize + haSize + otherSize + deviceSize + entitySize;
}
}
customElements.define('xiaoshi-offline-card', XiaoshiOfflineCard);
customElements.define('xiaoshi-ha-info-card', XiaoshiHaInfoCard);

File diff suppressed because it is too large Load Diff

View File

@@ -941,12 +941,12 @@ class XiaoshiTodoCard extends LitElement {
}
_handleRefresh() {
this._handleClick();
this._loadTodoData();
navigator.vibrate(50);
}
_handleEntityClick(entity) {
navigator.vibrate(50);
this._handleClick();
// 点击实体时打开实体详情页
if (entity.entity_id) {
const evt = new Event('hass-more-info', { composed: true });
@@ -955,6 +955,17 @@ class XiaoshiTodoCard extends LitElement {
}
}
_handleClick(){
if (navigator.vibrate) {
navigator.vibrate(50);
}
else if (navigator.webkitVibrate) {
navigator.webkitVibrate(50);
}
else {
}
}
async _addTodoItem(entityId, item, description = '', due = '') {
try {
const params = {
@@ -1082,7 +1093,25 @@ class XiaoshiTodoCard extends LitElement {
${todoData.items.length === 0 ?
html`<div class="no-devices">暂无待办事项</div>` :
html`
${todoData.items.map(item => {
${(() => {
// 将待办事项分为有时间和无时间两组
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;
@@ -1091,7 +1120,10 @@ class XiaoshiTodoCard extends LitElement {
<input
type="checkbox"
.checked=${item.status === 'completed'}
@change=${(e) => this._updateTodoItem(todoData.entity_id, item.summary || item.uid, e.target.checked ? 'completed' : 'needs_action')}
@change=${(e) => {
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`
@@ -1124,6 +1156,7 @@ class XiaoshiTodoCard extends LitElement {
@input=${(e) => {
this._editingItem.due = e.target.value;
this.requestUpdate();
this._handleClick();
}}
/>
<button
@@ -1132,6 +1165,7 @@ class XiaoshiTodoCard extends LitElement {
this._editTodoItem(todoData.entity_id, item.summary || item.uid, this._editingItem.summary, this._editingItem.description, this._editingItem.due);
this._editingItem = null;
this.requestUpdate();
this._handleClick();
}}
>
保存
@@ -1141,6 +1175,7 @@ class XiaoshiTodoCard extends LitElement {
@click=${() => {
this._editingItem = null;
this.requestUpdate();
this._handleClick();
}}
>
取消
@@ -1168,6 +1203,7 @@ class XiaoshiTodoCard extends LitElement {
due: this._formatDateForInput(item.due) || ''
};
this.requestUpdate();
this._handleClick();
}}
style="margin-left: 8px; margin-top: 2px;"
title="修改"
@@ -1177,7 +1213,10 @@ class XiaoshiTodoCard extends LitElement {
` : ''}
<button
class="remove-btn"
@click=${() => this._removeTodoItem(todoData.entity_id, item.summary || item.uid)}
@click=${() => {
this._removeTodoItem(todoData.entity_id, item.summary || item.uid);
this._handleClick();
}}
style="margin-left: 4px; margin-top: 2px;"
title="删除"
>
@@ -1185,7 +1224,8 @@ class XiaoshiTodoCard extends LitElement {
</button>
</div>
`;
})}
});
})()}
`
}
<div>
@@ -1197,6 +1237,7 @@ class XiaoshiTodoCard extends LitElement {
[todoData.entity_id]: !this._expandedAddForm[todoData.entity_id]
};
this.requestUpdate();
this._handleClick();
}}
>
${this._expandedAddForm[todoData.entity_id] ? '收起' : '添加新待办事项'}
@@ -1244,6 +1285,7 @@ class XiaoshiTodoCard extends LitElement {
descInput.value = '';
dateInput.value = '';
}
this._handleClick();
}}
>
添加

View File

@@ -1,904 +0,0 @@
import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module";
class XiaoshiUpdateCardEditor 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;
}
`;
}
render() {
if (!this.hass) return html``;
return html`
<div class="form">
<div class="form-group">
<label>卡片宽度:支持像素(px)和百分比(%)默认100%</label>
<input
type="text"
@change=${this._entityChanged}
.value=${this.config.width !== undefined ? this.config.width : '100%'}
name="width"
placeholder="默认100%"
/>
</div>
<div class="form-group">
<label>主题</label>
<select
@change=${this._entityChanged}
.value=${this.config.theme !== undefined ? this.config.theme : 'on'}
name="theme"
>
<option value="on">浅色主题(白底黑字)</option>
<option value="off">深色主题(深灰底白字)</option>
</select>
</div>
</div>
`;
}
_entityChanged(e) {
const { name, value } = e.target;
if (!value && name !== 'theme' && name !== 'width') return;
let finalValue = value;
// 处理不同字段的默认值
if (name === 'width') {
finalValue = value || '100%';
}
this.config = {
...this.config,
[name]: finalValue
};
this.dispatchEvent(new CustomEvent('config-changed', {
detail: { config: this.config },
bubbles: true,
composed: true
}));
}
setConfig(config) {
this.config = config;
}
}
customElements.define('xiaoshi-update-card-editor', XiaoshiUpdateCardEditor);
export class XiaoshiUpdateCard extends LitElement {
static get properties() {
return {
hass: Object,
config: Object,
_haUpdates: Array,
_otherUpdates: Array,
_loading: Boolean,
_refreshInterval: Number,
theme: { type: String }
};
}
static get styles() {
return css`
:host {
display: block;
width: var(--card-width, 100%);
}
ha-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: var(--bg-color, #fff);
border-radius: 12px;
}
/*标题容器*/
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: var(--bg-color, #fff);
border-radius: 12px;
}
/*标题红色圆点*/
.offline-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
/*标题红色圆点动画*/
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
/*标题*/
.card-title {
font-size: 20px;
font-weight: 500;
color: var(--fg-color, #000);
height: 30px;
line-height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
/*标题统计数字*/
.device-count {
color: var(--fg-color, #000);
border-radius: 8px;
font-size: 13px;
width: 30px;
height: 30px;
text-align: center;
line-height: 30px;
font-weight: bold;
padding: 0px;
}
.device-count.non-zero {
background: rgb(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 24px 8px 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;
}
.device-name {
font-weight: 500;
color: var(--fg-color, #000);
margin-bottom: 4px;
}
.device-entity {
font-size: 10px;
color: var(--fg-color, #000);
font-family: monospace;
}
.device-details {
font-size: 10px;
color: var(--fg-color, #000);
margin-top: 4px;
}
.device-last-seen {
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:hover {
background: rgb(255, 0, 0, 0.2);
}
.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);
}
/* 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 16px 0 30px;
border-bottom: 1px solid rgb(150,150,150,0.2);
display: grid;
grid-template-columns: auto auto auto;
gap: 4px;
align-items: center;
}
`;
}
constructor() {
super();
this._haUpdates = [];
this._otherUpdates = [];
this._loading = false;
this._refreshInterval = null;
this.theme = 'on';
}
static getConfigElement() {
return document.createElement("xiaoshi-update-card-editor");
}
connectedCallback() {
super.connectedCallback();
this._loadUpdateData();
// 设置主题属性
this.setAttribute('theme', this._evaluateTheme());
// 每300秒刷新一次数据减少频繁刷新
this._refreshInterval = setInterval(() => {
this._loadUpdateData();
}, 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 _loadUpdateData() {
if (!this.hass) return;
this._loading = true;
this.requestUpdate();
try {
const haUpdates = [];
const otherUpdates = [];
// 获取update.开头的实体更新信息
try {
const entities = Object.values(this.hass.states);
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) {
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 || ''
};
// 检查是否为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._loadUpdateData();
navigator.vibrate(50);
}
_handleUpdateClick(update) {
navigator.vibrate(50);
// 点击更新项时弹出实体详情
// 如果有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(); // 阻止默认行为
navigator.vibrate(50);
// 弹出确认对话框
const confirmed = confirm(`确认要更新 ${update.name} 吗?\n当前版本: ${update.current_version}\n最新版本: ${update.latest_version}`);
if (confirmed) {
this._executeUpdate(update);
}
}
_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`
<div class="version-label">OS</div>
<div class="current-version ${osCurrentVersionClass}">当前版本:${current}</div>
<div class="latest-version">最新版本:${latest}</div>
`);
}
// 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`
<div class="version-label">Core</div>
<div class="current-version ${coreCurrentVersionClass}">当前版本:${current}</div>
<div class="latest-version">最新版本:${latest}</div>
`);
}
// 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`
<div class="version-label">Supervisor</div>
<div class="current-version ${supervisorCurrentVersionClass}">当前版本:${current}</div>
<div class="latest-version">最新版本:${latest}</div>
`);
}
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`
<div class="backup-label">HA上次备份</div>
<div class="backup-time" style="grid-column: 2 / -1;">${lastBackupCombined}</div>
`);
}
// 下次备份信息
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`
<div class="backup-label">HA下次备份</div>
<div class="backup-time" style="grid-column: 2 / -1;">${nextBackupCombined}</div>
`);
}
return html`${backupElements}`;
}
render() {
if (!this.hass) {
return html`<div class="loading">等待Home Assistant连接...</div>`;
}
// 获取主题和颜色
const theme = this._evaluateTheme();
const fgColor = theme === 'on' ? 'rgb(0, 0, 0)' : 'rgb(255, 255, 255)';
const bgColor = theme === 'on' ? 'rgb(255, 255, 255)' : 'rgb(50, 50, 50)';
return html`
<ha-card style="--fg-color: ${fgColor}; --bg-color: ${bgColor};">
<div class="card-header">
<div class="card-title">
<span class="offline-indicator" style="background: ${this._haUpdates.length + this._otherUpdates.length === 0 ? 'rgb(0,205,0)' : 'rgb(255,20,0)'}; animation: ${this._haUpdates.length + this._otherUpdates.length === 0 ? 'none' : 'pulse 1s infinite'}"></span>
HA更新监控
</div>
<div style="display: flex; align-items: center; gap: 8px; ">
<span class="device-count ${this._haUpdates.length + this._otherUpdates.length > 0 ? 'non-zero' : 'zero'}">
${this._haUpdates.length + this._otherUpdates.length}
</span>
<button class="refresh-btn" style="background: ${this._haUpdates.length + this._otherUpdates.length > 0 ? 'rgb(255,0,0,0.5)' : 'rgb(0,205,0)'}" @click=${this._handleRefresh}>
刷新
</button>
</div>
</div>
<!-- HA版本信息 -->
<div class="section-divider">
<div class="section-title">
<span> • HA版本信息</span>
</div>
</div>
<div class="ha-version-info">
${this._renderHAVersionInfo()}
</div>
<!-- 备份信息 -->
<div class="section-divider">
<div class="section-title">
<span> • 备份信息</span>
</div>
</div>
<div class="backup-info">
${this._renderBackupInfo()}
</div>
<div class="devices-list">
${this._loading ?
html`<div class="loading">加载中...</div>` :
(this._haUpdates.length === 0 && this._otherUpdates.length === 0) ?
html`<div class="no-devices">✅ 所有组件都是最新版本</div>` :
html`
${this._haUpdates.length > 0 ? html`
<div class="section-divider">
<div class="section-title">
<span> • HA版本更新</span>
<span class="section-count ${this._haUpdates.length > 0 ? 'non-zero' : 'zero'}">${this._haUpdates.length}</span>
</div>
</div>
${this._haUpdates.map(update => html`
<div class="device-item">
<div class="device-icon">
<ha-icon icon="${update.icon}"></ha-icon>
</div>
<div class="device-info">
<div class="device-name">${update.name}</div>
<div class="device-details">
当前版本: ${update.current_version} → 最新版本: ${update.latest_version}
</div>
</div>
<div class="device-last-seen" @click=${(e) => this._handleConfirmUpdate(update, e)}>
立即更新
</div>
</div>
`)}\n ` : ''}
${this._otherUpdates.length > 0 ? html`
<div class="section-divider">
<div class="section-title">
<span> • 加载项、卡片更新</span>
<span class="section-count ${this._otherUpdates.length > 0 ? 'non-zero' : 'zero'}">${this._otherUpdates.length}</span>
</div>
</div>
${this._otherUpdates.map(update => html`
<div class="device-item">
<div class="device-icon">
<ha-icon icon="${update.icon}"></ha-icon>
</div>
<div class="device-info">
<div class="device-name">${update.name}</div>
<div class="device-details">
当前版本: ${update.current_version} → 最新版本: ${update.latest_version}
</div>
</div>
<div class="device-last-seen" @click=${(e) => this._handleConfirmUpdate(update, e)}>
立即更新
</div>
</div>
`)}\n ` : ''}
`
}
</div>
</ha-card>
`;
}
setConfig(config) {
this.config = config;
// 设置CSS变量来控制卡片的宽度和高度
if (config.width) {
this.style.setProperty('--card-width', config.width);
}
// 设置主题
if (config.theme) {
this.setAttribute('theme', config.theme);
}
}
getCardSize() {
// 根据更新项数量动态计算卡片大小
const baseSize = 4;
const haSize = Math.max(0, Math.min(this._haUpdates.length, 6));
const otherSize = Math.max(0, Math.min(this._otherUpdates.length, 8));
return baseSize + haSize + otherSize;
}
}
customElements.define('xiaoshi-update-card', XiaoshiUpdateCard);

View File

@@ -1,12 +1,15 @@
console.info("%c 消逝卡-平板端 \n%c v 0.0.9 ", "color: red; font-weight: bold; background: black", "color: white; font-weight: bold; background: black");
console.info("%c 消逝卡-平板端 \n%c v 0.2.5 ", "color: red; font-weight: bold; background: black", "color: white; font-weight: bold; background: black");
const loadCards = async () => {
await import('./xiaoshi-pad-grid-card.js');
await import('./xiaoshi-device-update-card.js');
await import('./xiaoshi-device-offline-card.js');
await import('./xiaoshi-device-balance-card.js');
await import('./xiaoshi-device-todo-card.js');
await import('./xiaoshi-device-consumables-card.js');
const loadCards = () => {
import('./xiaoshi-pad-grid-card.js');
import('./xiaoshi-device-balance-card.js');
import('./xiaoshi-device-balance-button.js');
import('./xiaoshi-device-todo-card.js');
import('./xiaoshi-device-todo-button.js');
import('./xiaoshi-device-consumables-card.js');
import('./xiaoshi-device-consumables-button.js');
import('./xiaoshi-device-ha-info-card.js');
import('./xiaoshi-device-ha-info-button.js');
window.customCards = window.customCards || [];
window.customCards.push(...cardConfigs);
@@ -19,33 +22,51 @@ const cardConfigs = [
description: '温度分布、湿度分布'
},
{
type: 'xiaoshi-update-card',
name: '消逝HA更新监控卡片',
description: '显示需要更新的组件和版本',
type: 'xiaoshi-ha-info-card',
name: '消逝HA信息卡片',
description: '消逝HA信息卡片',
preview: true
},
{
type: 'xiaoshi-offline-card',
name: '消逝HA离线设备卡片',
description: '显示所有离线的设备和实体',
type: 'xiaoshi-ha-info-button',
name: '消逝HA信息按钮',
description: '消逝HA信息按钮',
preview: true
},
{
type: 'xiaoshi-balance-card',
name: '消逝电话余额卡',
description: '消逝电话余额卡',
name: '消逝余额信息卡片',
description: '消逝余额信息卡片',
preview: true
},
{
type: 'xiaoshi-balance-button',
name: '消逝余额信息按钮',
description: '消逝余额信息按钮',
preview: true
},
{
type: 'xiaoshi-todo-card',
name: '消逝待办事项',
description: '消逝待办事项',
name: '消逝待办信息卡片',
description: '消逝待办信息卡片',
preview: true
},
{
type: 'xiaoshi-todo-button',
name: '消逝待办信息按钮',
description: '消逝待办信息按钮',
preview: true
},
{
type: 'xiaoshi-consumables-card',
name: '消逝耗材统计',
description: '消逝耗材统计',
name: '消逝耗材信息卡片',
description: '消逝耗材信息卡片',
preview: true
},
{
type: 'xiaoshi-consumables-button',
name: '消逝耗材信息按钮',
description: '消逝耗材信息按钮',
preview: true
}
];

View File

@@ -1,6 +1,6 @@
import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module";
export class XiaoshiPadGridCard extends LitElement {
class XiaoshiPadGridCard extends LitElement {
static get properties() {
return {
hass: Object,