homebridge-multiple-switch 1.4.1 → 1.5.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.0] - 2026-03-21
4
+
5
+ ### Added
6
+ - Multi-device support: create multiple separate HomeKit accessories, each with
7
+ its own name, switch behavior mode, and set of switches
8
+ - `devices` array in config — each device becomes a separate accessory in HomeKit
9
+ - Full backward compatibility: existing configs with `switches` at root level
10
+ continue to work and are auto-migrated to the new `devices` format in the UI
11
+
3
12
  ## [1.4.1] - 2026-03-21
4
13
 
5
14
  ### Fixed
@@ -12,65 +12,73 @@
12
12
  "type": "string",
13
13
  "default": "Multiple Switch Platform"
14
14
  },
15
- "switchBehavior": {
16
- "title": "Switch Behavior Mode",
17
- "description": "Controls how switches interact with each other.",
18
- "type": "string",
19
- "default": "independent",
20
- "oneOf": [
21
- { "title": "Independent", "enum": ["independent"] },
22
- { "title": "Master", "enum": ["master"] },
23
- { "title": "Single", "enum": ["single"] }
24
- ]
25
- },
26
- "switches": {
27
- "title": "Switches",
28
- "description": "List of virtual switches to create.",
15
+ "devices": {
16
+ "title": "Devices",
17
+ "description": "List of switch devices. Each device appears as a separate accessory in HomeKit.",
29
18
  "type": "array",
30
19
  "items": {
31
20
  "type": "object",
32
21
  "properties": {
33
22
  "name": {
34
- "title": "Switch Name",
35
- "description": "Display name of the switch in HomeKit.",
23
+ "title": "Device Name",
24
+ "description": "Display name of this device in HomeKit.",
36
25
  "type": "string"
37
26
  },
38
- "type": {
39
- "title": "Switch Type",
40
- "description": "The HomeKit accessory type for this switch.",
27
+ "switchBehavior": {
28
+ "title": "Switch Behavior Mode",
29
+ "description": "Controls how switches interact with each other within this device.",
41
30
  "type": "string",
42
- "default": "outlet",
31
+ "default": "independent",
43
32
  "oneOf": [
44
- { "title": "Switch", "enum": ["switch"] },
45
- { "title": "Outlet", "enum": ["outlet"] }
33
+ { "title": "Independent", "enum": ["independent"] },
34
+ { "title": "Master", "enum": ["master"] },
35
+ { "title": "Single", "enum": ["single"] }
46
36
  ]
47
37
  },
48
- "defaultState": {
49
- "title": "Default State",
50
- "description": "Initial power state when Homebridge starts.",
51
- "type": "boolean",
52
- "default": false
53
- },
54
- "delayOff": {
55
- "title": "Auto Turn Off (ms)",
56
- "description": "Automatically turn off after this many milliseconds. Set to 0 to disable.",
57
- "type": "number",
58
- "default": 0,
59
- "minimum": 0
38
+ "switches": {
39
+ "title": "Switches",
40
+ "description": "List of virtual switches in this device.",
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "properties": {
45
+ "name": {
46
+ "title": "Switch Name",
47
+ "description": "Display name of the switch in HomeKit.",
48
+ "type": "string"
49
+ },
50
+ "type": {
51
+ "title": "Switch Type",
52
+ "description": "The HomeKit accessory type for this switch.",
53
+ "type": "string",
54
+ "default": "outlet",
55
+ "oneOf": [
56
+ { "title": "Switch", "enum": ["switch"] },
57
+ { "title": "Outlet", "enum": ["outlet"] }
58
+ ]
59
+ },
60
+ "defaultState": {
61
+ "title": "Default State",
62
+ "description": "Initial power state when Homebridge starts.",
63
+ "type": "boolean",
64
+ "default": false
65
+ },
66
+ "delayOff": {
67
+ "title": "Auto Turn Off (ms)",
68
+ "description": "Automatically turn off after this many milliseconds. Set to 0 to disable.",
69
+ "type": "number",
70
+ "default": 0,
71
+ "minimum": 0
72
+ }
73
+ },
74
+ "required": ["name", "type"]
75
+ }
60
76
  }
61
77
  },
62
- "required": ["name", "type"]
63
- },
64
- "default": [
65
- {
66
- "name": "Switch 1",
67
- "type": "outlet",
68
- "defaultState": false,
69
- "delayOff": 0
70
- }
71
- ]
78
+ "required": ["name", "switches"]
79
+ }
72
80
  }
73
81
  },
74
- "required": ["name", "switches"]
82
+ "required": ["name"]
75
83
  }
76
84
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "إزالة",
24
24
  "save": "حفظ",
25
25
  "on": "تشغيل",
26
- "off": "إيقاف"
26
+ "off": "إيقاف",
27
+ "devices": "الأجهزة",
28
+ "devicesDesc": "يظهر كل جهاز كملحق منفصل في HomeKit مع مفاتيحه الخاصة.",
29
+ "device": "جهاز",
30
+ "deviceName": "اسم الجهاز",
31
+ "addDevice": "إضافة جهاز",
32
+ "removeDevice": "إزالة الجهاز"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "Entfernen",
24
24
  "save": "Speichern",
25
25
  "on": "Ein",
26
- "off": "Aus"
26
+ "off": "Aus",
27
+ "devices": "Geräte",
28
+ "devicesDesc": "Jedes Gerät erscheint als separates Zubehör in HomeKit mit eigenen Schaltern.",
29
+ "device": "Gerät",
30
+ "deviceName": "Gerätename",
31
+ "addDevice": "Gerät hinzufügen",
32
+ "removeDevice": "Gerät entfernen"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "Remove",
24
24
  "save": "Save",
25
25
  "on": "On",
26
- "off": "Off"
26
+ "off": "Off",
27
+ "devices": "Devices",
28
+ "devicesDesc": "Each device appears as a separate accessory in HomeKit with its own switches.",
29
+ "device": "Device",
30
+ "deviceName": "Device Name",
31
+ "addDevice": "Add Device",
32
+ "removeDevice": "Remove Device"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "Eliminar",
24
24
  "save": "Guardar",
25
25
  "on": "Encendido",
26
- "off": "Apagado"
26
+ "off": "Apagado",
27
+ "devices": "Dispositivos",
28
+ "devicesDesc": "Cada dispositivo aparece como un accesorio separado en HomeKit con sus propios interruptores.",
29
+ "device": "Dispositivo",
30
+ "deviceName": "Nombre del dispositivo",
31
+ "addDevice": "Agregar dispositivo",
32
+ "removeDevice": "Eliminar dispositivo"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "Supprimer",
24
24
  "save": "Enregistrer",
25
25
  "on": "Activé",
26
- "off": "Désactivé"
26
+ "off": "Désactivé",
27
+ "devices": "Appareils",
28
+ "devicesDesc": "Chaque appareil apparaît comme un accessoire séparé dans HomeKit avec ses propres interrupteurs.",
29
+ "device": "Appareil",
30
+ "deviceName": "Nom de l'appareil",
31
+ "addDevice": "Ajouter un appareil",
32
+ "removeDevice": "Supprimer l'appareil"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "Rimuovi",
24
24
  "save": "Salva",
25
25
  "on": "Acceso",
26
- "off": "Spento"
26
+ "off": "Spento",
27
+ "devices": "Dispositivi",
28
+ "devicesDesc": "Ogni dispositivo appare come un accessorio separato in HomeKit con i propri interruttori.",
29
+ "device": "Dispositivo",
30
+ "deviceName": "Nome del dispositivo",
31
+ "addDevice": "Aggiungi dispositivo",
32
+ "removeDevice": "Rimuovi dispositivo"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "削除",
24
24
  "save": "保存",
25
25
  "on": "オン",
26
- "off": "オフ"
26
+ "off": "オフ",
27
+ "devices": "デバイス",
28
+ "devicesDesc": "各デバイスは HomeKit で独自のスイッチを持つ個別のアクセサリとして表示されます。",
29
+ "device": "デバイス",
30
+ "deviceName": "デバイス名",
31
+ "addDevice": "デバイスを追加",
32
+ "removeDevice": "デバイスを削除"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "삭제",
24
24
  "save": "저장",
25
25
  "on": "켜짐",
26
- "off": "꺼짐"
26
+ "off": "꺼짐",
27
+ "devices": "장치",
28
+ "devicesDesc": "각 장치는 HomeKit에서 자체 스위치를 가진 별도의 액세서리로 표시됩니다.",
29
+ "device": "장치",
30
+ "deviceName": "장치 이름",
31
+ "addDevice": "장치 추가",
32
+ "removeDevice": "장치 삭제"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "Verwijderen",
24
24
  "save": "Opslaan",
25
25
  "on": "Aan",
26
- "off": "Uit"
26
+ "off": "Uit",
27
+ "devices": "Apparaten",
28
+ "devicesDesc": "Elk apparaat verschijnt als een apart accessoire in HomeKit met zijn eigen schakelaars.",
29
+ "device": "Apparaat",
30
+ "deviceName": "Apparaatnaam",
31
+ "addDevice": "Apparaat toevoegen",
32
+ "removeDevice": "Apparaat verwijderen"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "Usuń",
24
24
  "save": "Zapisz",
25
25
  "on": "Wł",
26
- "off": "Wył"
26
+ "off": "Wył",
27
+ "devices": "Urządzenia",
28
+ "devicesDesc": "Każde urządzenie pojawia się jako osobne akcesorium w HomeKit z własnymi przełącznikami.",
29
+ "device": "Urządzenie",
30
+ "deviceName": "Nazwa urządzenia",
31
+ "addDevice": "Dodaj urządzenie",
32
+ "removeDevice": "Usuń urządzenie"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "Remover",
24
24
  "save": "Salvar",
25
25
  "on": "Ligado",
26
- "off": "Desligado"
26
+ "off": "Desligado",
27
+ "devices": "Dispositivos",
28
+ "devicesDesc": "Cada dispositivo aparece como um acessório separado no HomeKit com seus próprios interruptores.",
29
+ "device": "Dispositivo",
30
+ "deviceName": "Nome do dispositivo",
31
+ "addDevice": "Adicionar dispositivo",
32
+ "removeDevice": "Remover dispositivo"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "Удалить",
24
24
  "save": "Сохранить",
25
25
  "on": "Вкл",
26
- "off": "Выкл"
26
+ "off": "Выкл",
27
+ "devices": "Устройства",
28
+ "devicesDesc": "Каждое устройство отображается как отдельный аксессуар в HomeKit со своими переключателями.",
29
+ "device": "Устройство",
30
+ "deviceName": "Название устройства",
31
+ "addDevice": "Добавить устройство",
32
+ "removeDevice": "Удалить устройство"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "Kaldır",
24
24
  "save": "Kaydet",
25
25
  "on": "Açık",
26
- "off": "Kapalı"
26
+ "off": "Kapalı",
27
+ "devices": "Cihazlar",
28
+ "devicesDesc": "Her cihaz HomeKit'te kendi anahtarlarıyla ayrı bir aksesuar olarak görünür.",
29
+ "device": "Cihaz",
30
+ "deviceName": "Cihaz Adı",
31
+ "addDevice": "Cihaz Ekle",
32
+ "removeDevice": "Cihazı Kaldır"
27
33
  }
@@ -23,5 +23,11 @@
23
23
  "removeSwitch": "删除",
24
24
  "save": "保存",
25
25
  "on": "开",
26
- "off": "关"
26
+ "off": "关",
27
+ "devices": "设备",
28
+ "devicesDesc": "每个设备在 HomeKit 中显示为一个独立的配件,拥有自己的开关。",
29
+ "device": "设备",
30
+ "deviceName": "设备名称",
31
+ "addDevice": "添加设备",
32
+ "removeDevice": "删除设备"
27
33
  }
@@ -7,6 +7,8 @@
7
7
  --ui-card-bg: #f8f9fa;
8
8
  --ui-input-bg: #fff;
9
9
  --ui-accent: #007bff;
10
+ --ui-device-bg: rgba(0,123,255,0.04);
11
+ --ui-device-border: rgba(0,123,255,0.2);
10
12
  }
11
13
  @media (prefers-color-scheme: dark) {
12
14
  :root {
@@ -17,6 +19,8 @@
17
19
  --ui-card-bg: rgba(255,255,255,0.06);
18
20
  --ui-input-bg: rgba(255,255,255,0.08);
19
21
  --ui-accent: #7b61ff;
22
+ --ui-device-bg: rgba(123,97,255,0.06);
23
+ --ui-device-border: rgba(123,97,255,0.25);
20
24
  }
21
25
  }
22
26
  * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -46,6 +50,19 @@
46
50
  border-color: var(--ui-accent);
47
51
  outline: none;
48
52
  }
53
+ .device-card {
54
+ border: 2px solid var(--ui-device-border);
55
+ border-radius: 10px; padding: 20px; margin-bottom: 20px;
56
+ background: var(--ui-device-bg);
57
+ }
58
+ .device-header {
59
+ display: flex; justify-content: space-between; align-items: center;
60
+ margin-bottom: 16px; padding-bottom: 10px;
61
+ border-bottom: 1px solid var(--ui-border);
62
+ }
63
+ .device-header strong {
64
+ font-size: 16px; color: var(--ui-accent);
65
+ }
49
66
  .switch-card {
50
67
  border: 1px solid var(--ui-border);
51
68
  border-radius: 8px; padding: 16px; margin-bottom: 12px;
@@ -67,6 +84,11 @@
67
84
  .btn-danger:hover { background: #c82333; }
68
85
  .btn-primary { background: var(--ui-accent); color: #fff; }
69
86
  .btn-primary:hover { opacity: 0.85; }
87
+ .btn-outline {
88
+ background: transparent; color: var(--ui-accent);
89
+ border: 1px solid var(--ui-accent);
90
+ }
91
+ .btn-outline:hover { background: var(--ui-accent); color: #fff; }
70
92
  .actions { margin-top: 16px; display: flex; gap: 10px; }
71
93
  .toggle-wrap { display: flex; align-items: center; gap: 8px; }
72
94
  .toggle-wrap input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--ui-accent); }
@@ -113,10 +135,24 @@
113
135
  config = configs[0] || {};
114
136
  } catch {}
115
137
 
138
+ // Migrate old format (switches at root) to devices array
139
+ if (Array.isArray(config.switches) && config.switches.length > 0 && !config.devices) {
140
+ config.devices = [{
141
+ name: config.name || 'Multiple Switch Panel',
142
+ switchBehavior: config.switchBehavior || 'independent',
143
+ switches: config.switches,
144
+ }];
145
+ delete config.switches;
146
+ delete config.switchBehavior;
147
+ }
148
+
149
+ if (!config.devices) {
150
+ config.devices = [];
151
+ }
152
+
116
153
  const app = document.getElementById('app');
117
154
 
118
155
  function render() {
119
- const switches = config.switches || [];
120
156
  app.innerHTML = `
121
157
  <div class="form-group">
122
158
  <label>${t.platformName || 'Platform Name'}</label>
@@ -124,25 +160,15 @@
124
160
  <input type="text" id="cfg-name" value="${esc(config.name || 'Multiple Switch Platform')}">
125
161
  </div>
126
162
 
127
- <div class="form-group">
128
- <label>${t.switchBehavior || 'Switch Behavior Mode'}</label>
129
- <div class="desc">${t.switchBehaviorDesc || ''}</div>
130
- <select id="cfg-behavior">
131
- <option value="independent" ${config.switchBehavior === 'independent' || !config.switchBehavior ? 'selected' : ''}>${t.behaviorIndependent || 'Independent'}</option>
132
- <option value="master" ${config.switchBehavior === 'master' ? 'selected' : ''}>${t.behaviorMaster || 'Master'}</option>
133
- <option value="single" ${config.switchBehavior === 'single' ? 'selected' : ''}>${t.behaviorSingle || 'Single'}</option>
134
- </select>
135
- </div>
136
-
137
- <div class="section-title">${t.switches || 'Switches'}</div>
138
- <div class="desc" style="margin-bottom:12px">${t.switchesDesc || ''}</div>
163
+ <div class="section-title">${t.devices || 'Devices'}</div>
164
+ <div class="desc" style="margin-bottom:16px">${t.devicesDesc || ''}</div>
139
165
 
140
- <div id="switches-list">
141
- ${switches.map((sw, i) => renderSwitch(sw, i)).join('')}
166
+ <div id="devices-list">
167
+ ${config.devices.map((dev, di) => renderDevice(dev, di)).join('')}
142
168
  </div>
143
169
 
144
170
  <div class="actions">
145
- <button class="btn btn-primary" id="btn-add">+ ${t.addSwitch || 'Add Switch'}</button>
171
+ <button class="btn btn-outline" id="btn-add-device">+ ${t.addDevice || 'Add Device'}</button>
146
172
  </div>
147
173
  `;
148
174
 
@@ -150,21 +176,50 @@
150
176
  config.name = e.target.value;
151
177
  save();
152
178
  });
153
- document.getElementById('cfg-behavior').addEventListener('change', (e) => {
154
- config.switchBehavior = e.target.value;
155
- save();
156
- });
157
- document.getElementById('btn-add').addEventListener('click', () => {
158
- if (!config.switches) config.switches = [];
159
- config.switches.push({ name: '', type: 'outlet', defaultState: false, delayOff: 0 });
179
+
180
+ document.getElementById('btn-add-device').addEventListener('click', () => {
181
+ config.devices.push({
182
+ name: '',
183
+ switchBehavior: 'independent',
184
+ switches: [{ name: '', type: 'outlet', defaultState: false, delayOff: 0 }],
185
+ });
160
186
  render();
161
187
  save();
162
188
  });
163
189
 
164
- document.querySelectorAll('.btn-remove').forEach(btn => {
190
+ // Device-level events
191
+ document.querySelectorAll('.dev-field').forEach(input => {
192
+ input.addEventListener('change', () => {
193
+ const di = parseInt(input.dataset.dev);
194
+ const field = input.dataset.field;
195
+ config.devices[di][field] = input.value;
196
+ save();
197
+ });
198
+ });
199
+
200
+ document.querySelectorAll('.btn-remove-device').forEach(btn => {
201
+ btn.addEventListener('click', () => {
202
+ config.devices.splice(parseInt(btn.dataset.dev), 1);
203
+ render();
204
+ save();
205
+ });
206
+ });
207
+
208
+ // Switch-level events
209
+ document.querySelectorAll('.btn-add-switch').forEach(btn => {
165
210
  btn.addEventListener('click', () => {
166
- const idx = parseInt(btn.dataset.idx);
167
- config.switches.splice(idx, 1);
211
+ const di = parseInt(btn.dataset.dev);
212
+ config.devices[di].switches.push({ name: '', type: 'outlet', defaultState: false, delayOff: 0 });
213
+ render();
214
+ save();
215
+ });
216
+ });
217
+
218
+ document.querySelectorAll('.btn-remove-switch').forEach(btn => {
219
+ btn.addEventListener('click', () => {
220
+ const di = parseInt(btn.dataset.dev);
221
+ const si = parseInt(btn.dataset.sw);
222
+ config.devices[di].switches.splice(si, 1);
168
223
  render();
169
224
  save();
170
225
  });
@@ -172,35 +227,69 @@
172
227
 
173
228
  document.querySelectorAll('.sw-field').forEach(input => {
174
229
  input.addEventListener('change', () => {
175
- const idx = parseInt(input.dataset.idx);
230
+ const di = parseInt(input.dataset.dev);
231
+ const si = parseInt(input.dataset.sw);
176
232
  const field = input.dataset.field;
177
233
  if (field === 'defaultState') {
178
- config.switches[idx][field] = input.checked;
234
+ config.devices[di].switches[si][field] = input.checked;
179
235
  } else if (field === 'delayOff') {
180
- config.switches[idx][field] = parseInt(input.value) || 0;
236
+ config.devices[di].switches[si][field] = parseInt(input.value) || 0;
181
237
  } else {
182
- config.switches[idx][field] = input.value;
238
+ config.devices[di].switches[si][field] = input.value;
183
239
  }
184
240
  save();
185
241
  });
186
242
  });
187
243
  }
188
244
 
189
- function renderSwitch(sw, i) {
245
+ function renderDevice(dev, di) {
246
+ const switches = dev.switches || [];
247
+ return `
248
+ <div class="device-card">
249
+ <div class="device-header">
250
+ <strong>${t.device || 'Device'} #${di + 1}</strong>
251
+ <button class="btn btn-danger btn-remove-device" data-dev="${di}">${t.removeDevice || 'Remove Device'}</button>
252
+ </div>
253
+
254
+ <div class="inline-row">
255
+ <div class="form-group">
256
+ <label>${t.deviceName || 'Device Name'}</label>
257
+ <input type="text" class="dev-field" data-dev="${di}" data-field="name" value="${esc(dev.name || '')}">
258
+ </div>
259
+ <div class="form-group">
260
+ <label>${t.switchBehavior || 'Switch Behavior Mode'}</label>
261
+ <select class="dev-field" data-dev="${di}" data-field="switchBehavior">
262
+ <option value="independent" ${dev.switchBehavior === 'independent' || !dev.switchBehavior ? 'selected' : ''}>${t.behaviorIndependent || 'Independent'}</option>
263
+ <option value="master" ${dev.switchBehavior === 'master' ? 'selected' : ''}>${t.behaviorMaster || 'Master'}</option>
264
+ <option value="single" ${dev.switchBehavior === 'single' ? 'selected' : ''}>${t.behaviorSingle || 'Single'}</option>
265
+ </select>
266
+ </div>
267
+ </div>
268
+
269
+ <div class="section-title">${t.switches || 'Switches'}</div>
270
+
271
+ ${switches.map((sw, si) => renderSwitch(sw, di, si)).join('')}
272
+
273
+ <button class="btn btn-primary btn-add-switch" data-dev="${di}">+ ${t.addSwitch || 'Add Switch'}</button>
274
+ </div>
275
+ `;
276
+ }
277
+
278
+ function renderSwitch(sw, di, si) {
190
279
  return `
191
280
  <div class="switch-card">
192
281
  <div class="switch-header">
193
- <strong>#${i + 1}</strong>
194
- <button class="btn btn-danger btn-remove" data-idx="${i}">${t.removeSwitch || 'Remove'}</button>
282
+ <strong>#${si + 1}</strong>
283
+ <button class="btn btn-danger btn-remove-switch" data-dev="${di}" data-sw="${si}">${t.removeSwitch || 'Remove'}</button>
195
284
  </div>
196
285
  <div class="inline-row">
197
286
  <div class="form-group">
198
287
  <label>${t.switchName || 'Switch Name'}</label>
199
- <input type="text" class="sw-field" data-idx="${i}" data-field="name" value="${esc(sw.name || '')}">
288
+ <input type="text" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="name" value="${esc(sw.name || '')}">
200
289
  </div>
201
290
  <div class="form-group">
202
291
  <label>${t.switchType || 'Switch Type'}</label>
203
- <select class="sw-field" data-idx="${i}" data-field="type">
292
+ <select class="sw-field" data-dev="${di}" data-sw="${si}" data-field="type">
204
293
  <option value="switch" ${sw.type === 'switch' ? 'selected' : ''}>${t.typeSwitch || 'Switch'}</option>
205
294
  <option value="outlet" ${sw.type === 'outlet' || !sw.type ? 'selected' : ''}>${t.typeOutlet || 'Outlet'}</option>
206
295
  </select>
@@ -209,12 +298,12 @@
209
298
  <div class="inline-row">
210
299
  <div class="form-group">
211
300
  <label>${t.delayOff || 'Auto Turn Off (ms)'}</label>
212
- <input type="number" class="sw-field" data-idx="${i}" data-field="delayOff" min="0" value="${sw.delayOff || 0}">
301
+ <input type="number" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="delayOff" min="0" value="${sw.delayOff || 0}">
213
302
  </div>
214
303
  <div class="form-group">
215
304
  <label>${t.defaultState || 'Default State'}</label>
216
305
  <div class="toggle-wrap" style="margin-top:6px">
217
- <input type="checkbox" class="sw-field" data-idx="${i}" data-field="defaultState" ${sw.defaultState ? 'checked' : ''}>
306
+ <input type="checkbox" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="defaultState" ${sw.defaultState ? 'checked' : ''}>
218
307
  <span>${sw.defaultState ? (t.on || 'On') : (t.off || 'Off')}</span>
219
308
  </div>
220
309
  </div>
package/index.js CHANGED
@@ -20,7 +20,7 @@ class MultipleSwitchPlatform {
20
20
  this.Service = api.hap.Service;
21
21
  this.Characteristic = api.hap.Characteristic;
22
22
  this.cachedAccessories = new Map();
23
- this.switchServices = new Map();
23
+ this.deviceServices = new Map();
24
24
 
25
25
  this.api.on('didFinishLaunching', () => {
26
26
  this.log.info('MultipleSwitchPlatform started.');
@@ -32,16 +32,47 @@ class MultipleSwitchPlatform {
32
32
  this.cachedAccessories.set(accessory.UUID, accessory);
33
33
  }
34
34
 
35
+ getDevices() {
36
+ if (Array.isArray(this.config.devices) && this.config.devices.length > 0) {
37
+ return this.config.devices;
38
+ }
39
+
40
+ if (Array.isArray(this.config.switches) && this.config.switches.length > 0) {
41
+ return [{
42
+ name: this.config.name || 'Multiple Switch Panel',
43
+ switchBehavior: this.config.switchBehavior || 'independent',
44
+ switches: this.config.switches,
45
+ }];
46
+ }
47
+
48
+ return [];
49
+ }
50
+
35
51
  setupAccessories() {
36
- const switches = this.config.switches;
37
- if (!Array.isArray(switches) || switches.length === 0) {
38
- this.log.warn('No switches configured. Removing stale accessories.');
52
+ const devices = this.getDevices();
53
+
54
+ if (devices.length === 0) {
55
+ this.log.warn('No devices configured. Removing stale accessories.');
39
56
  this.removeStaleCachedAccessories();
40
57
  return;
41
58
  }
42
59
 
43
- const name = this.config.name || 'Multiple Switch Panel';
44
- const behavior = this.config.switchBehavior || 'independent';
60
+ for (const device of devices) {
61
+ this.setupDevice(device);
62
+ }
63
+
64
+ this.removeStaleCachedAccessories();
65
+ }
66
+
67
+ setupDevice(device) {
68
+ const switches = device.switches;
69
+ if (!Array.isArray(switches) || switches.length === 0) {
70
+ this.log.warn(`Device "${device.name}" has no switches, skipping.`);
71
+ return;
72
+ }
73
+
74
+ const name = device.name || 'Multiple Switch Panel';
75
+ const behavior = device.switchBehavior || 'independent';
45
76
  const uuid = this.api.hap.uuid.generate(name);
46
77
 
47
78
  let accessory = this.cachedAccessories.get(uuid);
@@ -54,17 +85,19 @@ class MultipleSwitchPlatform {
54
85
  accessory.context.switchBehavior = behavior;
55
86
  accessory.context.switchStates = accessory.context.switchStates || {};
56
87
 
57
- this.reconcileServices(accessory, switches);
88
+ const services = new Map();
89
+ this.deviceServices.set(uuid, services);
90
+
91
+ this.reconcileServices(accessory, switches, services);
58
92
 
59
93
  if (isNew) {
60
94
  this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
61
95
  }
62
96
 
63
97
  this.cachedAccessories.delete(uuid);
64
- this.removeStaleCachedAccessories();
65
98
  }
66
99
 
67
- reconcileServices(accessory, switches) {
100
+ reconcileServices(accessory, switches, services) {
68
101
  const activeSubtypes = new Set();
69
102
 
70
103
  switches.forEach((sw, index) => {
@@ -79,9 +112,9 @@ class MultipleSwitchPlatform {
79
112
  }
80
113
 
81
114
  service.setCharacteristic(this.Characteristic.Name, sw.name);
82
- this.configureSwitchHandlers(accessory, service, sw, subtype);
115
+ this.configureSwitchHandlers(accessory, service, sw, subtype, services);
83
116
 
84
- this.switchServices.set(subtype, service);
117
+ services.set(subtype, service);
85
118
 
86
119
  if (accessory.context.switchStates[subtype] === undefined) {
87
120
  accessory.context.switchStates[subtype] = sw.defaultState || false;
@@ -94,7 +127,7 @@ class MultipleSwitchPlatform {
94
127
  servicesToRemove.forEach((s) => accessory.removeService(s));
95
128
  }
96
129
 
97
- configureSwitchHandlers(accessory, service, sw, subtype) {
130
+ configureSwitchHandlers(accessory, service, sw, subtype, services) {
98
131
  service.getCharacteristic(this.Characteristic.On)
99
132
  .onGet(() => accessory.context.switchStates[subtype] ?? false)
100
133
  .onSet((value) => {
@@ -104,11 +137,11 @@ class MultipleSwitchPlatform {
104
137
  const behavior = accessory.context.switchBehavior;
105
138
 
106
139
  if (behavior === 'single' && value) {
107
- this.turnOffOthers(accessory, subtype);
140
+ this.turnOffOthers(accessory, subtype, services);
108
141
  }
109
142
 
110
143
  if (behavior === 'master') {
111
- this.setAll(accessory, value);
144
+ this.setAll(accessory, value, services);
112
145
  }
113
146
 
114
147
  if (value && sw.delayOff > 0) {
@@ -117,8 +150,8 @@ class MultipleSwitchPlatform {
117
150
  });
118
151
  }
119
152
 
120
- turnOffOthers(accessory, excludeSubtype) {
121
- for (const [key, svc] of this.switchServices) {
153
+ turnOffOthers(accessory, excludeSubtype, services) {
154
+ for (const [key, svc] of services) {
122
155
  if (key !== excludeSubtype) {
123
156
  accessory.context.switchStates[key] = false;
124
157
  svc.updateCharacteristic(this.Characteristic.On, false);
@@ -126,8 +159,8 @@ class MultipleSwitchPlatform {
126
159
  }
127
160
  }
128
161
 
129
- setAll(accessory, value) {
130
- for (const [key, svc] of this.switchServices) {
162
+ setAll(accessory, value, services) {
163
+ for (const [key, svc] of services) {
131
164
  accessory.context.switchStates[key] = value;
132
165
  svc.updateCharacteristic(this.Characteristic.On, value);
133
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-multiple-switch",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Multiple switch platform for Homebridge",
5
5
  "homepage": "https://github.com/azadaydinli/homebridge-multiple-switch",
6
6
  "main": "index.js",