homebridge-multiple-switch 1.5.0 → 1.6.0-beta.1

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,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.0-beta.1] - 2026-03-21
4
+
5
+ ### Removed
6
+ - Master switch behavior mode — replaced by a more useful master switch option
7
+ within Single mode
8
+
9
+ ### Changed
10
+ - Switch behavior now only has two modes: Independent and Single
11
+ - Added descriptions to both behavior modes explaining how they work
12
+
13
+ ### Added
14
+ - Master Switch option (available in Single mode only): adds an extra switch
15
+ that turns all switches on or off at once
16
+ - New i18n keys for behavior descriptions, master switch label/description
17
+
18
+ ## [1.5.1] - 2026-03-21
19
+
20
+ ### Added
21
+ - Collapsible device cards — click the device header to expand/collapse
22
+ - Collapsible switch cards — click the switch header to expand/collapse
23
+ - Summary info shown when collapsed (switch count for devices, type/delay for switches)
24
+ - Chevron indicator (▶) with rotation animation for open/closed state
25
+
3
26
  ## [1.5.0] - 2026-03-21
4
27
 
5
28
  ### Added
@@ -31,10 +31,15 @@
31
31
  "default": "independent",
32
32
  "oneOf": [
33
33
  { "title": "Independent", "enum": ["independent"] },
34
- { "title": "Master", "enum": ["master"] },
35
34
  { "title": "Single", "enum": ["single"] }
36
35
  ]
37
36
  },
37
+ "masterSwitch": {
38
+ "title": "Master Switch",
39
+ "description": "Add a master switch that turns all switches on or off at once. Only available in Single mode.",
40
+ "type": "boolean",
41
+ "default": false
42
+ },
38
43
  "switches": {
39
44
  "title": "Switches",
40
45
  "description": "List of virtual switches in this device.",
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "وضع سلوك المفتاح",
6
6
  "switchBehaviorDesc": "يتحكم في كيفية تفاعل المفاتيح مع بعضها البعض.",
7
7
  "behaviorIndependent": "مستقل",
8
- "behaviorMaster": "رئيسي",
9
8
  "behaviorSingle": "فردي",
10
9
  "switches": "المفاتيح",
11
10
  "switchesDesc": "قائمة المفاتيح الافتراضية المراد إنشاؤها.",
@@ -29,5 +28,10 @@
29
28
  "device": "جهاز",
30
29
  "deviceName": "اسم الجهاز",
31
30
  "addDevice": "إضافة جهاز",
32
- "removeDevice": "إزالة الجهاز"
31
+ "removeDevice": "إزالة الجهاز",
32
+ "behaviorIndependentDesc": "كل مفتاح يعمل بشكل مستقل. تشغيل أو إيقاف أحدها لا يؤثر على الآخرين.",
33
+ "behaviorSingleDesc": "يمكن تشغيل مفتاح واحد فقط في نفس الوقت. تشغيل واحد يقوم بإيقاف جميع الآخرين تلقائياً.",
34
+ "masterSwitch": "المفتاح الرئيسي",
35
+ "masterSwitchDesc": "يضيف مفتاحاً رئيسياً يقوم بتشغيل أو إيقاف جميع المفاتيح دفعة واحدة.",
36
+ "switchSingular": "مفتاح"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "Schaltverhalten",
6
6
  "switchBehaviorDesc": "Steuert, wie die Schalter miteinander interagieren.",
7
7
  "behaviorIndependent": "Unabhängig",
8
- "behaviorMaster": "Master",
9
8
  "behaviorSingle": "Einzeln",
10
9
  "switches": "Schalter",
11
10
  "switchesDesc": "Liste der zu erstellenden virtuellen Schalter.",
@@ -29,5 +28,10 @@
29
28
  "device": "Gerät",
30
29
  "deviceName": "Gerätename",
31
30
  "addDevice": "Gerät hinzufügen",
32
- "removeDevice": "Gerät entfernen"
31
+ "removeDevice": "Gerät entfernen",
32
+ "behaviorIndependentDesc": "Jeder Schalter arbeitet unabhängig. Das Ein- oder Ausschalten eines Schalters hat keinen Einfluss auf die anderen.",
33
+ "behaviorSingleDesc": "Es kann nur ein Schalter gleichzeitig eingeschaltet sein. Das Einschalten eines Schalters schaltet alle anderen automatisch aus.",
34
+ "masterSwitch": "Hauptschalter",
35
+ "masterSwitchDesc": "Fügt einen Hauptschalter hinzu, der alle Schalter auf einmal ein- oder ausschaltet.",
36
+ "switchSingular": "Schalter"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "Switch Behavior Mode",
6
6
  "switchBehaviorDesc": "Controls how switches interact with each other.",
7
7
  "behaviorIndependent": "Independent",
8
- "behaviorMaster": "Master",
9
8
  "behaviorSingle": "Single",
10
9
  "switches": "Switches",
11
10
  "switchesDesc": "List of virtual switches to create.",
@@ -29,5 +28,10 @@
29
28
  "device": "Device",
30
29
  "deviceName": "Device Name",
31
30
  "addDevice": "Add Device",
32
- "removeDevice": "Remove Device"
31
+ "removeDevice": "Remove Device",
32
+ "behaviorIndependentDesc": "Each switch works independently. Turning one on or off has no effect on the others.",
33
+ "behaviorSingleDesc": "Only one switch can be on at a time. Turning one on automatically turns off all others.",
34
+ "masterSwitch": "Master Switch",
35
+ "masterSwitchDesc": "Adds a master switch that turns all switches on or off at once.",
36
+ "switchSingular": "switch"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "Modo de comportamiento",
6
6
  "switchBehaviorDesc": "Controla cómo interactúan los interruptores entre sí.",
7
7
  "behaviorIndependent": "Independiente",
8
- "behaviorMaster": "Maestro",
9
8
  "behaviorSingle": "Único",
10
9
  "switches": "Interruptores",
11
10
  "switchesDesc": "Lista de interruptores virtuales a crear.",
@@ -29,5 +28,10 @@
29
28
  "device": "Dispositivo",
30
29
  "deviceName": "Nombre del dispositivo",
31
30
  "addDevice": "Agregar dispositivo",
32
- "removeDevice": "Eliminar dispositivo"
31
+ "removeDevice": "Eliminar dispositivo",
32
+ "behaviorIndependentDesc": "Cada interruptor funciona de forma independiente. Encender o apagar uno no afecta a los demás.",
33
+ "behaviorSingleDesc": "Solo un interruptor puede estar encendido a la vez. Encender uno apaga automáticamente todos los demás.",
34
+ "masterSwitch": "Interruptor maestro",
35
+ "masterSwitchDesc": "Agrega un interruptor maestro que enciende o apaga todos los interruptores a la vez.",
36
+ "switchSingular": "interruptor"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "Mode de comportement",
6
6
  "switchBehaviorDesc": "Contrôle la façon dont les interrupteurs interagissent entre eux.",
7
7
  "behaviorIndependent": "Indépendant",
8
- "behaviorMaster": "Maître",
9
8
  "behaviorSingle": "Unique",
10
9
  "switches": "Interrupteurs",
11
10
  "switchesDesc": "Liste des interrupteurs virtuels à créer.",
@@ -29,5 +28,10 @@
29
28
  "device": "Appareil",
30
29
  "deviceName": "Nom de l'appareil",
31
30
  "addDevice": "Ajouter un appareil",
32
- "removeDevice": "Supprimer l'appareil"
31
+ "removeDevice": "Supprimer l'appareil",
32
+ "behaviorIndependentDesc": "Chaque interrupteur fonctionne indépendamment. Activer ou désactiver l'un n'affecte pas les autres.",
33
+ "behaviorSingleDesc": "Un seul interrupteur peut être activé à la fois. En activer un désactive automatiquement tous les autres.",
34
+ "masterSwitch": "Interrupteur principal",
35
+ "masterSwitchDesc": "Ajoute un interrupteur principal qui active ou désactive tous les interrupteurs en une seule fois.",
36
+ "switchSingular": "interrupteur"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "Modalità di comportamento",
6
6
  "switchBehaviorDesc": "Controlla come gli interruttori interagiscono tra loro.",
7
7
  "behaviorIndependent": "Indipendente",
8
- "behaviorMaster": "Master",
9
8
  "behaviorSingle": "Singolo",
10
9
  "switches": "Interruttori",
11
10
  "switchesDesc": "Elenco degli interruttori virtuali da creare.",
@@ -29,5 +28,10 @@
29
28
  "device": "Dispositivo",
30
29
  "deviceName": "Nome del dispositivo",
31
30
  "addDevice": "Aggiungi dispositivo",
32
- "removeDevice": "Rimuovi dispositivo"
31
+ "removeDevice": "Rimuovi dispositivo",
32
+ "behaviorIndependentDesc": "Ogni interruttore funziona in modo indipendente. Accendere o spegnere uno non influisce sugli altri.",
33
+ "behaviorSingleDesc": "Solo un interruttore può essere acceso alla volta. Accenderne uno spegne automaticamente tutti gli altri.",
34
+ "masterSwitch": "Interruttore principale",
35
+ "masterSwitchDesc": "Aggiunge un interruttore principale che accende o spegne tutti gli interruttori contemporaneamente.",
36
+ "switchSingular": "interruttore"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "スイッチ動作モード",
6
6
  "switchBehaviorDesc": "スイッチ同士の相互作用を制御します。",
7
7
  "behaviorIndependent": "独立",
8
- "behaviorMaster": "マスター",
9
8
  "behaviorSingle": "シングル",
10
9
  "switches": "スイッチ",
11
10
  "switchesDesc": "作成する仮想スイッチのリスト。",
@@ -29,5 +28,10 @@
29
28
  "device": "デバイス",
30
29
  "deviceName": "デバイス名",
31
30
  "addDevice": "デバイスを追加",
32
- "removeDevice": "デバイスを削除"
31
+ "removeDevice": "デバイスを削除",
32
+ "behaviorIndependentDesc": "各スイッチは独立して動作します。1つのオン/オフは他に影響しません。",
33
+ "behaviorSingleDesc": "同時に1つのスイッチのみオンにできます。1つをオンにすると他はすべて自動的にオフになります。",
34
+ "masterSwitch": "マスタースイッチ",
35
+ "masterSwitchDesc": "すべてのスイッチを一度にオン/オフするマスタースイッチを追加します。",
36
+ "switchSingular": "スイッチ"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "스위치 동작 모드",
6
6
  "switchBehaviorDesc": "스위치 간의 상호 작용 방식을 제어합니다.",
7
7
  "behaviorIndependent": "독립",
8
- "behaviorMaster": "마스터",
9
8
  "behaviorSingle": "단일",
10
9
  "switches": "스위치",
11
10
  "switchesDesc": "생성할 가상 스위치 목록.",
@@ -29,5 +28,10 @@
29
28
  "device": "장치",
30
29
  "deviceName": "장치 이름",
31
30
  "addDevice": "장치 추가",
32
- "removeDevice": "장치 삭제"
31
+ "removeDevice": "장치 삭제",
32
+ "behaviorIndependentDesc": "각 스위치는 독립적으로 작동합니다. 하나를 켜거나 끄는 것이 다른 것에 영향을 주지 않습니다.",
33
+ "behaviorSingleDesc": "한 번에 하나의 스위치만 켤 수 있습니다. 하나를 켜면 다른 모든 것이 자동으로 꺼집니다.",
34
+ "masterSwitch": "마스터 스위치",
35
+ "masterSwitchDesc": "모든 스위치를 한 번에 켜거나 끄는 마스터 스위치를 추가합니다.",
36
+ "switchSingular": "스위치"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "Schakelaargedrag",
6
6
  "switchBehaviorDesc": "Bepaalt hoe de schakelaars met elkaar omgaan.",
7
7
  "behaviorIndependent": "Onafhankelijk",
8
- "behaviorMaster": "Master",
9
8
  "behaviorSingle": "Enkelvoudig",
10
9
  "switches": "Schakelaars",
11
10
  "switchesDesc": "Lijst van virtuele schakelaars om aan te maken.",
@@ -29,5 +28,10 @@
29
28
  "device": "Apparaat",
30
29
  "deviceName": "Apparaatnaam",
31
30
  "addDevice": "Apparaat toevoegen",
32
- "removeDevice": "Apparaat verwijderen"
31
+ "removeDevice": "Apparaat verwijderen",
32
+ "behaviorIndependentDesc": "Elke schakelaar werkt onafhankelijk. Het in- of uitschakelen van één heeft geen invloed op de andere.",
33
+ "behaviorSingleDesc": "Er kan slechts één schakelaar tegelijk aan staan. Het inschakelen van één schakelaar schakelt alle andere automatisch uit.",
34
+ "masterSwitch": "Hoofdschakelaar",
35
+ "masterSwitchDesc": "Voegt een hoofdschakelaar toe die alle schakelaars in één keer in- of uitschakelt.",
36
+ "switchSingular": "schakelaar"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "Tryb zachowania przełączników",
6
6
  "switchBehaviorDesc": "Kontroluje sposób interakcji przełączników ze sobą.",
7
7
  "behaviorIndependent": "Niezależny",
8
- "behaviorMaster": "Główny",
9
8
  "behaviorSingle": "Pojedynczy",
10
9
  "switches": "Przełączniki",
11
10
  "switchesDesc": "Lista wirtualnych przełączników do utworzenia.",
@@ -29,5 +28,10 @@
29
28
  "device": "Urządzenie",
30
29
  "deviceName": "Nazwa urządzenia",
31
30
  "addDevice": "Dodaj urządzenie",
32
- "removeDevice": "Usuń urządzenie"
31
+ "removeDevice": "Usuń urządzenie",
32
+ "behaviorIndependentDesc": "Każdy przełącznik działa niezależnie. Włączenie lub wyłączenie jednego nie wpływa na pozostałe.",
33
+ "behaviorSingleDesc": "Tylko jeden przełącznik może być włączony jednocześnie. Włączenie jednego automatycznie wyłącza wszystkie pozostałe.",
34
+ "masterSwitch": "Przełącznik główny",
35
+ "masterSwitchDesc": "Dodaje przełącznik główny, który włącza lub wyłącza wszystkie przełączniki jednocześnie.",
36
+ "switchSingular": "przełącznik"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "Modo de comportamento",
6
6
  "switchBehaviorDesc": "Controla como os interruptores interagem entre si.",
7
7
  "behaviorIndependent": "Independente",
8
- "behaviorMaster": "Mestre",
9
8
  "behaviorSingle": "Único",
10
9
  "switches": "Interruptores",
11
10
  "switchesDesc": "Lista de interruptores virtuais a criar.",
@@ -29,5 +28,10 @@
29
28
  "device": "Dispositivo",
30
29
  "deviceName": "Nome do dispositivo",
31
30
  "addDevice": "Adicionar dispositivo",
32
- "removeDevice": "Remover dispositivo"
31
+ "removeDevice": "Remover dispositivo",
32
+ "behaviorIndependentDesc": "Cada interruptor funciona de forma independente. Ligar ou desligar um não afeta os outros.",
33
+ "behaviorSingleDesc": "Apenas um interruptor pode estar ligado por vez. Ligar um desliga automaticamente todos os outros.",
34
+ "masterSwitch": "Interruptor principal",
35
+ "masterSwitchDesc": "Adiciona um interruptor principal que liga ou desliga todos os interruptores de uma só vez.",
36
+ "switchSingular": "interruptor"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "Режим поведения",
6
6
  "switchBehaviorDesc": "Управляет взаимодействием переключателей друг с другом.",
7
7
  "behaviorIndependent": "Независимый",
8
- "behaviorMaster": "Мастер",
9
8
  "behaviorSingle": "Одиночный",
10
9
  "switches": "Переключатели",
11
10
  "switchesDesc": "Список виртуальных переключателей для создания.",
@@ -29,5 +28,10 @@
29
28
  "device": "Устройство",
30
29
  "deviceName": "Название устройства",
31
30
  "addDevice": "Добавить устройство",
32
- "removeDevice": "Удалить устройство"
31
+ "removeDevice": "Удалить устройство",
32
+ "behaviorIndependentDesc": "Каждый переключатель работает независимо. Включение или выключение одного не влияет на остальные.",
33
+ "behaviorSingleDesc": "Одновременно может быть включён только один переключатель. Включение одного автоматически выключает все остальные.",
34
+ "masterSwitch": "Главный переключатель",
35
+ "masterSwitchDesc": "Добавляет главный переключатель, который включает или выключает все переключатели одновременно.",
36
+ "switchSingular": "переключатель"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "Anahtar Davranış Modu",
6
6
  "switchBehaviorDesc": "Anahtarların birbirleriyle nasıl etkileşime gireceğini kontrol eder.",
7
7
  "behaviorIndependent": "Bağımsız",
8
- "behaviorMaster": "Ana Kontrol",
9
8
  "behaviorSingle": "Tekli",
10
9
  "switches": "Anahtarlar",
11
10
  "switchesDesc": "Oluşturulacak sanal anahtarların listesi.",
@@ -29,5 +28,10 @@
29
28
  "device": "Cihaz",
30
29
  "deviceName": "Cihaz Adı",
31
30
  "addDevice": "Cihaz Ekle",
32
- "removeDevice": "Cihazı Kaldır"
31
+ "removeDevice": "Cihazı Kaldır",
32
+ "behaviorIndependentDesc": "Her anahtar bağımsız çalışır. Birini açmak veya kapatmak diğerlerini etkilemez.",
33
+ "behaviorSingleDesc": "Aynı anda yalnızca bir anahtar açık olabilir. Birini açmak diğerlerini otomatik kapatır.",
34
+ "masterSwitch": "Ana Anahtar",
35
+ "masterSwitchDesc": "Tüm anahtarları tek seferde açan veya kapatan bir ana anahtar ekler.",
36
+ "switchSingular": "anahtar"
33
37
  }
@@ -5,7 +5,6 @@
5
5
  "switchBehavior": "开关行为模式",
6
6
  "switchBehaviorDesc": "控制开关之间的交互方式。",
7
7
  "behaviorIndependent": "独立",
8
- "behaviorMaster": "主控",
9
8
  "behaviorSingle": "单选",
10
9
  "switches": "开关",
11
10
  "switchesDesc": "要创建的虚拟开关列表。",
@@ -29,5 +28,10 @@
29
28
  "device": "设备",
30
29
  "deviceName": "设备名称",
31
30
  "addDevice": "添加设备",
32
- "removeDevice": "删除设备"
31
+ "removeDevice": "删除设备",
32
+ "behaviorIndependentDesc": "每个开关独立工作。打开或关闭一个不会影响其他开关。",
33
+ "behaviorSingleDesc": "同一时间只能有一个开关处于打开状态。打开一个会自动关闭所有其他开关。",
34
+ "masterSwitch": "主开关",
35
+ "masterSwitchDesc": "添加一个主开关,可以一次性打开或关闭所有开关。",
36
+ "switchSingular": "开关"
33
37
  }
@@ -52,29 +52,58 @@
52
52
  }
53
53
  .device-card {
54
54
  border: 2px solid var(--ui-device-border);
55
- border-radius: 10px; padding: 20px; margin-bottom: 20px;
55
+ border-radius: 10px; margin-bottom: 20px;
56
56
  background: var(--ui-device-bg);
57
+ overflow: hidden;
57
58
  }
58
59
  .device-header {
59
60
  display: flex; justify-content: space-between; align-items: center;
60
- margin-bottom: 16px; padding-bottom: 10px;
61
- border-bottom: 1px solid var(--ui-border);
61
+ padding: 14px 20px; cursor: pointer; user-select: none;
62
62
  }
63
- .device-header strong {
63
+ .device-header:hover { opacity: 0.85; }
64
+ .device-header-left {
65
+ display: flex; align-items: center; gap: 10px;
66
+ }
67
+ .device-header-left strong {
64
68
  font-size: 16px; color: var(--ui-accent);
65
69
  }
70
+ .device-header-left .chevron {
71
+ font-size: 12px; color: var(--ui-text-muted);
72
+ transition: transform 0.2s;
73
+ display: inline-block;
74
+ }
75
+ .device-header-left .chevron.open { transform: rotate(90deg); }
76
+ .device-header-right { display: flex; align-items: center; gap: 8px; }
77
+ .device-body { padding: 0 20px 20px; }
78
+ .device-body.collapsed { display: none; }
66
79
  .switch-card {
67
80
  border: 1px solid var(--ui-border);
68
- border-radius: 8px; padding: 16px; margin-bottom: 12px;
81
+ border-radius: 8px; margin-bottom: 12px;
69
82
  background: var(--ui-card-bg);
70
- position: relative;
83
+ position: relative; overflow: hidden;
71
84
  }
72
- .switch-card .switch-header {
73
- display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;
85
+ .switch-header {
86
+ display: flex; justify-content: space-between; align-items: center;
87
+ padding: 12px 16px; cursor: pointer; user-select: none;
74
88
  }
75
- .switch-card .switch-header strong {
76
- font-size: 14px;
77
- color: var(--ui-text);
89
+ .switch-header:hover { opacity: 0.85; }
90
+ .switch-header-left {
91
+ display: flex; align-items: center; gap: 8px;
92
+ }
93
+ .switch-header-left strong {
94
+ font-size: 14px; color: var(--ui-text);
95
+ }
96
+ .switch-header-left .chevron {
97
+ font-size: 11px; color: var(--ui-text-muted);
98
+ transition: transform 0.2s;
99
+ display: inline-block;
100
+ }
101
+ .switch-header-left .chevron.open { transform: rotate(90deg); }
102
+ .switch-header-right { display: flex; align-items: center; gap: 8px; }
103
+ .switch-body { padding: 0 16px 16px; }
104
+ .switch-body.collapsed { display: none; }
105
+ .switch-summary {
106
+ font-size: 12px; color: var(--ui-text-muted); margin-left: 4px;
78
107
  }
79
108
  .btn {
80
109
  padding: 8px 16px; border: none; border-radius: 6px;
@@ -82,6 +111,7 @@
82
111
  }
83
112
  .btn-danger { background: #dc3545; color: #fff; }
84
113
  .btn-danger:hover { background: #c82333; }
114
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
85
115
  .btn-primary { background: var(--ui-accent); color: #fff; }
86
116
  .btn-primary:hover { opacity: 0.85; }
87
117
  .btn-outline {
@@ -103,6 +133,11 @@
103
133
  background: var(--ui-input-bg);
104
134
  color: var(--ui-text);
105
135
  }
136
+ .master-option {
137
+ margin-top: 12px; padding: 12px 14px;
138
+ border: 1px dashed var(--ui-border);
139
+ border-radius: 8px; background: var(--ui-card-bg);
140
+ }
106
141
  </style>
107
142
 
108
143
  <div id="app"></div>
@@ -150,6 +185,15 @@
150
185
  config.devices = [];
151
186
  }
152
187
 
188
+ // Track expand/collapse state
189
+ const expandedDevices = new Set(config.devices.map((_, i) => i));
190
+ const expandedSwitches = new Set();
191
+ config.devices.forEach((dev, di) => {
192
+ (dev.switches || []).forEach((_, si) => {
193
+ expandedSwitches.add(`${di}_${si}`);
194
+ });
195
+ });
196
+
153
197
  const app = document.getElementById('app');
154
198
 
155
199
  function render() {
@@ -178,28 +222,76 @@
178
222
  });
179
223
 
180
224
  document.getElementById('btn-add-device').addEventListener('click', () => {
225
+ const di = config.devices.length;
181
226
  config.devices.push({
182
227
  name: '',
183
228
  switchBehavior: 'independent',
184
229
  switches: [{ name: '', type: 'outlet', defaultState: false, delayOff: 0 }],
185
230
  });
231
+ expandedDevices.add(di);
232
+ expandedSwitches.add(`${di}_0`);
186
233
  render();
187
234
  save();
188
235
  });
189
236
 
237
+ // Device toggle
238
+ document.querySelectorAll('.device-toggle').forEach(el => {
239
+ el.addEventListener('click', (e) => {
240
+ if (e.target.closest('.btn')) return;
241
+ const di = parseInt(el.dataset.dev);
242
+ if (expandedDevices.has(di)) {
243
+ expandedDevices.delete(di);
244
+ } else {
245
+ expandedDevices.add(di);
246
+ }
247
+ render();
248
+ });
249
+ });
250
+
251
+ // Switch toggle
252
+ document.querySelectorAll('.switch-toggle').forEach(el => {
253
+ el.addEventListener('click', (e) => {
254
+ if (e.target.closest('.btn')) return;
255
+ const key = `${el.dataset.dev}_${el.dataset.sw}`;
256
+ if (expandedSwitches.has(key)) {
257
+ expandedSwitches.delete(key);
258
+ } else {
259
+ expandedSwitches.add(key);
260
+ }
261
+ render();
262
+ });
263
+ });
264
+
190
265
  // Device-level events
191
266
  document.querySelectorAll('.dev-field').forEach(input => {
192
267
  input.addEventListener('change', () => {
193
268
  const di = parseInt(input.dataset.dev);
194
269
  const field = input.dataset.field;
195
270
  config.devices[di][field] = input.value;
271
+ // If switching away from single, disable masterSwitch
272
+ if (field === 'switchBehavior' && input.value !== 'single') {
273
+ delete config.devices[di].masterSwitch;
274
+ }
275
+ render();
276
+ save();
277
+ });
278
+ });
279
+
280
+ // Master switch toggle
281
+ document.querySelectorAll('.master-toggle').forEach(input => {
282
+ input.addEventListener('change', () => {
283
+ const di = parseInt(input.dataset.dev);
284
+ config.devices[di].masterSwitch = input.checked;
196
285
  save();
197
286
  });
198
287
  });
199
288
 
200
289
  document.querySelectorAll('.btn-remove-device').forEach(btn => {
201
- btn.addEventListener('click', () => {
202
- config.devices.splice(parseInt(btn.dataset.dev), 1);
290
+ btn.addEventListener('click', (e) => {
291
+ e.stopPropagation();
292
+ const di = parseInt(btn.dataset.dev);
293
+ config.devices.splice(di, 1);
294
+ rebuildExpandState();
203
295
  render();
204
296
  save();
205
297
  });
@@ -209,17 +301,21 @@
209
301
  document.querySelectorAll('.btn-add-switch').forEach(btn => {
210
302
  btn.addEventListener('click', () => {
211
303
  const di = parseInt(btn.dataset.dev);
304
+ const si = config.devices[di].switches.length;
212
305
  config.devices[di].switches.push({ name: '', type: 'outlet', defaultState: false, delayOff: 0 });
306
+ expandedSwitches.add(`${di}_${si}`);
213
307
  render();
214
308
  save();
215
309
  });
216
310
  });
217
311
 
218
312
  document.querySelectorAll('.btn-remove-switch').forEach(btn => {
219
- btn.addEventListener('click', () => {
313
+ btn.addEventListener('click', (e) => {
314
+ e.stopPropagation();
220
315
  const di = parseInt(btn.dataset.dev);
221
316
  const si = parseInt(btn.dataset.sw);
222
317
  config.devices[di].switches.splice(si, 1);
318
+ rebuildExpandState();
223
319
  render();
224
320
  save();
225
321
  });
@@ -242,69 +338,123 @@
242
338
  });
243
339
  }
244
340
 
341
+ function rebuildExpandState() {
342
+ const newDevices = new Set();
343
+ config.devices.forEach((_, i) => {
344
+ if (expandedDevices.has(i)) newDevices.add(i);
345
+ });
346
+ expandedDevices.clear();
347
+ newDevices.forEach(i => expandedDevices.add(i));
348
+
349
+ const newSwitches = new Set();
350
+ config.devices.forEach((dev, di) => {
351
+ (dev.switches || []).forEach((_, si) => {
352
+ if (expandedSwitches.has(`${di}_${si}`)) newSwitches.add(`${di}_${si}`);
353
+ });
354
+ });
355
+ expandedSwitches.clear();
356
+ newSwitches.forEach(k => expandedSwitches.add(k));
357
+ }
358
+
245
359
  function renderDevice(dev, di) {
246
360
  const switches = dev.switches || [];
361
+ const isOpen = expandedDevices.has(di);
362
+ const devLabel = dev.name || `${t.device || 'Device'} #${di + 1}`;
363
+ const switchCount = switches.length;
364
+ const isSingle = dev.switchBehavior === 'single';
365
+
247
366
  return `
248
367
  <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>
368
+ <div class="device-header device-toggle" data-dev="${di}">
369
+ <div class="device-header-left">
370
+ <span class="chevron ${isOpen ? 'open' : ''}">&#9654;</span>
371
+ <strong>${esc(devLabel)}</strong>
372
+ ${!isOpen ? `<span class="switch-summary">(${switchCount} ${switchCount === 1 ? (t.switchSingular || 'switch') : (t.switches || 'switches').toLowerCase()})</span>` : ''}
373
+ </div>
374
+ <div class="device-header-right">
375
+ <button class="btn btn-danger btn-sm btn-remove-device" data-dev="${di}">${t.removeDevice || 'Remove Device'}</button>
376
+ </div>
252
377
  </div>
253
378
 
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 || '')}">
379
+ <div class="device-body ${isOpen ? '' : 'collapsed'}">
380
+ <div class="inline-row">
381
+ <div class="form-group">
382
+ <label>${t.deviceName || 'Device Name'}</label>
383
+ <input type="text" class="dev-field" data-dev="${di}" data-field="name" value="${esc(dev.name || '')}">
384
+ </div>
385
+ <div class="form-group">
386
+ <label>${t.switchBehavior || 'Switch Behavior Mode'}</label>
387
+ <div class="desc">${isSingle ? (t.behaviorSingleDesc || '') : (t.behaviorIndependentDesc || '')}</div>
388
+ <select class="dev-field" data-dev="${di}" data-field="switchBehavior">
389
+ <option value="independent" ${dev.switchBehavior === 'independent' || !dev.switchBehavior ? 'selected' : ''}>${t.behaviorIndependent || 'Independent'}</option>
390
+ <option value="single" ${isSingle ? 'selected' : ''}>${t.behaviorSingle || 'Single'}</option>
391
+ </select>
392
+ </div>
258
393
  </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>
394
+
395
+ ${isSingle ? `
396
+ <div class="master-option">
397
+ <div class="toggle-wrap">
398
+ <input type="checkbox" class="master-toggle" data-dev="${di}" ${dev.masterSwitch ? 'checked' : ''}>
399
+ <div>
400
+ <label style="margin-bottom:0">${t.masterSwitch || 'Master Switch'}</label>
401
+ <div class="desc" style="margin-bottom:0">${t.masterSwitchDesc || ''}</div>
402
+ </div>
403
+ </div>
266
404
  </div>
267
- </div>
405
+ ` : ''}
268
406
 
269
- <div class="section-title">${t.switches || 'Switches'}</div>
407
+ <div class="section-title" style="margin-top:16px">${t.switches || 'Switches'}</div>
270
408
 
271
- ${switches.map((sw, si) => renderSwitch(sw, di, si)).join('')}
409
+ ${switches.map((sw, si) => renderSwitch(sw, di, si)).join('')}
272
410
 
273
- <button class="btn btn-primary btn-add-switch" data-dev="${di}">+ ${t.addSwitch || 'Add Switch'}</button>
411
+ <button class="btn btn-primary btn-add-switch" data-dev="${di}">+ ${t.addSwitch || 'Add Switch'}</button>
412
+ </div>
274
413
  </div>
275
414
  `;
276
415
  }
277
416
 
278
417
  function renderSwitch(sw, di, si) {
418
+ const isOpen = expandedSwitches.has(`${di}_${si}`);
419
+ const swLabel = sw.name || `#${si + 1}`;
420
+
279
421
  return `
280
422
  <div class="switch-card">
281
- <div class="switch-header">
282
- <strong>#${si + 1}</strong>
283
- <button class="btn btn-danger btn-remove-switch" data-dev="${di}" data-sw="${si}">${t.removeSwitch || 'Remove'}</button>
284
- </div>
285
- <div class="inline-row">
286
- <div class="form-group">
287
- <label>${t.switchName || 'Switch Name'}</label>
288
- <input type="text" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="name" value="${esc(sw.name || '')}">
423
+ <div class="switch-header switch-toggle" data-dev="${di}" data-sw="${si}">
424
+ <div class="switch-header-left">
425
+ <span class="chevron ${isOpen ? 'open' : ''}">&#9654;</span>
426
+ <strong>${esc(swLabel)}</strong>
427
+ ${!isOpen ? `<span class="switch-summary">${sw.type || 'outlet'}${sw.delayOff ? ` / ${sw.delayOff}ms` : ''}</span>` : ''}
289
428
  </div>
290
- <div class="form-group">
291
- <label>${t.switchType || 'Switch Type'}</label>
292
- <select class="sw-field" data-dev="${di}" data-sw="${si}" data-field="type">
293
- <option value="switch" ${sw.type === 'switch' ? 'selected' : ''}>${t.typeSwitch || 'Switch'}</option>
294
- <option value="outlet" ${sw.type === 'outlet' || !sw.type ? 'selected' : ''}>${t.typeOutlet || 'Outlet'}</option>
295
- </select>
429
+ <div class="switch-header-right">
430
+ <button class="btn btn-danger btn-sm btn-remove-switch" data-dev="${di}" data-sw="${si}">${t.removeSwitch || 'Remove'}</button>
296
431
  </div>
297
432
  </div>
298
- <div class="inline-row">
299
- <div class="form-group">
300
- <label>${t.delayOff || 'Auto Turn Off (ms)'}</label>
301
- <input type="number" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="delayOff" min="0" value="${sw.delayOff || 0}">
433
+ <div class="switch-body ${isOpen ? '' : 'collapsed'}">
434
+ <div class="inline-row">
435
+ <div class="form-group">
436
+ <label>${t.switchName || 'Switch Name'}</label>
437
+ <input type="text" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="name" value="${esc(sw.name || '')}">
438
+ </div>
439
+ <div class="form-group">
440
+ <label>${t.switchType || 'Switch Type'}</label>
441
+ <select class="sw-field" data-dev="${di}" data-sw="${si}" data-field="type">
442
+ <option value="switch" ${sw.type === 'switch' ? 'selected' : ''}>${t.typeSwitch || 'Switch'}</option>
443
+ <option value="outlet" ${sw.type === 'outlet' || !sw.type ? 'selected' : ''}>${t.typeOutlet || 'Outlet'}</option>
444
+ </select>
445
+ </div>
302
446
  </div>
303
- <div class="form-group">
304
- <label>${t.defaultState || 'Default State'}</label>
305
- <div class="toggle-wrap" style="margin-top:6px">
306
- <input type="checkbox" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="defaultState" ${sw.defaultState ? 'checked' : ''}>
307
- <span>${sw.defaultState ? (t.on || 'On') : (t.off || 'Off')}</span>
447
+ <div class="inline-row">
448
+ <div class="form-group">
449
+ <label>${t.delayOff || 'Auto Turn Off (ms)'}</label>
450
+ <input type="number" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="delayOff" min="0" value="${sw.delayOff || 0}">
451
+ </div>
452
+ <div class="form-group">
453
+ <label>${t.defaultState || 'Default State'}</label>
454
+ <div class="toggle-wrap" style="margin-top:6px">
455
+ <input type="checkbox" class="sw-field" data-dev="${di}" data-sw="${si}" data-field="defaultState" ${sw.defaultState ? 'checked' : ''}>
456
+ <span>${sw.defaultState ? (t.on || 'On') : (t.off || 'Off')}</span>
457
+ </div>
308
458
  </div>
309
459
  </div>
310
460
  </div>
package/index.js CHANGED
@@ -8,6 +8,8 @@ const SERVICE_TYPES = {
8
8
  outlet: 'Outlet',
9
9
  };
10
10
 
11
+ const MASTER_SUBTYPE = 'master_switch';
12
+
11
13
  module.exports = (api) => {
12
14
  api.registerPlatform(PLATFORM_NAME, MultipleSwitchPlatform);
13
15
  };
@@ -73,6 +75,7 @@ class MultipleSwitchPlatform {
73
75
 
74
76
  const name = device.name || 'Multiple Switch Panel';
75
77
  const behavior = device.switchBehavior || 'independent';
78
+ const hasMaster = behavior === 'single' && device.masterSwitch === true;
76
79
  const uuid = this.api.hap.uuid.generate(name);
77
80
 
78
81
  let accessory = this.cachedAccessories.get(uuid);
@@ -83,12 +86,13 @@ class MultipleSwitchPlatform {
83
86
  }
84
87
 
85
88
  accessory.context.switchBehavior = behavior;
89
+ accessory.context.hasMaster = hasMaster;
86
90
  accessory.context.switchStates = accessory.context.switchStates || {};
87
91
 
88
92
  const services = new Map();
89
93
  this.deviceServices.set(uuid, services);
90
94
 
91
- this.reconcileServices(accessory, switches, services);
95
+ this.reconcileServices(accessory, switches, services, hasMaster);
92
96
 
93
97
  if (isNew) {
94
98
  this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
@@ -97,9 +101,10 @@ class MultipleSwitchPlatform {
97
101
  this.cachedAccessories.delete(uuid);
98
102
  }
99
103
 
100
- reconcileServices(accessory, switches, services) {
104
+ reconcileServices(accessory, switches, services, hasMaster) {
101
105
  const activeSubtypes = new Set();
102
106
 
107
+ // Create regular switches
103
108
  switches.forEach((sw, index) => {
104
109
  const subtype = `switch_${index}`;
105
110
  activeSubtypes.add(subtype);
@@ -121,6 +126,26 @@ class MultipleSwitchPlatform {
121
126
  }
122
127
  });
123
128
 
129
+ // Create master switch if enabled
130
+ if (hasMaster) {
131
+ activeSubtypes.add(MASTER_SUBTYPE);
132
+
133
+ let masterService = accessory.getServiceById(this.Service.Switch, MASTER_SUBTYPE);
134
+ if (!masterService) {
135
+ masterService = accessory.addService(this.Service.Switch, 'Master', MASTER_SUBTYPE);
136
+ }
137
+
138
+ masterService.setCharacteristic(this.Characteristic.Name, 'Master');
139
+ this.configureMasterHandler(accessory, masterService, services);
140
+
141
+ services.set(MASTER_SUBTYPE, masterService);
142
+
143
+ if (accessory.context.switchStates[MASTER_SUBTYPE] === undefined) {
144
+ accessory.context.switchStates[MASTER_SUBTYPE] = false;
145
+ }
146
+ }
147
+
148
+ // Remove stale services
124
149
  const servicesToRemove = accessory.services.filter((s) => {
125
150
  return s.subtype && !activeSubtypes.has(s.subtype);
126
151
  });
@@ -140,32 +165,39 @@ class MultipleSwitchPlatform {
140
165
  this.turnOffOthers(accessory, subtype, services);
141
166
  }
142
167
 
143
- if (behavior === 'master') {
144
- this.setAll(accessory, value, services);
145
- }
146
-
147
168
  if (value && sw.delayOff > 0) {
148
169
  this.scheduleAutoOff(accessory, service, sw, subtype);
149
170
  }
150
171
  });
151
172
  }
152
173
 
174
+ configureMasterHandler(accessory, masterService, services) {
175
+ masterService.getCharacteristic(this.Characteristic.On)
176
+ .onGet(() => accessory.context.switchStates[MASTER_SUBTYPE] ?? false)
177
+ .onSet((value) => {
178
+ accessory.context.switchStates[MASTER_SUBTYPE] = value;
179
+ this.log.info(`[Master] ${value ? 'ON' : 'OFF'}`);
180
+
181
+ // Master ON → all regular switches ON
182
+ // Master OFF → all regular switches OFF
183
+ for (const [key, svc] of services) {
184
+ if (key !== MASTER_SUBTYPE) {
185
+ accessory.context.switchStates[key] = value;
186
+ svc.updateCharacteristic(this.Characteristic.On, value);
187
+ }
188
+ }
189
+ });
190
+ }
191
+
153
192
  turnOffOthers(accessory, excludeSubtype, services) {
154
193
  for (const [key, svc] of services) {
155
- if (key !== excludeSubtype) {
194
+ if (key !== excludeSubtype && key !== MASTER_SUBTYPE) {
156
195
  accessory.context.switchStates[key] = false;
157
196
  svc.updateCharacteristic(this.Characteristic.On, false);
158
197
  }
159
198
  }
160
199
  }
161
200
 
162
- setAll(accessory, value, services) {
163
- for (const [key, svc] of services) {
164
- accessory.context.switchStates[key] = value;
165
- svc.updateCharacteristic(this.Characteristic.On, value);
166
- }
167
- }
168
-
169
201
  scheduleAutoOff(accessory, service, sw, subtype) {
170
202
  setTimeout(() => {
171
203
  if (accessory.context.switchStates[subtype]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-multiple-switch",
3
- "version": "1.5.0",
3
+ "version": "1.6.0-beta.1",
4
4
  "description": "Multiple switch platform for Homebridge",
5
5
  "homepage": "https://github.com/azadaydinli/homebridge-multiple-switch",
6
6
  "main": "index.js",