/**
* SigMesh Gateway 配网控制面板 - Lovelace 自定义卡片
*
* 使用方法:
* 1. 在 HACS 中安装本集成后,卡片会自动可用
* 2. 或者手动复制此文件到 HA 的 www/sigmesh_gateway/ 目录
* 3. 在 HA 的 configuration.yaml 中添加:
* frontend:
* extra_module_url:
* - /local/sigmesh_gateway/sigmesh-gateway-panel.js
* 4. 在 Lovelace Dashboard 中添加自定义卡片: custom:sigmesh-gateway-panel
*/
// 配置常量
const API_BASE = '/api/sigmesh_gateway';
const POLL_INTERVAL = 3000;
// 注册到全局自定义卡片列表
if (!window.customCards) {
window.customCards = [];
}
window.customCards.push({
type: 'sigmesh-gateway-panel',
name: 'SigMesh Gateway 配网管理',
description: '管理 SigMesh 网关的配网和分组功能',
preview: true,
documentationURL: 'https://github.com/impress-sig-mesh/sigmesh_gateway',
});
/**
* Lovelace 卡片组件
*/
class SigMeshGatewayCard extends HTMLElement {
static get placeholder() {
return {
type: 'custom:sigmesh-gateway-panel',
label: 'SigMesh Gateway 配网管理'
};
}
static getConfigElement() {
// 返回配置元素(如果需要配置界面)
return document.createElement('div');
}
static getStubConfig() {
return {};
}
constructor() {
super();
this._hass = null;
this._config = null;
this._state = 'idle';
this._devices = [];
this._groups = [];
this._pollTimer = null;
this._selectedDevice = null;
this._groupAddress = '0xC001';
}
set hass(hass) {
this._hass = hass;
if (!this.hasAttribute('rendered')) {
this._render();
this.setAttribute('rendered', 'true');
}
this._updateState();
}
setConfig(config) {
this._config = config;
}
getCardSize() {
return 10;
}
connectedCallback() {
this._startPolling();
}
disconnectedCallback() {
this._stopPolling();
}
_startPolling() {
this._fetchStatus();
this._pollTimer = setInterval(() => this._fetchStatus(), POLL_INTERVAL);
}
_stopPolling() {
if (this._pollTimer) {
clearInterval(this._pollTimer);
this._pollTimer = null;
}
}
async _fetchStatus() {
if (!this._hass) return;
try {
const response = await fetch(`${API_BASE}/status`, {
headers: {
'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
'Content-Type': 'application/json',
},
});
const data = await response.json();
this._state = data.state;
this._devices = data.devices || [];
this._groups = data.groups || [];
this._updateState();
} catch (error) {
console.error('获取状态失败:', error);
}
}
_updateState() {
if (!this.shadowRoot) return;
// 更新状态徽章
const statusBadge = this.shadowRoot.querySelector('.status-badge');
if (statusBadge) {
statusBadge.textContent = this._state;
statusBadge.style.background = this._getStateColor();
}
// 更新设备列表
this._renderDeviceList();
// 更新组列表
this._renderGroupList();
}
_getStateColor() {
const colors = {
idle: '#4caf50',
scanning: '#2196f3',
prov_starting: '#ff9800',
prov_in_progress: '#ff9800',
prov_completed: '#4caf50',
prov_failed: '#f44336',
timeout: '#f44336',
};
return colors[this._state] || '#757575';
}
_render() {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
${this._renderScanSection()}
${this._renderDeviceList()}
${this._renderProvActions()}
${this._renderGroupManagement()}
${this._renderGroupList()}
`;
this._attachEventListeners();
}
_renderScanSection() {
const isScanning = this._state === 'scanning';
return `
设备扫描
`;
}
_renderDeviceList() {
if (this._devices.length === 0) {
return `
`;
}
return `
已发现设备 (${this._devices.length})
${this._devices.map((device, index) => `
设备 ${device.mac}
元素数:${device.elements} |
地址:${device.address || '未分配'}
`).join('')}
`;
}
_renderProvActions() {
if (!this._selectedDevice && this._selectedDevice !== 0) {
return '';
}
const device = this._devices[this._selectedDevice];
const isProvInProgress = this._state === 'prov_in_progress' || this._state === 'prov_starting';
return `
配网操作 - ${device?.mac}
`;
}
_renderGroupManagement() {
if (!this._selectedDevice && this._selectedDevice !== 0) {
return '';
}
return `
`;
}
_renderGroupList() {
if (this._groups.length === 0) {
return `
`;
}
return `
组配置 (${this._groups.length})
${this._groups.map(group => `
组地址:${group.address}
元素:${group.element_address}
Model: ${group.model_id}
`).join('')}
`;
}
_renderDeviceList() {
const deviceListEl = this.shadowRoot.querySelector('.device-list');
if (!deviceListEl) return;
deviceListEl.innerHTML = this._devices.map((device, index) => `
设备 ${device.mac}
元素数:${device.elements} |
地址:${device.address || '未分配'}
`).join('');
}
_renderGroupList() {
const groupListEl = this.shadowRoot.querySelector('.group-list');
if (!groupListEl) return;
if (this._groups.length === 0) {
groupListEl.parentElement.querySelector('.empty-state')?.remove();
groupListEl.innerHTML = this._groups.map(group => `
组地址:${group.address}
元素:${group.element_address}
Model: ${group.model_id}
`).join('');
} else {
groupListEl.innerHTML = this._groups.map(group => `
组地址:${group.address}
元素:${group.element_address}
Model: ${group.model_id}
`).join('');
}
}
_attachEventListeners() {
if (!this.shadowRoot) return;
// 扫描按钮
this.shadowRoot.querySelector('#btn-scan')?.addEventListener('click', () => this._handleScan());
this.shadowRoot.querySelector('#btn-refresh')?.addEventListener('click', () => this._fetchStatus());
// 设备选择
this.shadowRoot.querySelectorAll('.device-item').forEach(item => {
item.addEventListener('click', () => {
const index = parseInt(item.dataset.index);
this._selectedDevice = index === this._selectedDevice ? null : index;
this._render();
});
});
// 配网按钮
this.shadowRoot.querySelector('#btn-prov-start')?.addEventListener('click', () => this._handleProvStart());
this.shadowRoot.querySelector('#btn-prov-stop')?.addEventListener('click', () => this._handleProvStop());
this.shadowRoot.querySelector('#btn-bind-key')?.addEventListener('click', () => this._handleBindKey());
// 分组按钮
this.shadowRoot.querySelector('#btn-add-group')?.addEventListener('click', () => this._handleAddGroup());
this.shadowRoot.querySelector('#btn-remove-group')?.addEventListener('click', () => this._handleRemoveGroup());
}
async _handleScan() {
if (!this._hass) return;
try {
await fetch(`${API_BASE}/scan`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
this._fetchStatus();
} catch (error) {
console.error('扫描失败:', error);
}
}
async _handleProvStart() {
if (!this._hass) return;
const deviceAddress = this.shadowRoot?.querySelector('#device-address')?.value;
try {
await fetch(`${API_BASE}/provisioning`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'start',
device_address: deviceAddress,
}),
});
this._fetchStatus();
} catch (error) {
console.error('配网失败:', error);
}
}
async _handleProvStop() {
if (!this._hass) return;
try {
await fetch(`${API_BASE}/provisioning`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ action: 'stop' }),
});
this._fetchStatus();
} catch (error) {
console.error('停止配网失败:', error);
}
}
async _handleBindKey() {
if (!this._hass) return;
const deviceAddress = this.shadowRoot?.querySelector('#device-address')?.value;
const elementAddress = 0;
try {
await fetch(`${API_BASE}/provisioning`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'bind',
device_address: deviceAddress,
element_address: elementAddress,
}),
});
alert('App Key 绑定成功');
this._fetchStatus();
} catch (error) {
console.error('绑定 App Key 失败:', error);
alert('绑定失败:' + error.message);
}
}
async _handleAddGroup() {
if (!this._hass) return;
const targetAddress = this.shadowRoot?.querySelector('#target-address')?.value;
const groupAddress = this.shadowRoot?.querySelector('#group-address')?.value;
const modelId = parseInt(this.shadowRoot?.querySelector('#model-id')?.value || '4352');
try {
await fetch(`${API_BASE}/group`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'add',
target_address: targetAddress,
group_address: groupAddress,
model_id: modelId,
is_sig: true,
}),
});
this._groupAddress = groupAddress;
this._fetchStatus();
} catch (error) {
console.error('添加分组失败:', error);
}
}
async _handleRemoveGroup() {
if (!this._hass) return;
const targetAddress = this.shadowRoot?.querySelector('#target-address')?.value;
const groupAddress = this.shadowRoot?.querySelector('#group-address')?.value;
const modelId = parseInt(this.shadowRoot?.querySelector('#model-id')?.value || '4352');
try {
await fetch(`${API_BASE}/group`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'remove',
target_address: targetAddress,
group_address: groupAddress,
model_id: modelId,
is_sig: true,
}),
});
this._fetchStatus();
} catch (error) {
console.error('移除分组失败:', error);
}
}
}
// 注册自定义元素
customElements.define('sigmesh-gateway-panel', SigMeshGatewayCard);
// 添加到 window 供调试
window.SigMeshGatewayCard = SigMeshGatewayCard;
console.log('SigMesh Gateway Panel 已加载');