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 +9 -0
- package/config.schema.json +53 -45
- package/homebridge-ui/public/i18n/ar.json +7 -1
- package/homebridge-ui/public/i18n/de.json +7 -1
- package/homebridge-ui/public/i18n/en.json +7 -1
- package/homebridge-ui/public/i18n/es.json +7 -1
- package/homebridge-ui/public/i18n/fr.json +7 -1
- package/homebridge-ui/public/i18n/it.json +7 -1
- package/homebridge-ui/public/i18n/ja.json +7 -1
- package/homebridge-ui/public/i18n/ko.json +7 -1
- package/homebridge-ui/public/i18n/nl.json +7 -1
- package/homebridge-ui/public/i18n/pl.json +7 -1
- package/homebridge-ui/public/i18n/pt.json +7 -1
- package/homebridge-ui/public/i18n/ru.json +7 -1
- package/homebridge-ui/public/i18n/tr.json +7 -1
- package/homebridge-ui/public/i18n/zh-CN.json +7 -1
- package/homebridge-ui/public/index.html +126 -37
- package/index.js +51 -18
- package/package.json +1 -1
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
|
package/config.schema.json
CHANGED
|
@@ -12,65 +12,73 @@
|
|
|
12
12
|
"type": "string",
|
|
13
13
|
"default": "Multiple Switch Platform"
|
|
14
14
|
},
|
|
15
|
-
"
|
|
16
|
-
"title": "
|
|
17
|
-
"description": "
|
|
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": "
|
|
35
|
-
"description": "Display name of
|
|
23
|
+
"title": "Device Name",
|
|
24
|
+
"description": "Display name of this device in HomeKit.",
|
|
36
25
|
"type": "string"
|
|
37
26
|
},
|
|
38
|
-
"
|
|
39
|
-
"title": "Switch
|
|
40
|
-
"description": "
|
|
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": "
|
|
31
|
+
"default": "independent",
|
|
43
32
|
"oneOf": [
|
|
44
|
-
{ "title": "
|
|
45
|
-
{ "title": "
|
|
33
|
+
{ "title": "Independent", "enum": ["independent"] },
|
|
34
|
+
{ "title": "Master", "enum": ["master"] },
|
|
35
|
+
{ "title": "Single", "enum": ["single"] }
|
|
46
36
|
]
|
|
47
37
|
},
|
|
48
|
-
"
|
|
49
|
-
"title": "
|
|
50
|
-
"description": "
|
|
51
|
-
"type": "
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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", "
|
|
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"
|
|
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="
|
|
128
|
-
|
|
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="
|
|
141
|
-
${
|
|
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-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
167
|
-
config.switches.
|
|
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
|
|
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[
|
|
234
|
+
config.devices[di].switches[si][field] = input.checked;
|
|
179
235
|
} else if (field === 'delayOff') {
|
|
180
|
-
config.switches[
|
|
236
|
+
config.devices[di].switches[si][field] = parseInt(input.value) || 0;
|
|
181
237
|
} else {
|
|
182
|
-
config.switches[
|
|
238
|
+
config.devices[di].switches[si][field] = input.value;
|
|
183
239
|
}
|
|
184
240
|
save();
|
|
185
241
|
});
|
|
186
242
|
});
|
|
187
243
|
}
|
|
188
244
|
|
|
189
|
-
function
|
|
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>#${
|
|
194
|
-
<button class="btn btn-danger btn-remove" data-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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