homebridge-multiple-switch 1.2.0 → 1.3.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.
@@ -11,31 +11,20 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
 
13
13
  steps:
14
- - name: 📥 Repo-nu klonla
15
- uses: actions/checkout@v3
14
+ - name: Checkout
15
+ uses: actions/checkout@v4
16
16
 
17
- - name: ⚙️ Node.js qur
18
- uses: actions/setup-node@v3
17
+ - name: Setup Node.js
18
+ uses: actions/setup-node@v4
19
19
  with:
20
20
  node-version: '18'
21
21
  cache: 'npm'
22
22
 
23
- - name: 📦 Asılılıqları qur
23
+ - name: Install dependencies
24
24
  run: npm install
25
25
 
26
- - name: ✅ Kod yoxlaması (lint varsa)
27
- run: |
28
- if [ -f .eslintrc.js ] || [ -f .eslintrc.json ]; then
29
- npm run lint
30
- else
31
- echo "Lint skripti tapılmadı, keçilir..."
32
- fi
33
-
34
- - name: 🧪 Testləri icra et (əgər varsa)
35
- run: |
36
- if [ -f package.json ] && grep -q '\"test\"' package.json; then
37
- npm test
38
- else
39
- echo "Test skripti yoxdur, keçilir..."
40
- fi
26
+ - name: Lint
27
+ run: npm run lint
41
28
 
29
+ - name: Test
30
+ run: npm test
@@ -1,4 +1,4 @@
1
- name: 📦 Publish to npm
1
+ name: Publish to npm
2
2
 
3
3
  on:
4
4
  push:
@@ -10,19 +10,19 @@ jobs:
10
10
  runs-on: ubuntu-latest
11
11
 
12
12
  steps:
13
- - name: 📥 Repo-nu klonla
14
- uses: actions/checkout@v3
13
+ - name: Checkout
14
+ uses: actions/checkout@v4
15
15
 
16
- - name: ⚙️ Node.js qur
17
- uses: actions/setup-node@v3
16
+ - name: Setup Node.js
17
+ uses: actions/setup-node@v4
18
18
  with:
19
19
  node-version: '18'
20
20
  registry-url: 'https://registry.npmjs.org/'
21
21
 
22
- - name: 📦 Asılılıqları qur
22
+ - name: Install dependencies
23
23
  run: npm install
24
24
 
25
- - name: 🚀 NPM-ə yüklə
25
+ - name: Publish to npm
26
26
  run: npm publish
27
27
  env:
28
28
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## [1.3.0] - 2026-03-21
4
+
5
+ ### Changed
6
+ - Replaced global mutable variables with instance properties
7
+ - Refactored accessory setup to reuse cached accessories instead of creating duplicates on restart
8
+ - Extracted switch behavior logic into dedicated methods (`turnOffOthers`, `setAll`, `scheduleAutoOff`)
9
+ - Used `Map` for service and accessory tracking instead of plain objects
10
+ - Service type lookup uses a constant map instead of a switch statement
11
+ - Auto-off now checks current state before turning off (prevents stale timeouts)
12
+ - Updated minimum Node.js version from 14 to 18
13
+ - Updated GitHub Actions from v3 to v4
14
+ - Cleaned up CI workflow
15
+
16
+ ### Fixed
17
+ - `delayOff` not working in `master` mode due to if/else logic bug
18
+ - Cached accessories not being restored on Homebridge restart (caused duplicate accessories)
19
+ - Services stored in `accessory.context` which is not serializable
20
+ - README field names (`autoTurnOff`, `mode`) now match actual config schema (`delayOff`, `switchBehavior`)
21
+
22
+ ### Added
23
+ - Automatic removal of stale cached accessories when config changes
24
+ - Service reconciliation: adds new services and removes old ones on config update
25
+ - Validation for empty or missing switches array
26
+
27
+ ## [1.2.0] - 2025-01-01
28
+
29
+ - Initial published version with independent, master, and single switch modes
30
+ - Support for switch, outlet, lightbulb, and fan accessory types
31
+ - Per-switch config: type, defaultState, delayOff
32
+ - Homebridge UI config schema
package/README.md CHANGED
@@ -5,26 +5,26 @@
5
5
  [![GitHub issues](https://img.shields.io/github/issues/azadaydinli/homebridge-multiple-switch)](https://github.com/azadaydinli/homebridge-multiple-switch/issues)
6
6
  [![GitHub license](https://img.shields.io/github/license/azadaydinli/homebridge-multiple-switch)](https://github.com/azadaydinli/homebridge-multiple-switch/blob/master/LICENSE)
7
7
 
8
- A lightweight Homebridge plugin that lets you create multiple customizable dummy switches under a single accessory — configurable as `Switch`, `Outlet`, `Lightbulb`, or `Fan`.
8
+ A lightweight Homebridge plugin that lets you create multiple customizable dummy switches under a single accessory — configurable as `Switch`, `Outlet`, `Lightbulb`, or `Fan`.
9
9
  Supports `Independent`, `Master`, and `Single` switch modes.
10
10
 
11
11
  ---
12
12
 
13
- ## Features
13
+ ## Features
14
14
 
15
15
  - Grouped multiple switches in one HomeKit tile
16
16
  - Accessory type: `switch`, `outlet`, `lightbulb`, or `fan`
17
17
  - **Independent Mode** – All switches operate separately
18
- - **Master Mode** – One master switch controls the rest
18
+ - **Master Mode** – One switch controls all others
19
19
  - **Single Mode** – Only one switch can be active at a time
20
- - Per-switch config support (type, auto-off, default state)
21
- - Switch states are preserved after Homebridge restart
22
- - Fully dynamic config reload (no need to restart Homebridge)
20
+ - Per-switch config support (type, auto-off delay, default state)
21
+ - Switch states preserved across Homebridge restarts via cached accessories
22
+ - Automatic cleanup of stale accessories on config change
23
23
  - Compatible with HomeKit and Siri
24
24
 
25
25
  ---
26
26
 
27
- ## 📦 Installation
27
+ ## Installation
28
28
 
29
29
  Install via Homebridge UI:
30
30
 
@@ -40,20 +40,21 @@ npm install -g homebridge-multiple-switch
40
40
 
41
41
  ---
42
42
 
43
- ## ⚙️ Configuration (Platform Mode)
43
+ ## Configuration
44
44
 
45
- Configure from Homebridge UI or manually edit `config.json` like below:
45
+ Configure from Homebridge UI or manually edit `config.json`:
46
46
 
47
47
  ```json
48
48
  {
49
49
  "platform": "MultipleSwitchPlatform",
50
50
  "name": "Multiple Switches",
51
+ "switchBehavior": "single",
51
52
  "switches": [
52
53
  {
53
54
  "name": "Heater",
54
55
  "type": "outlet",
55
56
  "defaultState": true,
56
- "autoTurnOff": 10000
57
+ "delayOff": 10000
57
58
  },
58
59
  {
59
60
  "name": "Fan",
@@ -62,35 +63,34 @@ Configure from Homebridge UI or manually edit `config.json` like below:
62
63
  {
63
64
  "name": "Light",
64
65
  "type": "lightbulb",
65
- "autoTurnOff": 5000
66
+ "delayOff": 5000
66
67
  }
67
- ],
68
- "mode": "single"
68
+ ]
69
69
  }
70
70
  ```
71
71
 
72
72
  ---
73
73
 
74
- ### 🔧 Configuration Options
74
+ ### Platform Options
75
75
 
76
- | Field | Type | Required | Description |
77
- |----------------|---------|----------|-------------------------------------------------------------------------|
78
- | `name` | string | | Name of the platform instance |
79
- | `switches` | array | | List of switches to create |
80
- | `mode` | string | | `independent`, `master`, or `single` |
81
- | `type` | string | ❌ | Switch type: `switch`, `outlet`, `lightbulb`, `fan` (overridden per switch) |
82
- | `autoTurnOff` | number | ❌ | Global auto-off (ms) – can be overridden per switch |
83
- | `defaultState` | boolean | ❌ | Default power state on restart – can be overridden per switch |
76
+ | Field | Type | Required | Description |
77
+ |------------------|--------|----------|--------------------------------------|
78
+ | `name` | string | Yes | Name of the platform instance |
79
+ | `switchBehavior` | string | No | `independent`, `master`, or `single` |
80
+ | `switches` | array | Yes | List of switches to create |
84
81
 
85
- Each object inside `switches[]` can include:
86
- - `name`: Name of the switch
87
- - `type`: Optional (`switch`, `outlet`, etc.)
88
- - `autoTurnOff`: Optional (in ms)
89
- - `defaultState`: Optional (true/false)
82
+ ### Per-Switch Options
83
+
84
+ | Field | Type | Required | Description |
85
+ |----------------|---------|----------|--------------------------------------------------|
86
+ | `name` | string | Yes | Name of the switch |
87
+ | `type` | string | No | `switch`, `outlet`, `lightbulb`, or `fan` |
88
+ | `defaultState` | boolean | No | Initial power state (default: `false`) |
89
+ | `delayOff` | number | No | Auto turn off after N milliseconds (default: `0`) |
90
90
 
91
91
  ---
92
92
 
93
- ## 📣 Example Use Cases
93
+ ## Example Use Cases
94
94
 
95
95
  - Simulate smart plugs for automation testing
96
96
  - Trigger HomeKit scenes manually
@@ -99,7 +99,7 @@ Each object inside `switches[]` can include:
99
99
 
100
100
  ---
101
101
 
102
- ## 🔗 Links
102
+ ## Links
103
103
 
104
104
  - [NPM Package](https://www.npmjs.com/package/homebridge-multiple-switch)
105
105
  - [Homebridge](https://homebridge.io/)
@@ -107,6 +107,6 @@ Each object inside `switches[]` can include:
107
107
 
108
108
  ---
109
109
 
110
- ## 📜 License
110
+ ## License
111
111
 
112
112
  MIT © [Azad Aydınlı](https://github.com/azadaydinli)
package/index.js CHANGED
@@ -1,109 +1,160 @@
1
- // homebridge-multiple-switch: index.js (Platform plugin, bir Accessory, çox Service)
1
+ 'use strict';
2
2
 
3
- let Service, Characteristic, UUIDGen;
3
+ const PLUGIN_NAME = 'homebridge-multiple-switch';
4
+ const PLATFORM_NAME = 'MultipleSwitchPlatform';
4
5
 
5
- module.exports = (api) => {
6
- Service = api.hap.Service;
7
- Characteristic = api.hap.Characteristic;
8
- UUIDGen = api.hap.uuid;
6
+ const SERVICE_TYPES = {
7
+ switch: 'Switch',
8
+ lightbulb: 'Lightbulb',
9
+ fan: 'Fan',
10
+ outlet: 'Outlet',
11
+ };
9
12
 
10
- api.registerPlatform('MultipleSwitchPlatform', MultipleSwitchPlatform);
13
+ module.exports = (api) => {
14
+ api.registerPlatform(PLATFORM_NAME, MultipleSwitchPlatform);
11
15
  };
12
16
 
13
17
  class MultipleSwitchPlatform {
14
18
  constructor(log, config, api) {
15
19
  this.log = log;
16
- this.config = config;
20
+ this.config = config || {};
17
21
  this.api = api;
18
- this.accessories = [];
22
+ this.Service = api.hap.Service;
23
+ this.Characteristic = api.hap.Characteristic;
24
+ this.cachedAccessories = new Map();
25
+ this.switchServices = new Map();
19
26
 
20
27
  this.api.on('didFinishLaunching', () => {
21
- this.log('🔌 MultipleSwitchPlatform başladıldı.');
28
+ this.log.info('MultipleSwitchPlatform started.');
22
29
  this.setupAccessories();
23
30
  });
24
31
  }
25
32
 
33
+ configureAccessory(accessory) {
34
+ this.cachedAccessories.set(accessory.UUID, accessory);
35
+ }
36
+
26
37
  setupAccessories() {
27
- const switches = this.config.switches || [];
28
- const behavior = this.config.switchBehavior || 'independent';
38
+ const switches = this.config.switches;
39
+ if (!Array.isArray(switches) || switches.length === 0) {
40
+ this.log.warn('No switches configured. Removing stale accessories.');
41
+ this.removeStaleCachedAccessories();
42
+ return;
43
+ }
44
+
29
45
  const name = this.config.name || 'Multiple Switch Panel';
46
+ const behavior = this.config.switchBehavior || 'independent';
47
+ const uuid = this.api.hap.uuid.generate(name);
48
+
49
+ let accessory = this.cachedAccessories.get(uuid);
50
+ const isNew = !accessory;
30
51
 
31
- const uuid = UUIDGen.generate(name);
32
- const accessory = new this.api.platformAccessory(name, uuid);
52
+ if (isNew) {
53
+ accessory = new this.api.platformAccessory(name, uuid);
54
+ }
33
55
 
34
- accessory.context.switchStates = {};
35
- accessory.context.switchServices = {};
36
56
  accessory.context.switchBehavior = behavior;
57
+ accessory.context.switchStates = accessory.context.switchStates || {};
58
+
59
+ this.reconcileServices(accessory, switches);
60
+
61
+ if (isNew) {
62
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
63
+ }
64
+
65
+ this.cachedAccessories.delete(uuid);
66
+ this.removeStaleCachedAccessories();
67
+ }
68
+
69
+ reconcileServices(accessory, switches) {
70
+ const activeSubtypes = new Set();
37
71
 
38
72
  switches.forEach((sw, index) => {
39
- const id = `switch_${index}`;
40
- const service = this.createSwitchService(accessory, sw, id);
73
+ const subtype = `switch_${index}`;
74
+ activeSubtypes.add(subtype);
41
75
 
42
- accessory.addService(service);
43
- accessory.context.switchStates[id] = sw.defaultState || false;
44
- accessory.context.switchServices[id] = service;
76
+ const ServiceClass = this.getServiceClass(sw.type);
77
+ let service = accessory.getServiceById(ServiceClass, subtype);
78
+
79
+ if (!service) {
80
+ service = accessory.addService(ServiceClass, sw.name, subtype);
81
+ }
82
+
83
+ service.setCharacteristic(this.Characteristic.Name, sw.name);
84
+ this.configureSwitchHandlers(accessory, service, sw, subtype);
85
+
86
+ this.switchServices.set(subtype, service);
87
+
88
+ if (accessory.context.switchStates[subtype] === undefined) {
89
+ accessory.context.switchStates[subtype] = sw.defaultState || false;
90
+ }
45
91
  });
46
92
 
47
- this.api.registerPlatformAccessories(
48
- 'homebridge-multiple-switch',
49
- 'MultipleSwitchPlatform',
50
- [accessory]
51
- );
52
- this.accessories.push(accessory);
93
+ const servicesToRemove = accessory.services.filter((s) => {
94
+ return s.subtype && !activeSubtypes.has(s.subtype);
95
+ });
96
+ servicesToRemove.forEach((s) => accessory.removeService(s));
53
97
  }
54
98
 
55
- createSwitchService(accessory, sw, id) {
56
- const ServiceType = this.getServiceClass(sw.type);
57
- const service = new ServiceType(sw.name, id);
58
-
59
- service.getCharacteristic(Characteristic.On)
60
- .onGet(() => {
61
- return accessory.context.switchStates[id];
62
- })
99
+ configureSwitchHandlers(accessory, service, sw, subtype) {
100
+ service.getCharacteristic(this.Characteristic.On)
101
+ .onGet(() => accessory.context.switchStates[subtype] ?? false)
63
102
  .onSet((value) => {
103
+ accessory.context.switchStates[subtype] = value;
104
+ this.log.info(`[${sw.name}] ${value ? 'ON' : 'OFF'}`);
105
+
64
106
  const behavior = accessory.context.switchBehavior;
65
- accessory.context.switchStates[id] = value;
66
- this.log(`[${sw.name}] → ${value ? 'ON' : 'OFF'}`);
67
107
 
68
108
  if (behavior === 'single' && value) {
69
- Object.keys(accessory.context.switchStates).forEach(key => {
70
- if (key !== id) {
71
- accessory.context.switchStates[key] = false;
72
- accessory.context.switchServices[key].updateCharacteristic(Characteristic.On, false);
73
- }
74
- });
109
+ this.turnOffOthers(accessory, subtype);
75
110
  }
76
111
 
77
112
  if (behavior === 'master') {
78
- Object.keys(accessory.context.switchStates).forEach(key => {
79
- accessory.context.switchStates[key] = value;
80
- accessory.context.switchServices[key].updateCharacteristic(Characteristic.On, value);
81
- });
82
- } else {
83
- if (value && sw.delayOff > 0) {
84
- setTimeout(() => {
85
- accessory.context.switchStates[id] = false;
86
- service.updateCharacteristic(Characteristic.On, false);
87
- this.log(`[${sw.name}] auto-off after ${sw.delayOff}ms`);
88
- }, sw.delayOff);
89
- }
113
+ this.setAll(accessory, value);
114
+ }
115
+
116
+ if (value && sw.delayOff > 0) {
117
+ this.scheduleAutoOff(accessory, service, sw, subtype);
90
118
  }
91
119
  });
120
+ }
92
121
 
93
- return service;
122
+ turnOffOthers(accessory, excludeSubtype) {
123
+ for (const [key, svc] of this.switchServices) {
124
+ if (key !== excludeSubtype) {
125
+ accessory.context.switchStates[key] = false;
126
+ svc.updateCharacteristic(this.Characteristic.On, false);
127
+ }
128
+ }
94
129
  }
95
130
 
96
- getServiceClass(type) {
97
- switch ((type || '').toLowerCase()) {
98
- case 'switch': return Service.Switch;
99
- case 'lightbulb': return Service.Lightbulb;
100
- case 'fan': return Service.Fan;
101
- case 'outlet':
102
- default: return Service.Outlet;
131
+ setAll(accessory, value) {
132
+ for (const [key, svc] of this.switchServices) {
133
+ accessory.context.switchStates[key] = value;
134
+ svc.updateCharacteristic(this.Characteristic.On, value);
103
135
  }
104
136
  }
105
137
 
106
- configureAccessory(accessory) {
107
- this.accessories.push(accessory);
138
+ scheduleAutoOff(accessory, service, sw, subtype) {
139
+ setTimeout(() => {
140
+ if (accessory.context.switchStates[subtype]) {
141
+ accessory.context.switchStates[subtype] = false;
142
+ service.updateCharacteristic(this.Characteristic.On, false);
143
+ this.log.info(`[${sw.name}] auto-off after ${sw.delayOff}ms`);
144
+ }
145
+ }, sw.delayOff);
146
+ }
147
+
148
+ getServiceClass(type) {
149
+ const key = (type || 'outlet').toLowerCase();
150
+ const name = SERVICE_TYPES[key] || SERVICE_TYPES.outlet;
151
+ return this.Service[name];
152
+ }
153
+
154
+ removeStaleCachedAccessories() {
155
+ if (this.cachedAccessories.size === 0) return;
156
+ const stale = [...this.cachedAccessories.values()];
157
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, stale);
158
+ this.cachedAccessories.clear();
108
159
  }
109
160
  }
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "homebridge-multiple-switch",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Multiple switch platform for Homebridge",
5
5
  "homepage": "https://github.com/azadaydinli/homebridge-multiple-switch",
6
6
  "main": "index.js",
7
7
  "author": "Azad Aydınlı",
8
8
  "license": "ISC",
9
+ "scripts": {
10
+ "lint": "echo \"No linter configured\"",
11
+ "test": "echo \"No tests configured\""
12
+ },
9
13
  "bugs": {
10
14
  "url": "https://github.com/azadaydinli/homebridge-multiple-switch/issues"
11
15
  },
@@ -26,7 +30,7 @@
26
30
  "url": "https://github.com/azadaydinli/homebridge-multiple-switch.git"
27
31
  },
28
32
  "engines": {
29
- "node": ">=14.17.0",
33
+ "node": ">=18.0.0",
30
34
  "homebridge": ">=1.3.0"
31
35
  }
32
36
  }
@@ -1,28 +0,0 @@
1
- name: 📦 Publish to npm
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*.*.*'
7
-
8
- jobs:
9
- publish:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: 📥 Repo-nu klonla
14
- uses: actions/checkout@v3
15
-
16
- - name: ⚙️ Node.js qur
17
- uses: actions/setup-node@v3
18
- with:
19
- node-version: '18'
20
- registry-url: 'https://registry.npmjs.org/'
21
-
22
- - name: 📦 Asılılıqları qur
23
- run: npm install
24
-
25
- - name: 🚀 NPM-ə yüklə
26
- run: npm publish
27
- env:
28
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -1,28 +0,0 @@
1
- name: 📦 Publish to npm
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*.*.*'
7
-
8
- jobs:
9
- publish:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: 📥 Repo-nu klonla
14
- uses: actions/checkout@v3
15
-
16
- - name: ⚙️ Node.js qur
17
- uses: actions/setup-node@v3
18
- with:
19
- node-version: '18'
20
- registry-url: 'https://registry.npmjs.org/'
21
-
22
- - name: 📦 Asılılıqları qur
23
- run: npm install
24
-
25
- - name: 🚀 NPM-ə yüklə
26
- run: npm publish
27
- env:
28
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -1,28 +0,0 @@
1
- name: 📦 Publish to npm
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*.*.*'
7
-
8
- jobs:
9
- publish:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: 📥 Repo-nu klonla
14
- uses: actions/checkout@v3
15
-
16
- - name: ⚙️ Node.js qur
17
- uses: actions/setup-node@v3
18
- with:
19
- node-version: '18'
20
- registry-url: 'https://registry.npmjs.org/'
21
-
22
- - name: 📦 Asılılıqları qur
23
- run: npm install
24
-
25
- - name: 🚀 NPM-ə yüklə
26
- run: npm publish
27
- env:
28
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}