Compare commits

..

62 Commits

Author SHA1 Message Date
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
xiaoshi
5f36835adb Update xiaoshi-pad-card.js 2025-11-26 01:00:55 +08:00
xiaoshi
05d083c259 Update README.md 2025-11-26 00:59:56 +08:00
xiaoshi
3738b0b092 Update xiaoshi-device-consumables-card.js 2025-11-25 19:51:37 +08:00
xiaoshi
87e9e68b79 Update xiaoshi-pad-card.js 2025-11-25 19:22:55 +08:00
xiaoshi
7ddd378b9a Create xiaoshi-device-consumables-card.js 2025-11-25 19:22:06 +08:00
xiaoshi
23b6e5b8a3 Update xiaoshi-device-balance-card.js 2025-11-25 19:15:23 +08:00
xiaoshi
4067518d0d Update xiaoshi-device-todo-card.js 2025-11-25 18:30:32 +08:00
xiaoshi
53e06aacf7 Update xiaoshi-device-todo-card.js 2025-11-25 13:35:29 +08:00
xiaoshi
9a59c2bb90 Create xiaoshi-device-todo-card.js 2025-11-25 13:34:43 +08:00
xiaoshi
0ae5851361 Update xiaoshi-pad-card.js 2025-11-25 13:34:07 +08:00
xiaoshi
a17033c0f0 Update README.md 2025-11-25 13:33:09 +08:00
11 changed files with 11483 additions and 983 deletions

View File

@@ -24,31 +24,26 @@ max: 80 # 当前地区最大值
mode: 湿度 # 【温度】或者【湿度】
~~~
## 功能2HA版本更新卡(手机平板端通用)
## 功能2HA信息卡(手机平板端通用)
**引用示例**
~~~
type: custom:xiaoshi-update-card
width: 100p%
width: 100%
skip_updates: false #是否包含已跳过的更新
theme: on
~~~
## 功能3HA离线设备卡(手机平板端通用)
**引用示例**
~~~
type: custom:xiaoshi-offline-card
width: 320px
exclude_devices:
- *设备*
exclude_entities:
- *shiti*
~~~
## 功能4:电话信息余额卡(手机平板端通用)
## 功能3:电话信息余额卡(手机平板端通用)
**引用示例**
~~~
type: custom:xiaoshi-balance-card
name: 电话余额信息
width: 350px
width: 100%
theme: on
entities:
- entity_id: sensor.999
attribute: null
@@ -65,3 +60,40 @@ entities:
unit_of_measurement: ""
warning: "99"
~~~
## 功能4待办事项卡(手机平板端通用)
**引用示例**
~~~
type: custom:xiaoshi-todo-card
width: 100%
theme: on
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

@@ -236,7 +236,37 @@ class XiaoshiBalanceCardEditor extends LitElement {
/>
</div>
<div class="form-group">
<label>全局预警条件:当任一实体满足此条件时触发预警</label>
<input
type="text"
@change=${this._entityChanged}
.value=${this.config.global_warning || ''}
name="global_warning"
placeholder="如: >10, <=5, ==on, ==off, =='hello world'"
/>
</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>
<div class="form-group">
<label>主题</label>
@@ -371,13 +401,11 @@ class XiaoshiBalanceCardEditor extends LitElement {
/>
<span class="override-label">预警:</span>
<input
type="number"
type="text"
class="override-input"
@change=${(e) => this._updateEntityOverrideValue(index, 'warning', e.target.value)}
.value=${entityConfig.overrides?.warning || ''}
placeholder="预警值"
min="0"
step="0.01"
placeholder="如: >10, <=5, ==on, ==off, =='hello world'"
?disabled=${entityConfig.overrides?.warning === undefined}
/>
</div>
@@ -393,7 +421,7 @@ class XiaoshiBalanceCardEditor extends LitElement {
• 名称重定义:勾选后可自定义显示名称<br>
• 图标重定义:勾选后可自定义图标(如 mdi:phone<br>
• 单位重定义:勾选后可自定义单位(如 元、$、kWh 等)<br>
• 预警:勾选后设置预警值,低于此值显示红色<br>
• 预警条件:勾选后设置预警条件,支持 >10, >=10, <10, <=10, ==10, ==on, ==off, =="hello world" 等<br>
• 未勾选重定义时,将使用实体的原始属性值
</div>
</div>
@@ -455,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 = {
@@ -499,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 = {
@@ -805,7 +841,7 @@ class XiaoshiBalanceCard extends LitElement {
}
.device-value.warning {
color: #F44336;
color: var(--warning-color, #F44336);
}
.device-unit {
@@ -816,7 +852,7 @@ class XiaoshiBalanceCard extends LitElement {
}
.device-unit.warning {
color: #F44336;
color: var(--warning-color, #F44336);
}
.no-devices {
@@ -932,7 +968,7 @@ class XiaoshiBalanceCard extends LitElement {
unit = entityConfig.overrides.unit_of_measurement;
}
if (entityConfig.overrides.warning !== undefined && entityConfig.overrides.warning !== '') {
warningThreshold = parseFloat(entityConfig.overrides.warning);
warningThreshold = entityConfig.overrides.warning; // 保持原始字符串
}
}
@@ -956,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 });
@@ -970,6 +1006,65 @@ 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;
// 解析条件字符串,支持操作符后可能有空格
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) {
@@ -981,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>
@@ -997,10 +1092,18 @@ class XiaoshiBalanceCard extends LitElement {
html`<div class="no-devices">请配置余额实体</div>` :
html`
${this._oilPriceData.map(balanceData => {
const numericValue = parseFloat(balanceData.value);
const isWarning = balanceData.warning_threshold !== undefined &&
!isNaN(numericValue) &&
numericValue < balanceData.warning_threshold;
// 明细预警优先级最高
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`
<div class="device-item" @click=${() => this._handleEntityClick(balanceData)}>
@@ -1030,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

File diff suppressed because it is too large Load Diff

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 {
export 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

1328
xiaoshi-device-todo-card.js Normal file

File diff suppressed because it is too large Load Diff

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,10 +1,15 @@
console.info("%c 消逝卡-平板端 \n%c v 0.0.7 ", "color: red; font-weight: bold; background: black", "color: white; font-weight: bold; background: black");
console.info("%c 消逝卡-平板端 \n%c v 0.1.7 ", "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-balance-button.js');
await import('./xiaoshi-device-todo-card.js');
await import('./xiaoshi-device-todo-button.js');
await import('./xiaoshi-device-consumables-card.js');
await import('./xiaoshi-device-consumables-button.js');
await import('./xiaoshi-device-ha-info-card.js');
await import('./xiaoshi-device-ha-info-button.js');
window.customCards = window.customCards || [];
window.customCards.push(...cardConfigs);
@@ -17,21 +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: '消逝待办信息卡片',
preview: true
},
{
type: 'xiaoshi-todo-button',
name: '消逝待办信息按钮',
description: '消逝待办信息按钮',
preview: true
},
{
type: 'xiaoshi-consumables-card',
name: '消逝耗材信息卡片',
description: '消逝耗材信息卡片',
preview: true
},
{
type: 'xiaoshi-consumables-button',
name: '消逝耗材信息按钮',
description: '消逝耗材信息按钮',
preview: true
}
];