homebridge-multiple-switch 1.4.0 → 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 +16 -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 +164 -55
- package/index.js +51 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
12
|
+
## [1.4.1] - 2026-03-21
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Dark mode: replaced Bootstrap CSS variables (not available inside iframe) with
|
|
16
|
+
custom `--ui-*` variables and `@media (prefers-color-scheme: dark)` for reliable
|
|
17
|
+
light/dark theme detection across all elements (cards, inputs, labels, borders)
|
|
18
|
+
|
|
3
19
|
## [1.4.0] - 2026-03-21
|
|
4
20
|
|
|
5
21
|
### Removed
|
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
|
}
|
|
@@ -1,35 +1,72 @@
|
|
|
1
1
|
<style>
|
|
2
|
+
:root {
|
|
3
|
+
--ui-bg: #fff;
|
|
4
|
+
--ui-text: #212529;
|
|
5
|
+
--ui-text-muted: #6c757d;
|
|
6
|
+
--ui-border: #dee2e6;
|
|
7
|
+
--ui-card-bg: #f8f9fa;
|
|
8
|
+
--ui-input-bg: #fff;
|
|
9
|
+
--ui-accent: #007bff;
|
|
10
|
+
--ui-device-bg: rgba(0,123,255,0.04);
|
|
11
|
+
--ui-device-border: rgba(0,123,255,0.2);
|
|
12
|
+
}
|
|
13
|
+
@media (prefers-color-scheme: dark) {
|
|
14
|
+
:root {
|
|
15
|
+
--ui-bg: transparent;
|
|
16
|
+
--ui-text: #e1e1e1;
|
|
17
|
+
--ui-text-muted: #9e9e9e;
|
|
18
|
+
--ui-border: #444;
|
|
19
|
+
--ui-card-bg: rgba(255,255,255,0.06);
|
|
20
|
+
--ui-input-bg: rgba(255,255,255,0.08);
|
|
21
|
+
--ui-accent: #7b61ff;
|
|
22
|
+
--ui-device-bg: rgba(123,97,255,0.06);
|
|
23
|
+
--ui-device-border: rgba(123,97,255,0.25);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
2
26
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
3
27
|
html, body {
|
|
4
28
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
5
|
-
color: var(--
|
|
6
|
-
background:
|
|
29
|
+
color: var(--ui-text);
|
|
30
|
+
background: var(--ui-bg) !important;
|
|
7
31
|
}
|
|
8
32
|
.form-group { margin-bottom: 16px; }
|
|
9
33
|
label {
|
|
10
34
|
display: block; font-weight: 600; margin-bottom: 4px; font-size: 14px;
|
|
11
|
-
color: var(--
|
|
35
|
+
color: var(--ui-text);
|
|
12
36
|
}
|
|
13
37
|
.desc {
|
|
14
|
-
color: var(--
|
|
38
|
+
color: var(--ui-text-muted);
|
|
15
39
|
font-size: 12px; margin-bottom: 6px;
|
|
16
40
|
}
|
|
17
41
|
input[type="text"], input[type="number"], select {
|
|
18
42
|
width: 100%; padding: 8px 10px;
|
|
19
|
-
border: 1px solid var(--
|
|
43
|
+
border: 1px solid var(--ui-border);
|
|
20
44
|
border-radius: 6px; font-size: 14px;
|
|
21
|
-
background: var(--
|
|
22
|
-
color: var(--
|
|
45
|
+
background: var(--ui-input-bg);
|
|
46
|
+
color: var(--ui-text);
|
|
23
47
|
transition: border-color 0.2s;
|
|
24
48
|
}
|
|
25
49
|
input:focus, select:focus {
|
|
26
|
-
border-color: var(--
|
|
50
|
+
border-color: var(--ui-accent);
|
|
27
51
|
outline: none;
|
|
28
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
|
+
}
|
|
29
66
|
.switch-card {
|
|
30
|
-
border: 1px solid var(--
|
|
67
|
+
border: 1px solid var(--ui-border);
|
|
31
68
|
border-radius: 8px; padding: 16px; margin-bottom: 12px;
|
|
32
|
-
background: var(--
|
|
69
|
+
background: var(--ui-card-bg);
|
|
33
70
|
position: relative;
|
|
34
71
|
}
|
|
35
72
|
.switch-card .switch-header {
|
|
@@ -37,7 +74,7 @@
|
|
|
37
74
|
}
|
|
38
75
|
.switch-card .switch-header strong {
|
|
39
76
|
font-size: 14px;
|
|
40
|
-
color: var(--
|
|
77
|
+
color: var(--ui-text);
|
|
41
78
|
}
|
|
42
79
|
.btn {
|
|
43
80
|
padding: 8px 16px; border: none; border-radius: 6px;
|
|
@@ -45,21 +82,26 @@
|
|
|
45
82
|
}
|
|
46
83
|
.btn-danger { background: #dc3545; color: #fff; }
|
|
47
84
|
.btn-danger:hover { background: #c82333; }
|
|
48
|
-
.btn-primary { background: var(--
|
|
85
|
+
.btn-primary { background: var(--ui-accent); color: #fff; }
|
|
49
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; }
|
|
50
92
|
.actions { margin-top: 16px; display: flex; gap: 10px; }
|
|
51
93
|
.toggle-wrap { display: flex; align-items: center; gap: 8px; }
|
|
52
|
-
.toggle-wrap input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--
|
|
53
|
-
.toggle-wrap span { color: var(--
|
|
94
|
+
.toggle-wrap input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--ui-accent); }
|
|
95
|
+
.toggle-wrap span { color: var(--ui-text); }
|
|
54
96
|
.section-title {
|
|
55
97
|
font-size: 15px; font-weight: 700; margin-bottom: 10px;
|
|
56
|
-
padding-bottom: 6px; border-bottom: 1px solid var(--
|
|
57
|
-
color: var(--
|
|
98
|
+
padding-bottom: 6px; border-bottom: 1px solid var(--ui-border);
|
|
99
|
+
color: var(--ui-text);
|
|
58
100
|
}
|
|
59
101
|
.inline-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
60
102
|
select option {
|
|
61
|
-
background: var(--
|
|
62
|
-
color: var(--
|
|
103
|
+
background: var(--ui-input-bg);
|
|
104
|
+
color: var(--ui-text);
|
|
63
105
|
}
|
|
64
106
|
</style>
|
|
65
107
|
|
|
@@ -93,10 +135,24 @@
|
|
|
93
135
|
config = configs[0] || {};
|
|
94
136
|
} catch {}
|
|
95
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
|
+
|
|
96
153
|
const app = document.getElementById('app');
|
|
97
154
|
|
|
98
155
|
function render() {
|
|
99
|
-
const switches = config.switches || [];
|
|
100
156
|
app.innerHTML = `
|
|
101
157
|
<div class="form-group">
|
|
102
158
|
<label>${t.platformName || 'Platform Name'}</label>
|
|
@@ -104,25 +160,15 @@
|
|
|
104
160
|
<input type="text" id="cfg-name" value="${esc(config.name || 'Multiple Switch Platform')}">
|
|
105
161
|
</div>
|
|
106
162
|
|
|
107
|
-
<div class="
|
|
108
|
-
|
|
109
|
-
<div class="desc">${t.switchBehaviorDesc || ''}</div>
|
|
110
|
-
<select id="cfg-behavior">
|
|
111
|
-
<option value="independent" ${config.switchBehavior === 'independent' || !config.switchBehavior ? 'selected' : ''}>${t.behaviorIndependent || 'Independent'}</option>
|
|
112
|
-
<option value="master" ${config.switchBehavior === 'master' ? 'selected' : ''}>${t.behaviorMaster || 'Master'}</option>
|
|
113
|
-
<option value="single" ${config.switchBehavior === 'single' ? 'selected' : ''}>${t.behaviorSingle || 'Single'}</option>
|
|
114
|
-
</select>
|
|
115
|
-
</div>
|
|
116
|
-
|
|
117
|
-
<div class="section-title">${t.switches || 'Switches'}</div>
|
|
118
|
-
<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>
|
|
119
165
|
|
|
120
|
-
<div id="
|
|
121
|
-
${
|
|
166
|
+
<div id="devices-list">
|
|
167
|
+
${config.devices.map((dev, di) => renderDevice(dev, di)).join('')}
|
|
122
168
|
</div>
|
|
123
169
|
|
|
124
170
|
<div class="actions">
|
|
125
|
-
<button class="btn btn-
|
|
171
|
+
<button class="btn btn-outline" id="btn-add-device">+ ${t.addDevice || 'Add Device'}</button>
|
|
126
172
|
</div>
|
|
127
173
|
`;
|
|
128
174
|
|
|
@@ -130,21 +176,50 @@
|
|
|
130
176
|
config.name = e.target.value;
|
|
131
177
|
save();
|
|
132
178
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
});
|
|
140
186
|
render();
|
|
141
187
|
save();
|
|
142
188
|
});
|
|
143
189
|
|
|
144
|
-
|
|
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 => {
|
|
145
201
|
btn.addEventListener('click', () => {
|
|
146
|
-
|
|
147
|
-
|
|
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 => {
|
|
210
|
+
btn.addEventListener('click', () => {
|
|
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);
|
|
148
223
|
render();
|
|
149
224
|
save();
|
|
150
225
|
});
|
|
@@ -152,35 +227,69 @@
|
|
|
152
227
|
|
|
153
228
|
document.querySelectorAll('.sw-field').forEach(input => {
|
|
154
229
|
input.addEventListener('change', () => {
|
|
155
|
-
const
|
|
230
|
+
const di = parseInt(input.dataset.dev);
|
|
231
|
+
const si = parseInt(input.dataset.sw);
|
|
156
232
|
const field = input.dataset.field;
|
|
157
233
|
if (field === 'defaultState') {
|
|
158
|
-
config.switches[
|
|
234
|
+
config.devices[di].switches[si][field] = input.checked;
|
|
159
235
|
} else if (field === 'delayOff') {
|
|
160
|
-
config.switches[
|
|
236
|
+
config.devices[di].switches[si][field] = parseInt(input.value) || 0;
|
|
161
237
|
} else {
|
|
162
|
-
config.switches[
|
|
238
|
+
config.devices[di].switches[si][field] = input.value;
|
|
163
239
|
}
|
|
164
240
|
save();
|
|
165
241
|
});
|
|
166
242
|
});
|
|
167
243
|
}
|
|
168
244
|
|
|
169
|
-
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) {
|
|
170
279
|
return `
|
|
171
280
|
<div class="switch-card">
|
|
172
281
|
<div class="switch-header">
|
|
173
|
-
<strong>#${
|
|
174
|
-
<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>
|
|
175
284
|
</div>
|
|
176
285
|
<div class="inline-row">
|
|
177
286
|
<div class="form-group">
|
|
178
287
|
<label>${t.switchName || 'Switch Name'}</label>
|
|
179
|
-
<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 || '')}">
|
|
180
289
|
</div>
|
|
181
290
|
<div class="form-group">
|
|
182
291
|
<label>${t.switchType || 'Switch Type'}</label>
|
|
183
|
-
<select class="sw-field" data-
|
|
292
|
+
<select class="sw-field" data-dev="${di}" data-sw="${si}" data-field="type">
|
|
184
293
|
<option value="switch" ${sw.type === 'switch' ? 'selected' : ''}>${t.typeSwitch || 'Switch'}</option>
|
|
185
294
|
<option value="outlet" ${sw.type === 'outlet' || !sw.type ? 'selected' : ''}>${t.typeOutlet || 'Outlet'}</option>
|
|
186
295
|
</select>
|
|
@@ -189,12 +298,12 @@
|
|
|
189
298
|
<div class="inline-row">
|
|
190
299
|
<div class="form-group">
|
|
191
300
|
<label>${t.delayOff || 'Auto Turn Off (ms)'}</label>
|
|
192
|
-
<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}">
|
|
193
302
|
</div>
|
|
194
303
|
<div class="form-group">
|
|
195
304
|
<label>${t.defaultState || 'Default State'}</label>
|
|
196
305
|
<div class="toggle-wrap" style="margin-top:6px">
|
|
197
|
-
<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' : ''}>
|
|
198
307
|
<span>${sw.defaultState ? (t.on || 'On') : (t.off || 'Off')}</span>
|
|
199
308
|
</div>
|
|
200
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