node-red-contrib-symi-mesh 1.6.5 → 1.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +313 -329
- package/examples/knx-sync-example.json +48 -410
- package/lib/device-manager.js +30 -3
- package/lib/mqtt-helper.js +3 -3
- package/lib/tcp-client.js +33 -13
- package/nodes/symi-485-bridge.html +15 -3
- package/nodes/symi-485-bridge.js +747 -206
- package/nodes/symi-485-config.js +90 -5
- package/nodes/symi-device.js +7 -7
- package/nodes/symi-gateway.js +86 -14
- package/nodes/symi-knx-bridge.html +368 -0
- package/nodes/symi-knx-bridge.js +1065 -0
- package/nodes/symi-mqtt.js +74 -25
- package/package.json +5 -4
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('symi-knx-bridge', {
|
|
3
|
+
category: 'Symi Mesh',
|
|
4
|
+
color: '#88AA55',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
gateway: { value: '', type: 'symi-gateway', required: true },
|
|
8
|
+
mappings: { value: '[]' },
|
|
9
|
+
knxEntities: { value: '[]' }
|
|
10
|
+
},
|
|
11
|
+
inputs: 1,
|
|
12
|
+
outputs: 2,
|
|
13
|
+
outputLabels: ['KNX输出', '调试信息'],
|
|
14
|
+
icon: 'bridge.svg',
|
|
15
|
+
label: function() { return this.name || 'KNX Bridge'; },
|
|
16
|
+
paletteLabel: 'KNX桥接',
|
|
17
|
+
oneditprepare: function() {
|
|
18
|
+
const node = this;
|
|
19
|
+
let mappings = [], devices = [], knxEntities = [];
|
|
20
|
+
const typeLabels = {switch:'开关',light_mono:'单调',light_cct:'双调',light_rgb:'RGB',light_rgbcw:'RGBCW',cover:'窗帘',climate:'空调',fresh_air:'新风',floor_heating:'地暖'};
|
|
21
|
+
|
|
22
|
+
try { mappings = JSON.parse(node.mappings || '[]'); } catch(e) { mappings = []; }
|
|
23
|
+
try { knxEntities = JSON.parse(node.knxEntities || '[]'); } catch(e) { knxEntities = []; }
|
|
24
|
+
|
|
25
|
+
function loadDevices() {
|
|
26
|
+
const gid = $('#node-input-gateway').val();
|
|
27
|
+
if (!gid) { devices = []; renderMappings(); return; }
|
|
28
|
+
$.getJSON('symi-knx-bridge/devices/' + gid, function(data) {
|
|
29
|
+
devices = data || [];
|
|
30
|
+
renderMappings();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 下载模板
|
|
35
|
+
$('#download-tpl-btn').on('click', function() {
|
|
36
|
+
const tpl = `# KNX实体导入模板 (Tab分隔)
|
|
37
|
+
# 格式: 名称 类型 命令地址 状态地址 扩展1 扩展2 扩展3
|
|
38
|
+
# 类型: switch, light_mono, light_cct, light_rgb, light_rgbcw, cover, climate, fresh_air, floor_heating
|
|
39
|
+
#
|
|
40
|
+
# 开关 (命令, 状态)
|
|
41
|
+
玄关射灯 switch 1/1/28 1/2/28
|
|
42
|
+
客厅射灯 switch 1/1/25 1/2/25
|
|
43
|
+
# 单色调光 (开关, 状态, 亮度)
|
|
44
|
+
卧室筒灯 light_mono 1/1/1 1/2/1 1/3/1
|
|
45
|
+
# 双色调光 (开关, 状态, 亮度, 色温)
|
|
46
|
+
客厅吊灯 light_cct 1/1/10 1/2/10 1/3/10 1/4/10
|
|
47
|
+
# RGB (开关, 状态, 亮度, RGB)
|
|
48
|
+
氛围灯 light_rgb 1/1/20 1/2/20 1/3/20 1/5/20
|
|
49
|
+
# RGBCW (开关, 状态, 亮度, 色温, RGB)
|
|
50
|
+
主灯 light_rgbcw 1/1/30 1/2/30 1/3/30 1/4/30 1/5/30
|
|
51
|
+
# 窗帘 (上下, 位置, 停止)
|
|
52
|
+
客厅布帘 cover 2/1/5 2/2/5 2/3/5
|
|
53
|
+
# 空调 (开关, 温度, 模式, 风速, 当前温度)
|
|
54
|
+
主卧空调 climate 3/1/1 3/2/1 3/3/1 3/4/1 3/5/1
|
|
55
|
+
# 新风 (开关, 风速)
|
|
56
|
+
全屋新风 fresh_air 4/1/1 4/2/1
|
|
57
|
+
# 地暖 (开关, 温度, 当前温度)
|
|
58
|
+
客厅地暖 floor_heating 5/1/1 5/2/1 5/3/1`;
|
|
59
|
+
const blob = new Blob([tpl], {type:'text/plain;charset=utf-8'});
|
|
60
|
+
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
|
|
61
|
+
a.download = 'knx-template.txt'; a.click();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 渲染KNX实体表格
|
|
65
|
+
function renderKnxEntities() {
|
|
66
|
+
const c = $('#knx-list'); c.empty(); $('#knx-cnt').text(knxEntities.length);
|
|
67
|
+
if (!knxEntities.length) { c.html('<div class="tips">点击"导入"或"添加"管理KNX实体</div>'); return; }
|
|
68
|
+
let h = '<table class="tbl"><tr><th>名称</th><th>类型</th><th>命令</th><th>状态</th><th>扩展</th><th style="width:60px">操作</th></tr>';
|
|
69
|
+
knxEntities.forEach((e,i) => {
|
|
70
|
+
const ext = [e.ext1,e.ext2,e.ext3].filter(x=>x).join(',');
|
|
71
|
+
const inv = e.type==='cover' ? '<input type="checkbox" class="e-inv" title="位置反转"'+(e.invert?' checked':'')+'>' : '';
|
|
72
|
+
h += '<tr data-ei="'+i+'"><td>'+e.name+'</td><td>'+(typeLabels[e.type]||e.type)+inv+'</td><td>'+e.cmdAddr+'</td><td>'+(e.statusAddr||'-')+'</td><td>'+(ext||'-')+'</td><td><button class="red-ui-button red-ui-button-small e-edit" title="编辑"><i class="fa fa-pencil"></i></button> <button class="red-ui-button red-ui-button-small e-del" title="删除"><i class="fa fa-times"></i></button></td></tr>';
|
|
73
|
+
});
|
|
74
|
+
c.html(h+'</table>');
|
|
75
|
+
// 绑定实体事件
|
|
76
|
+
$('.e-inv').off('change').on('change', function() {
|
|
77
|
+
const ei = $(this).closest('tr').data('ei');
|
|
78
|
+
knxEntities[ei].invert = $(this).is(':checked');
|
|
79
|
+
saveKnxData();
|
|
80
|
+
});
|
|
81
|
+
$('.e-edit').off('click').on('click', function() {
|
|
82
|
+
const ei = $(this).closest('tr').data('ei');
|
|
83
|
+
editKnxEntity(ei);
|
|
84
|
+
});
|
|
85
|
+
$('.e-del').off('click').on('click', function() {
|
|
86
|
+
const ei = $(this).closest('tr').data('ei');
|
|
87
|
+
knxEntities.splice(ei, 1);
|
|
88
|
+
renderKnxEntities(); renderMappings();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 设备类型对应的字段配置
|
|
93
|
+
const typeFields = {
|
|
94
|
+
'switch': ['命令地址*','状态地址'],
|
|
95
|
+
'light_mono': ['开关地址*','状态地址','亮度地址'],
|
|
96
|
+
'light_cct': ['开关地址*','状态地址','亮度地址','色温地址'],
|
|
97
|
+
'light_rgb': ['开关地址*','状态地址','亮度地址','RGB地址'],
|
|
98
|
+
'light_rgbcw': ['开关地址*','状态地址','亮度地址','色温地址','RGB地址'],
|
|
99
|
+
'cover': ['上下地址*','位置地址','停止地址'],
|
|
100
|
+
'climate': ['开关地址*','温度地址','模式地址','风速地址','当前温度'],
|
|
101
|
+
'fresh_air': ['开关地址*','风速地址'],
|
|
102
|
+
'floor_heating': ['开关地址*','温度地址','当前温度']
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// 统一的添加/编辑KNX实体面板
|
|
106
|
+
let editingIndex = -1;
|
|
107
|
+
function showEntityPanel(index) {
|
|
108
|
+
const isEdit = index >= 0;
|
|
109
|
+
const e = isEdit ? knxEntities[index] : {name:'',type:'switch',cmdAddr:'',statusAddr:'',ext1:'',ext2:'',ext3:'',invert:false};
|
|
110
|
+
editingIndex = index;
|
|
111
|
+
const typeOpts = Object.keys(typeLabels).map(t => '<option value="'+t+'"'+(t===e.type?' selected':'')+'>'+typeLabels[t]+'</option>').join('');
|
|
112
|
+
const title = isEdit ? '编辑: '+e.name : '添加KNX实体';
|
|
113
|
+
const btnText = isEdit ? '保存' : '添加';
|
|
114
|
+
$('#edit-panel').html(
|
|
115
|
+
'<div class="edit-form"><h4>'+title+'</h4>'+
|
|
116
|
+
'<div class="form-row"><label>名称*</label><input type="text" id="edit-name" value="'+(e.name||'')+'" placeholder="如: 客厅灯"></div>'+
|
|
117
|
+
'<div class="form-row"><label>类型</label><select id="edit-type">'+typeOpts+'</select></div>'+
|
|
118
|
+
'<div class="form-row" id="row-cmd"><label id="lbl-cmd">命令地址*</label><input type="text" id="edit-cmd" value="'+(e.cmdAddr||'')+'"></div>'+
|
|
119
|
+
'<div class="form-row" id="row-status"><label id="lbl-status">状态地址</label><input type="text" id="edit-status" value="'+(e.statusAddr||'')+'"></div>'+
|
|
120
|
+
'<div class="form-row" id="row-ext1"><label id="lbl-ext1">扩展1</label><input type="text" id="edit-ext1" value="'+(e.ext1||'')+'"></div>'+
|
|
121
|
+
'<div class="form-row" id="row-ext2"><label id="lbl-ext2">扩展2</label><input type="text" id="edit-ext2" value="'+(e.ext2||'')+'"></div>'+
|
|
122
|
+
'<div class="form-row" id="row-ext3"><label id="lbl-ext3">扩展3</label><input type="text" id="edit-ext3" value="'+(e.ext3||'')+'"></div>'+
|
|
123
|
+
'<div class="form-row" id="row-inv" style="display:none"><label>位置反转</label><input type="checkbox" id="edit-inv"'+(e.invert?' checked':'')+'></div>'+
|
|
124
|
+
'<div class="form-row"><button id="save-edit" class="red-ui-button red-ui-button-small">'+btnText+'</button> <button id="cancel-edit" class="red-ui-button red-ui-button-small">取消</button></div>'+
|
|
125
|
+
'</div>'
|
|
126
|
+
).show();
|
|
127
|
+
|
|
128
|
+
function updateFieldLabels() {
|
|
129
|
+
const type = $('#edit-type').val();
|
|
130
|
+
const fields = typeFields[type] || ['命令地址*','状态地址'];
|
|
131
|
+
const rows = ['row-cmd','row-status','row-ext1','row-ext2','row-ext3'];
|
|
132
|
+
const labels = ['lbl-cmd','lbl-status','lbl-ext1','lbl-ext2','lbl-ext3'];
|
|
133
|
+
rows.forEach((row,i) => {
|
|
134
|
+
if (i < fields.length) {
|
|
135
|
+
$('#'+row).show();
|
|
136
|
+
$('#'+labels[i]).text(fields[i]);
|
|
137
|
+
} else {
|
|
138
|
+
$('#'+row).hide();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
$('#row-inv').toggle(type === 'cover');
|
|
142
|
+
}
|
|
143
|
+
updateFieldLabels();
|
|
144
|
+
$('#edit-type').on('change', updateFieldLabels);
|
|
145
|
+
|
|
146
|
+
$('#save-edit').on('click', function() {
|
|
147
|
+
const name = $('#edit-name').val().trim();
|
|
148
|
+
const cmd = $('#edit-cmd').val().trim();
|
|
149
|
+
if (!name || !cmd) { RED.notify('请填写名称和命令地址', 'warning'); return; }
|
|
150
|
+
const entity = {
|
|
151
|
+
id: isEdit ? e.id : 'k'+Date.now()+Math.random().toString(36).substr(2,4),
|
|
152
|
+
name: name,
|
|
153
|
+
type: $('#edit-type').val(),
|
|
154
|
+
cmdAddr: cmd,
|
|
155
|
+
statusAddr: $('#edit-status').val().trim(),
|
|
156
|
+
ext1: $('#edit-ext1').val().trim(),
|
|
157
|
+
ext2: $('#edit-ext2').val().trim(),
|
|
158
|
+
ext3: $('#edit-ext3').val().trim(),
|
|
159
|
+
invert: $('#edit-inv').is(':checked')
|
|
160
|
+
};
|
|
161
|
+
if (isEdit) { knxEntities[editingIndex] = entity; }
|
|
162
|
+
else { knxEntities.push(entity); }
|
|
163
|
+
$('#edit-panel').hide().empty();
|
|
164
|
+
renderKnxEntities(); renderMappings();
|
|
165
|
+
RED.notify(isEdit?'已更新':'已添加', 'success');
|
|
166
|
+
});
|
|
167
|
+
$('#cancel-edit').on('click', function() { $('#edit-panel').hide().empty(); });
|
|
168
|
+
}
|
|
169
|
+
function editKnxEntity(index) { showEntityPanel(index); }
|
|
170
|
+
|
|
171
|
+
// 渲染映射表格 - 一行显示
|
|
172
|
+
function renderMappings() {
|
|
173
|
+
const c = $('#map-list'); c.empty();
|
|
174
|
+
if (!mappings.length) { c.html('<div class="tips">点击"添加"创建映射</div>'); return; }
|
|
175
|
+
let h = '<table class="tbl"><tr><th style="width:24px">#</th><th style="width:38%">Mesh设备</th><th style="width:45px">通道</th><th style="width:45%">KNX实体</th><th style="width:32px">删除</th></tr>';
|
|
176
|
+
mappings.forEach((m, i) => {
|
|
177
|
+
const dev = devices.find(d => d.mac === m.meshMac);
|
|
178
|
+
h += '<tr data-i="'+i+'"><td>'+(i+1)+'</td>';
|
|
179
|
+
// Mesh设备选择
|
|
180
|
+
h += '<td><select class="m-dev"><option value="">--选择Mesh--</option>';
|
|
181
|
+
devices.forEach(d => {
|
|
182
|
+
const tl = d.type==='cover'?'窗帘':d.type==='switch'?d.channels+'键':d.type==='light'?'灯':d.type==='climate'?'温控':d.type;
|
|
183
|
+
h += '<option value="'+d.mac+'" data-ch="'+d.channels+'" data-t="'+d.type+'"'+(d.mac===m.meshMac?' selected':'')+'>'+d.name+'['+tl+']</option>';
|
|
184
|
+
});
|
|
185
|
+
h += '</select></td>';
|
|
186
|
+
// 通道
|
|
187
|
+
h += '<td><select class="m-ch">';
|
|
188
|
+
if (dev && dev.type === 'switch') {
|
|
189
|
+
for(let j=1; j<=dev.channels; j++) h += '<option value="'+j+'"'+(j===m.meshChannel?' selected':'')+'>'+j+'</option>';
|
|
190
|
+
} else { h += '<option value="1">-</option>'; }
|
|
191
|
+
h += '</select></td>';
|
|
192
|
+
// KNX实体选择
|
|
193
|
+
h += '<td><select class="m-knx"><option value="">--选择KNX--</option>';
|
|
194
|
+
knxEntities.forEach(e => {
|
|
195
|
+
const inv = e.invert ? '↕' : '';
|
|
196
|
+
h += '<option value="'+e.id+'"'+(e.id===m.knxEntityId?' selected':'')+'>'+e.name+'['+e.cmdAddr+']('+inv+(typeLabels[e.type]||'')+')</option>';
|
|
197
|
+
});
|
|
198
|
+
h += '</select></td>';
|
|
199
|
+
h += '<td><button class="red-ui-button red-ui-button-small m-del" title="删除"><i class="fa fa-times"></i></button></td></tr>';
|
|
200
|
+
});
|
|
201
|
+
c.html(h+'</table>');
|
|
202
|
+
bindEvents();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function bindEvents() {
|
|
206
|
+
$('.m-dev').off('change').on('change', function() {
|
|
207
|
+
const i = $(this).closest('tr').data('i');
|
|
208
|
+
mappings[i].meshMac = $(this).val();
|
|
209
|
+
mappings[i].meshChannel = 1;
|
|
210
|
+
renderMappings();
|
|
211
|
+
});
|
|
212
|
+
$('.m-ch').off('change').on('change', function() {
|
|
213
|
+
const i = $(this).closest('tr').data('i');
|
|
214
|
+
mappings[i].meshChannel = parseInt($(this).val()) || 1;
|
|
215
|
+
});
|
|
216
|
+
$('.m-knx').off('change').on('change', function() {
|
|
217
|
+
const i = $(this).closest('tr').data('i');
|
|
218
|
+
mappings[i].knxEntityId = $(this).val();
|
|
219
|
+
});
|
|
220
|
+
$('.m-del').off('click').on('click', function() {
|
|
221
|
+
mappings.splice($(this).closest('tr').data('i'), 1);
|
|
222
|
+
renderMappings();
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
$('#add-map-btn').on('click', function() {
|
|
227
|
+
mappings.push({ meshMac:'', meshChannel:1, knxEntityId:'' });
|
|
228
|
+
renderMappings();
|
|
229
|
+
});
|
|
230
|
+
$('#clear-map-btn').on('click', function() { if(confirm('清空映射?')) { mappings=[]; renderMappings(); } });
|
|
231
|
+
|
|
232
|
+
$('#import-btn').on('click', function() { $('#import-modal').show(); });
|
|
233
|
+
$('#import-cancel').on('click', function() { $('#import-modal').hide(); });
|
|
234
|
+
$('#import-confirm').on('click', function() {
|
|
235
|
+
const text = $('#import-input').val().trim();
|
|
236
|
+
if (!text) { $('#import-modal').hide(); return; }
|
|
237
|
+
let cnt = 0;
|
|
238
|
+
text.split('\n').forEach(line => {
|
|
239
|
+
line = line.trim();
|
|
240
|
+
if (!line || line.startsWith('#')) return;
|
|
241
|
+
const p = line.split(/\t+/);
|
|
242
|
+
if (p.length >= 2 && p[1] && /\d+\/\d+\/\d+/.test(p[2]||'')) {
|
|
243
|
+
const id = 'k' + Date.now() + Math.random().toString(36).substr(2,4);
|
|
244
|
+
knxEntities.push({ id, name:p[0].trim(), type:p[1].trim(), cmdAddr:p[2].trim(), statusAddr:(p[3]||'').trim(), ext1:(p[4]||'').trim(), ext2:(p[5]||'').trim(), ext3:(p[6]||'').trim(), invert:false });
|
|
245
|
+
cnt++;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
$('#import-modal').hide(); $('#import-input').val('');
|
|
249
|
+
renderKnxEntities(); renderMappings();
|
|
250
|
+
RED.notify('导入 '+cnt+' 个实体'+(cnt?'':'(需要有效组地址格式如1/2/3)'), cnt?'success':'warning');
|
|
251
|
+
});
|
|
252
|
+
$('#add-knx-btn').on('click', function() { showEntityPanel(-1); });
|
|
253
|
+
$('#clear-knx-btn').on('click', function() { if(confirm('清空KNX实体?')) { knxEntities=[]; renderKnxEntities(); renderMappings(); } });
|
|
254
|
+
|
|
255
|
+
// 保存KNX实体到隐藏字段
|
|
256
|
+
function saveKnxData() { $('#knx-data').val(JSON.stringify(knxEntities)); }
|
|
257
|
+
|
|
258
|
+
// 渲染后保存
|
|
259
|
+
const origRender = renderKnxEntities;
|
|
260
|
+
renderKnxEntities = function() { origRender(); saveKnxData(); };
|
|
261
|
+
|
|
262
|
+
$('#node-input-gateway').on('change', loadDevices);
|
|
263
|
+
setTimeout(function() { loadDevices(); renderKnxEntities(); }, 100);
|
|
264
|
+
},
|
|
265
|
+
oneditsave: function() {
|
|
266
|
+
const maps = [];
|
|
267
|
+
$('#map-list tr[data-i]').each(function() {
|
|
268
|
+
const m = { meshMac: $(this).find('.m-dev').val(), meshChannel: parseInt($(this).find('.m-ch').val())||1, knxEntityId: $(this).find('.m-knx').val() };
|
|
269
|
+
if (m.meshMac && m.knxEntityId) maps.push(m);
|
|
270
|
+
});
|
|
271
|
+
this.mappings = JSON.stringify(maps);
|
|
272
|
+
this.knxEntities = $('#knx-data').val() || '[]';
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
</script>
|
|
276
|
+
|
|
277
|
+
<script type="text/html" data-template-name="symi-knx-bridge">
|
|
278
|
+
<style>
|
|
279
|
+
#dialog-form { min-width: 950px; }
|
|
280
|
+
.tbl { width:100%; border-collapse:collapse; font-size:11px; }
|
|
281
|
+
.tbl th, .tbl td { padding:3px 5px; border:1px solid #ddd; }
|
|
282
|
+
.tbl th { background:#f0f0f0; }
|
|
283
|
+
.tbl select { width:100%; font-size:11px; padding:2px; }
|
|
284
|
+
.tbl input[type="checkbox"] { margin:0; }
|
|
285
|
+
#knx-list, #map-list { max-height:180px; overflow-y:auto; border:1px solid #ccc; margin:5px 0; padding:3px; }
|
|
286
|
+
.tips { color:#666; padding:8px; text-align:center; font-size:12px; }
|
|
287
|
+
.sec { display:flex; justify-content:space-between; align-items:center; margin:10px 0 4px; padding-bottom:4px; border-bottom:1px solid #ddd; }
|
|
288
|
+
.sec b { font-size:12px; }
|
|
289
|
+
.btns button { margin-left:4px; }
|
|
290
|
+
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:99999; }
|
|
291
|
+
.modal-box { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:#fff; padding:15px; border-radius:5px; box-shadow:0 4px 20px rgba(0,0,0,0.3); z-index:100000; }
|
|
292
|
+
#import-modal .modal-box { width:650px; }
|
|
293
|
+
#import-input { width:100%; height:160px; font-family:monospace; font-size:10px; }
|
|
294
|
+
.modal-box h4 { margin:0 0 10px; }
|
|
295
|
+
.modal-box .mbtns { text-align:right; margin-top:10px; }
|
|
296
|
+
.info { background:#f8f8e8; border:1px solid #e0e0c0; padding:5px 8px; margin:6px 0; font-size:11px; border-radius:3px; }
|
|
297
|
+
.edit-form h4 { margin:0 0 10px; color:#333; }
|
|
298
|
+
.edit-form .form-row { margin-bottom:8px; display:flex; align-items:center; }
|
|
299
|
+
.edit-form .form-row label { width:70px; font-size:12px; }
|
|
300
|
+
.edit-form .form-row input, .edit-form .form-row select { flex:1; padding:4px; font-size:12px; }
|
|
301
|
+
</style>
|
|
302
|
+
|
|
303
|
+
<div class="form-row">
|
|
304
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> 名称</label>
|
|
305
|
+
<input type="text" id="node-input-name" placeholder="KNX Bridge">
|
|
306
|
+
</div>
|
|
307
|
+
<div class="form-row">
|
|
308
|
+
<label for="node-input-gateway"><i class="fa fa-server"></i> Mesh网关</label>
|
|
309
|
+
<input type="text" id="node-input-gateway">
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div class="info"><b>连接:</b> <code>[knxUltimate-in] → [KNX桥接] → [knxUltimate-out]</code> | KNX IP网关由knx-ultimate配置(端口3671)</div>
|
|
313
|
+
|
|
314
|
+
<div class="sec">
|
|
315
|
+
<b><i class="fa fa-database"></i> KNX实体库 (<span id="knx-cnt">0</span>)</b>
|
|
316
|
+
<span class="btns">
|
|
317
|
+
<button type="button" class="red-ui-button red-ui-button-small" id="download-tpl-btn"><i class="fa fa-download"></i> 模板</button>
|
|
318
|
+
<button type="button" class="red-ui-button red-ui-button-small" id="import-btn"><i class="fa fa-upload"></i> 导入</button>
|
|
319
|
+
<button type="button" class="red-ui-button red-ui-button-small" id="add-knx-btn"><i class="fa fa-plus"></i> 添加</button>
|
|
320
|
+
<button type="button" class="red-ui-button red-ui-button-small" id="clear-knx-btn"><i class="fa fa-trash"></i></button>
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
<div id="knx-list"><div class="tips">点击"导入"或"添加"管理KNX实体</div></div>
|
|
324
|
+
<div id="edit-panel" style="display:none;background:#fffde7;border:1px solid #ffc107;padding:10px;margin:5px 0;border-radius:4px;"></div>
|
|
325
|
+
<input type="hidden" id="knx-data">
|
|
326
|
+
|
|
327
|
+
<div class="sec">
|
|
328
|
+
<b><i class="fa fa-exchange"></i> 设备映射</b>
|
|
329
|
+
<span class="btns">
|
|
330
|
+
<button type="button" class="red-ui-button red-ui-button-small" id="add-map-btn"><i class="fa fa-plus"></i> 添加</button>
|
|
331
|
+
<button type="button" class="red-ui-button red-ui-button-small" id="clear-map-btn"><i class="fa fa-trash"></i> 清空</button>
|
|
332
|
+
</span>
|
|
333
|
+
</div>
|
|
334
|
+
<div id="map-list"><div class="tips">添加Mesh↔KNX映射</div></div>
|
|
335
|
+
|
|
336
|
+
<div id="import-modal" class="modal-overlay">
|
|
337
|
+
<div class="modal-box">
|
|
338
|
+
<h4>导入KNX实体</h4>
|
|
339
|
+
<p style="font-size:11px;margin:0 0 8px">Tab分隔: <code>名称 类型 命令地址 状态地址 扩展...</code>(无效行自动忽略)</p>
|
|
340
|
+
<textarea id="import-input" placeholder="玄关射灯 switch 1/1/28 1/2/28
|
|
341
|
+
客厅吊灯 light_cct 1/1/10 1/2/10 1/3/10 1/4/10
|
|
342
|
+
客厅布帘 cover 2/1/5 2/2/5 2/3/5"></textarea>
|
|
343
|
+
<div class="mbtns">
|
|
344
|
+
<button type="button" class="red-ui-button" id="import-cancel">取消</button>
|
|
345
|
+
<button type="button" class="red-ui-button red-ui-button-primary" id="import-confirm">导入</button>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
</script>
|
|
351
|
+
|
|
352
|
+
<script type="text/html" data-help-name="symi-knx-bridge">
|
|
353
|
+
<p>Symi Mesh与KNX双向同步桥接</p>
|
|
354
|
+
<h3>使用步骤</h3>
|
|
355
|
+
<ol>
|
|
356
|
+
<li>点击"下载模板"获取KNX实体导入格式</li>
|
|
357
|
+
<li>填写后点击"导入"</li>
|
|
358
|
+
<li>点击"添加映射",选择Mesh设备和KNX实体</li>
|
|
359
|
+
</ol>
|
|
360
|
+
<h3>设备类型</h3>
|
|
361
|
+
<ul>
|
|
362
|
+
<li><b>switch</b> - 开关 (命令+状态)</li>
|
|
363
|
+
<li><b>light_mono/cct/rgb/rgbcw</b> - 调光灯</li>
|
|
364
|
+
<li><b>cover</b> - 窗帘 (上下+位置+停止)</li>
|
|
365
|
+
<li><b>climate</b> - 空调</li>
|
|
366
|
+
<li><b>fresh_air/floor_heating</b> - 新风/地暖</li>
|
|
367
|
+
</ul>
|
|
368
|
+
</script>
|