homebridge-multiple-switch 1.1.6 → 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
@@ -1,34 +1,38 @@
1
1
  # homebridge-multiple-switch
2
2
 
3
+ ![CI](https://github.com/azadaydinli/homebridge-multiple-switch/actions/workflows/ci.yml/badge.svg)
3
4
  [![npm](https://img.shields.io/npm/v/homebridge-multiple-switch)](https://www.npmjs.com/package/homebridge-multiple-switch)
4
5
  [![GitHub issues](https://img.shields.io/github/issues/azadaydinli/homebridge-multiple-switch)](https://github.com/azadaydinli/homebridge-multiple-switch/issues)
5
6
  [![GitHub license](https://img.shields.io/github/license/azadaydinli/homebridge-multiple-switch)](https://github.com/azadaydinli/homebridge-multiple-switch/blob/master/LICENSE)
6
7
 
7
- A lightweight Homebridge plugin that lets you create multiple customizable dummy switches (Outlet/Fan/Light/Switch) with different behavior modes including Independent, Master, and Single-Switch Mode.
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
+ Supports `Independent`, `Master`, and `Single` switch modes.
8
10
 
9
11
  ---
10
12
 
11
- ## Features
13
+ ## Features
12
14
 
13
- - Multiple switches in a single accessory
14
- - Each switch can be `switch`, `outlet`, `lightbulb`, or `fan`
15
- - **Independent Mode** – all switches work separately
16
- - **Master Mode** – adds a master switch that controls all other switches
17
- - **Single Mode** – only one switch can be on at any time
18
- - Auto turn-off (in milliseconds)
19
- - Works seamlessly with HomeKit and Siri
15
+ - Grouped multiple switches in one HomeKit tile
16
+ - Accessory type: `switch`, `outlet`, `lightbulb`, or `fan`
17
+ - **Independent Mode** – All switches operate separately
18
+ - **Master Mode** – One switch controls all others
19
+ - **Single Mode** – Only one switch can be active at a time
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
+ - Compatible with HomeKit and Siri
20
24
 
21
25
  ---
22
26
 
23
- ## 📦 Installation
27
+ ## Installation
24
28
 
25
- Install the plugin via the Homebridge UI:
29
+ Install via Homebridge UI:
26
30
 
27
- 1. Go to **Plugins**
31
+ 1. Open **Plugins**
28
32
  2. Search for `homebridge-multiple-switch`
29
33
  3. Click **Install**
30
34
 
31
- Or use the command line:
35
+ Or install via terminal:
32
36
 
33
37
  ```bash
34
38
  npm install -g homebridge-multiple-switch
@@ -36,35 +40,73 @@ npm install -g homebridge-multiple-switch
36
40
 
37
41
  ---
38
42
 
39
- ## ⚙️ Configuration
43
+ ## Configuration
40
44
 
41
- You can configure the plugin directly via the Homebridge UI, or manually in config.json:
45
+ Configure from Homebridge UI or manually edit `config.json`:
42
46
 
43
- ```bash
47
+ ```json
44
48
  {
45
- "accessory": "MultipleSwitchAccessory",
46
- "name": "My Multi Switch",
47
- "switchCount": 3,
48
- "type": "outlet",
49
- "mode": "independent",
50
- "autoTurnOff": 1000,
51
- "defaultState": false,
52
- "states": [
53
- { "type": "switch", "autoTurnOff": 3000 },
54
- { "type": "fan" },
55
- { "type": "lightbulb", "defaultState": true }
49
+ "platform": "MultipleSwitchPlatform",
50
+ "name": "Multiple Switches",
51
+ "switchBehavior": "single",
52
+ "switches": [
53
+ {
54
+ "name": "Heater",
55
+ "type": "outlet",
56
+ "defaultState": true,
57
+ "delayOff": 10000
58
+ },
59
+ {
60
+ "name": "Fan",
61
+ "type": "fan"
62
+ },
63
+ {
64
+ "name": "Light",
65
+ "type": "lightbulb",
66
+ "delayOff": 5000
67
+ }
56
68
  ]
57
69
  }
58
70
  ```
59
71
 
60
- ### 🔧 Configuration Options
61
-
62
- | Field | Type | Required | Description |
63
- |---------------|---------|----------|-----------------------------------------------------------------------------|
64
- | `name` | string | | Name of the accessory |
65
- | `switchCount` | number | ✅ | Number of switches to create (max depends on HomeKit limits) |
66
- | `type` | string | | Default type: `switch`, `outlet`, `lightbulb`, or `fan` |
67
- | `mode` | string | | `independent`, `master`, or `single` |
68
- | `autoTurnOff` | number | | Global auto-off in milliseconds |
69
- | `defaultState`| boolean | ❌ | Default on/off state on restart |
70
- | `states` | array | ❌ | Per-switch custom settings (overrides global config) |
72
+ ---
73
+
74
+ ### Platform Options
75
+
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 |
81
+
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
+
91
+ ---
92
+
93
+ ## Example Use Cases
94
+
95
+ - Simulate smart plugs for automation testing
96
+ - Trigger HomeKit scenes manually
97
+ - Create virtual switches for non-HomeKit devices
98
+ - Combine several virtual accessories under one tile
99
+
100
+ ---
101
+
102
+ ## Links
103
+
104
+ - [NPM Package](https://www.npmjs.com/package/homebridge-multiple-switch)
105
+ - [Homebridge](https://homebridge.io/)
106
+ - [Plugin Issues](https://github.com/azadaydinli/homebridge-multiple-switch/issues)
107
+
108
+ ---
109
+
110
+ ## License
111
+
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.1.6",
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
+ }