homebridge-switcher-laundry 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 mirimalulkasif
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,87 @@
1
- # Temporary Holding Version
1
+ # Switcher Laundry for Homebridge
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Homebridge plugin that watches a **Switcher Power Plug** and tells HomeKit when a washer or dryer finishes.
4
+
5
+ Apple Home does not show live watts. Eve can. This plugin listens to the same local Switcher status broadcasts and turns power changes into sensors Home can automate.
6
+
7
+ ## What you get in Home
8
+
9
+ For each configured plug:
10
+
11
+ - **Done** — contact sensor that opens when a cycle finishes (use this for notifications)
12
+ - **Running** — occupancy sensor while the machine is drawing sustained power
13
+ - **Clear Done** — momentary switch to close the done sensor without waiting for the next cycle
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ sudo npm install -g homebridge-switcher-laundry
19
+ ```
20
+
21
+ Until the package is on npm, install from GitHub:
22
+
23
+ ```sh
24
+ sudo npm install -g github:mirimalulkasif/hb-switcher-laundry
25
+ ```
26
+
27
+ Restart Homebridge, then add **Switcher Laundry** from the Plugins page.
28
+
29
+ ## Configuration
30
+
31
+ ```json
32
+ {
33
+ "platforms": [
34
+ {
35
+ "platform": "SwitcherLaundry",
36
+ "name": "Switcher Laundry",
37
+ "devices": [
38
+ {
39
+ "name": "Washing Machine",
40
+ "identifier": "e4eaf6",
41
+ "startWatts": 5,
42
+ "stopWatts": 2,
43
+ "startSeconds": 60,
44
+ "stopSeconds": 180
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
50
+ ```
51
+
52
+ `identifier` can be the Switcher device id, a static IP, or the exact device name from the Switcher app.
53
+
54
+ ### Calibrating thresholds
55
+
56
+ 1. Run a normal wash with the plug connected.
57
+ 2. Note idle watts (often near 0–2 W) and active watts in Eve or the Switcher app.
58
+ 3. Set `startWatts` above idle noise and below normal running power.
59
+ 4. Set `stopWatts` near idle.
60
+ 5. Set `stopSeconds` longer than the longest low-power pause in the cycle (fill/rinse). Start around 3 minutes and raise it if “done” fires early.
61
+
62
+ ## How detection works
63
+
64
+ 1. Power stays at or above `startWatts` for `startSeconds` → **running**
65
+ 2. After a real start, power stays at or below `stopWatts` for `stopSeconds` → **done**
66
+ 3. The next sustained high-power stretch starts a new cycle and clears **done**
67
+
68
+ The plug must stay on the same LAN as Homebridge. No cloud login is required for reading wattage from Power Plug broadcasts.
69
+
70
+ ## Notifications
71
+
72
+ In the Home app, create an automation:
73
+
74
+ - When **Washing Machine Done** opens → send a notification (and optionally announce on a HomePod)
75
+
76
+ ## Development
77
+
78
+ ```sh
79
+ npm install
80
+ npm test
81
+ ```
82
+
83
+ `npm test` compiles TypeScript and runs cycle-detection and HomeKit registration tests without a physical Switcher.
84
+
85
+ ## License
86
+
87
+ Apache-2.0
@@ -0,0 +1,67 @@
1
+ {
2
+ "pluginAlias": "SwitcherLaundry",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "Watches a Switcher Power Plug and exposes HomeKit sensors when a laundry cycle finishes. Apple Home does not show live watts; this plugin listens to the same Switcher broadcasts Eve uses.",
6
+ "schema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "name": {
10
+ "title": "Name",
11
+ "type": "string",
12
+ "default": "Switcher Laundry",
13
+ "required": true
14
+ },
15
+ "devices": {
16
+ "title": "Laundry devices",
17
+ "type": "array",
18
+ "required": true,
19
+ "items": {
20
+ "type": "object",
21
+ "properties": {
22
+ "name": {
23
+ "title": "Name",
24
+ "type": "string",
25
+ "default": "Washing Machine",
26
+ "required": true
27
+ },
28
+ "identifier": {
29
+ "title": "Switcher identifier",
30
+ "type": "string",
31
+ "required": true,
32
+ "description": "Device id, IP address, or exact name from the Switcher app / Homebridge Switcher plugin logs."
33
+ },
34
+ "startWatts": {
35
+ "title": "Start threshold (W)",
36
+ "type": "number",
37
+ "default": 5,
38
+ "minimum": 0,
39
+ "description": "Power must stay at or above this value to count as a real cycle start."
40
+ },
41
+ "stopWatts": {
42
+ "title": "Stop threshold (W)",
43
+ "type": "number",
44
+ "default": 2,
45
+ "minimum": 0,
46
+ "description": "Power must stay at or below this value before the cycle can finish."
47
+ },
48
+ "startSeconds": {
49
+ "title": "Start confirm (seconds)",
50
+ "type": "integer",
51
+ "default": 60,
52
+ "minimum": 1,
53
+ "description": "How long power must stay high before marking the washer as running."
54
+ },
55
+ "stopSeconds": {
56
+ "title": "Stop confirm (seconds)",
57
+ "type": "integer",
58
+ "default": 180,
59
+ "minimum": 1,
60
+ "description": "How long power must stay low after running before marking the washer as done. Use longer than fill/rinse pauses."
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,34 @@
1
+ import type { PlatformAccessory } from 'homebridge';
2
+ import { type CycleSnapshot } from './cycle';
3
+ import type { SwitcherLaundryPlatform } from './platform';
4
+ import type { PowerSource } from './switcher-power';
5
+ export interface LaundryDeviceConfig {
6
+ name: string;
7
+ identifier: string;
8
+ startWatts: number;
9
+ stopWatts: number;
10
+ startSeconds: number;
11
+ stopSeconds: number;
12
+ }
13
+ export declare class LaundryAccessory {
14
+ private readonly platform;
15
+ private readonly accessory;
16
+ private readonly device;
17
+ private readonly power;
18
+ private readonly doneService;
19
+ private readonly runningService;
20
+ private readonly clearService;
21
+ private readonly detector;
22
+ private lastWatts;
23
+ private snapshot;
24
+ constructor(platform: SwitcherLaundryPlatform, accessory: PlatformAccessory, device: LaundryDeviceConfig, power: PowerSource);
25
+ stop(): void;
26
+ getSnapshot(): CycleSnapshot;
27
+ /** Used by tests to inject watt samples without UDP traffic. */
28
+ ingestWatts(watts: number, now?: number): CycleSnapshot;
29
+ private onPower;
30
+ private clearDone;
31
+ private applySnapshot;
32
+ private doneContactState;
33
+ private runningOccupancy;
34
+ }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LaundryAccessory = void 0;
4
+ const cycle_1 = require("./cycle");
5
+ class LaundryAccessory {
6
+ platform;
7
+ accessory;
8
+ device;
9
+ power;
10
+ doneService;
11
+ runningService;
12
+ clearService;
13
+ detector;
14
+ lastWatts = 0;
15
+ snapshot = {
16
+ phase: 'idle',
17
+ watts: 0,
18
+ running: false,
19
+ done: false,
20
+ changed: false,
21
+ };
22
+ constructor(platform, accessory, device, power) {
23
+ this.platform = platform;
24
+ this.accessory = accessory;
25
+ this.device = device;
26
+ this.power = power;
27
+ const { Characteristic, Service } = this.platform;
28
+ this.detector = new cycle_1.LaundryCycleDetector({
29
+ startWatts: device.startWatts,
30
+ stopWatts: device.stopWatts,
31
+ startSeconds: device.startSeconds,
32
+ stopSeconds: device.stopSeconds,
33
+ });
34
+ this.accessory.getService(Service.AccessoryInformation)
35
+ ?.setCharacteristic(Characteristic.Manufacturer, 'Switcher')
36
+ .setCharacteristic(Characteristic.Model, 'Power Plug Laundry')
37
+ .setCharacteristic(Characteristic.SerialNumber, device.identifier);
38
+ this.doneService = this.accessory.getService('Washer Done')
39
+ || this.accessory.addService(Service.ContactSensor, `${device.name} Done`, 'washer-done');
40
+ this.doneService.setCharacteristic(Characteristic.Name, `${device.name} Done`);
41
+ this.doneService.getCharacteristic(Characteristic.ContactSensorState)
42
+ .onGet(() => this.doneContactState());
43
+ this.runningService = this.accessory.getService('Washer Running')
44
+ || this.accessory.addService(Service.OccupancySensor, `${device.name} Running`, 'washer-running');
45
+ this.runningService.setCharacteristic(Characteristic.Name, `${device.name} Running`);
46
+ this.runningService.getCharacteristic(Characteristic.OccupancyDetected)
47
+ .onGet(() => this.runningOccupancy());
48
+ this.clearService = this.accessory.getService('Clear Done')
49
+ || this.accessory.addService(Service.Switch, `${device.name} Clear Done`, 'clear-done');
50
+ this.clearService.setCharacteristic(Characteristic.Name, `${device.name} Clear Done`);
51
+ this.clearService.getCharacteristic(Characteristic.On)
52
+ .onGet(() => false)
53
+ .onSet((value) => this.clearDone(value));
54
+ this.applySnapshot(this.snapshot, true);
55
+ this.power.on('power', (sample) => this.onPower(sample));
56
+ this.power.on('error', (error) => {
57
+ this.platform.log.error(`[${device.name}] ${error.message}`);
58
+ });
59
+ this.power.start();
60
+ }
61
+ stop() {
62
+ this.power.stop();
63
+ }
64
+ getSnapshot() {
65
+ return this.snapshot;
66
+ }
67
+ /** Used by tests to inject watt samples without UDP traffic. */
68
+ ingestWatts(watts, now = Date.now()) {
69
+ return this.onPower({
70
+ deviceId: this.device.identifier,
71
+ deviceIp: '127.0.0.1',
72
+ name: this.device.name,
73
+ watts,
74
+ on: true,
75
+ }, now);
76
+ }
77
+ onPower(sample, now = Date.now()) {
78
+ this.lastWatts = sample.watts;
79
+ const next = this.detector.update(sample.watts, now);
80
+ if (next.changed) {
81
+ this.platform.log.info(`[${this.device.name}] ${next.phase} at ${sample.watts} W`);
82
+ }
83
+ else {
84
+ this.platform.log.debug(`[${this.device.name}] ${sample.watts} W (${next.phase})`);
85
+ }
86
+ this.applySnapshot(next);
87
+ return next;
88
+ }
89
+ clearDone(value) {
90
+ if (!value) {
91
+ return;
92
+ }
93
+ const next = this.detector.acknowledge();
94
+ this.platform.log.info(`[${this.device.name}] cleared done state`);
95
+ this.applySnapshot(next);
96
+ setTimeout(() => {
97
+ this.clearService.updateCharacteristic(this.platform.Characteristic.On, false);
98
+ }, 250);
99
+ }
100
+ applySnapshot(snapshot, force = false) {
101
+ if (!force && !snapshot.changed && snapshot.watts === this.lastWatts && this.snapshot.phase === snapshot.phase) {
102
+ this.snapshot = snapshot;
103
+ return;
104
+ }
105
+ this.snapshot = snapshot;
106
+ const { Characteristic } = this.platform;
107
+ this.doneService.updateCharacteristic(Characteristic.ContactSensorState, this.doneContactState());
108
+ this.runningService.updateCharacteristic(Characteristic.OccupancyDetected, this.runningOccupancy());
109
+ }
110
+ doneContactState() {
111
+ const { Characteristic } = this.platform;
112
+ // Open contact while laundry is waiting to be emptied.
113
+ return this.snapshot.done
114
+ ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
115
+ : Characteristic.ContactSensorState.CONTACT_DETECTED;
116
+ }
117
+ runningOccupancy() {
118
+ const { Characteristic } = this.platform;
119
+ return this.snapshot.running
120
+ ? Characteristic.OccupancyDetected.OCCUPANCY_DETECTED
121
+ : Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED;
122
+ }
123
+ }
124
+ exports.LaundryAccessory = LaundryAccessory;
125
+ //# sourceMappingURL=accessory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessory.js","sourceRoot":"","sources":["../src/accessory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ export type CyclePhase = 'idle' | 'running' | 'done';
2
+ export interface CycleThresholds {
3
+ startWatts: number;
4
+ stopWatts: number;
5
+ startSeconds: number;
6
+ stopSeconds: number;
7
+ }
8
+ export interface CycleSnapshot {
9
+ phase: CyclePhase;
10
+ watts: number;
11
+ running: boolean;
12
+ done: boolean;
13
+ changed: boolean;
14
+ }
15
+ /**
16
+ * Tracks a laundry cycle from Switcher plug wattage.
17
+ * A cycle starts after sustained high power, and finishes only after
18
+ * sustained low power that is longer than normal fill/rinse pauses.
19
+ */
20
+ export declare class LaundryCycleDetector {
21
+ private readonly thresholds;
22
+ private phase;
23
+ private highSince;
24
+ private lowSince;
25
+ constructor(thresholds: CycleThresholds);
26
+ getPhase(): CyclePhase;
27
+ update(watts: number, now?: number): CycleSnapshot;
28
+ /** Clears a finished cycle without waiting for the next start. */
29
+ acknowledge(): CycleSnapshot;
30
+ }
package/dist/cycle.js ADDED
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LaundryCycleDetector = void 0;
4
+ /**
5
+ * Tracks a laundry cycle from Switcher plug wattage.
6
+ * A cycle starts after sustained high power, and finishes only after
7
+ * sustained low power that is longer than normal fill/rinse pauses.
8
+ */
9
+ class LaundryCycleDetector {
10
+ thresholds;
11
+ phase = 'idle';
12
+ highSince = null;
13
+ lowSince = null;
14
+ constructor(thresholds) {
15
+ this.thresholds = thresholds;
16
+ }
17
+ getPhase() {
18
+ return this.phase;
19
+ }
20
+ update(watts, now = Date.now()) {
21
+ const previous = this.phase;
22
+ const startMs = this.thresholds.startSeconds * 1000;
23
+ const stopMs = this.thresholds.stopSeconds * 1000;
24
+ if (watts >= this.thresholds.startWatts) {
25
+ this.lowSince = null;
26
+ if (this.highSince == null) {
27
+ this.highSince = now;
28
+ }
29
+ if (this.phase !== 'running' && now - this.highSince >= startMs) {
30
+ this.phase = 'running';
31
+ }
32
+ }
33
+ else if (watts <= this.thresholds.stopWatts) {
34
+ this.highSince = null;
35
+ if (this.phase === 'running') {
36
+ if (this.lowSince == null) {
37
+ this.lowSince = now;
38
+ }
39
+ if (now - this.lowSince >= stopMs) {
40
+ this.phase = 'done';
41
+ this.lowSince = null;
42
+ }
43
+ }
44
+ else if (this.phase === 'idle') {
45
+ this.lowSince = null;
46
+ }
47
+ }
48
+ else {
49
+ // Between thresholds: treat as inconclusive noise.
50
+ this.highSince = null;
51
+ this.lowSince = null;
52
+ }
53
+ return {
54
+ phase: this.phase,
55
+ watts,
56
+ running: this.phase === 'running',
57
+ done: this.phase === 'done',
58
+ changed: previous !== this.phase,
59
+ };
60
+ }
61
+ /** Clears a finished cycle without waiting for the next start. */
62
+ acknowledge() {
63
+ const previous = this.phase;
64
+ if (this.phase === 'done') {
65
+ this.phase = 'idle';
66
+ this.highSince = null;
67
+ this.lowSince = null;
68
+ }
69
+ return {
70
+ phase: this.phase,
71
+ watts: 0,
72
+ running: false,
73
+ done: false,
74
+ changed: previous !== this.phase,
75
+ };
76
+ }
77
+ }
78
+ exports.LaundryCycleDetector = LaundryCycleDetector;
79
+ //# sourceMappingURL=cycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cycle.js","sourceRoot":"","sources":["../src/cycle.ts"],"names":[],"mappings":";;;AAiBA;;;;GAIG;AACH,MAAa,oBAAoB;IAKF;IAJrB,KAAK,GAAe,MAAM,CAAC;IAC3B,SAAS,GAAkB,IAAI,CAAC;IAChC,QAAQ,GAAkB,IAAI,CAAC;IAEvC,YAA6B,UAA2B;QAA3B,eAAU,GAAV,UAAU,CAAiB;IAAG,CAAC;IAE5D,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,MAAM,CAAC,KAAa,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;QACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC;QAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,YAAY,GAAG,IAAI,CAAC;QACpD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,GAAG,IAAI,CAAC;QAElD,IAAI,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC;YACxC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,EAAE,CAAC;gBAC3B,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC;YACvB,CAAC;YACD,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,IAAI,OAAO,EAAE,CAAC;gBAChE,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;YACzB,CAAC;QACH,CAAC;aAAM,IAAI,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC;YAC9C,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;gBAC7B,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;oBAC1B,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC;gBACtB,CAAC;gBACD,IAAI,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,MAAM,EAAE,CAAC;oBAClC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC;oBACpB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;gBACvB,CAAC;YACH,CAAC;iBAAM,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;gBACjC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACvB,CAAC;QACH,CAAC;aAAM,CAAC;YACN,mDAAmD;YACnD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACvB,CAAC;QAED,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,KAAK;YACL,OAAO,EAAE,IAAI,CAAC,KAAK,KAAK,SAAS;YACjC,IAAI,EAAE,IAAI,CAAC,KAAK,KAAK,MAAM;YAC3B,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC,KAAK;SACjC,CAAC;IACJ,CAAC;IAED,kEAAkE;IAClE,WAAW;QACT,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC;QAC5B,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;YAC1B,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC;YACpB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACvB,CAAC;QACD,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,KAAK,EAAE,CAAC;YACR,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC,KAAK;SACjC,CAAC;IACJ,CAAC;CACF;AApED,oDAoEC"}
@@ -0,0 +1,3 @@
1
+ import type { API } from 'homebridge';
2
+ declare const _default: (api: API) => void;
3
+ export = _default;
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ const platform_1 = require("./platform");
3
+ const settings_1 = require("./settings");
4
+ module.exports = (api) => {
5
+ api.registerPlatform(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, platform_1.SwitcherLaundryPlatform);
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,yCAAqD;AACrD,yCAAwD;AAExD,iBAAS,CAAC,GAAQ,EAAQ,EAAE;IAC1B,GAAG,CAAC,gBAAgB,CAAC,sBAAW,EAAE,wBAAa,EAAE,kCAAuB,CAAC,CAAC;AAC5E,CAAC,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { API, Characteristic, DynamicPlatformPlugin, Logging, PlatformAccessory, PlatformConfig, Service } from 'homebridge';
2
+ import { type LaundryDeviceConfig } from './accessory';
3
+ import { type PowerSource } from './switcher-power';
4
+ export declare class SwitcherLaundryPlatform implements DynamicPlatformPlugin {
5
+ readonly log: Logging;
6
+ readonly config: PlatformConfig;
7
+ readonly api: API;
8
+ private readonly powerFactory;
9
+ readonly Service: typeof Service;
10
+ readonly Characteristic: typeof Characteristic;
11
+ private readonly cachedAccessories;
12
+ private readonly handlers;
13
+ ready: Promise<void>;
14
+ constructor(log: Logging, config: PlatformConfig, api: API, powerFactory?: (device: LaundryDeviceConfig) => PowerSource);
15
+ configureAccessory(accessory: PlatformAccessory): void;
16
+ private start;
17
+ private readDevices;
18
+ private normalizeDevice;
19
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SwitcherLaundryPlatform = void 0;
4
+ const accessory_1 = require("./accessory");
5
+ const settings_1 = require("./settings");
6
+ const switcher_power_1 = require("./switcher-power");
7
+ class SwitcherLaundryPlatform {
8
+ log;
9
+ config;
10
+ api;
11
+ powerFactory;
12
+ Service;
13
+ Characteristic;
14
+ cachedAccessories = [];
15
+ handlers = [];
16
+ ready = Promise.resolve();
17
+ constructor(log, config, api, powerFactory = (device) => new switcher_power_1.SwitcherBroadcastPowerSource(device.identifier, (message) => this.log.debug(message))) {
18
+ this.log = log;
19
+ this.config = config;
20
+ this.api = api;
21
+ this.powerFactory = powerFactory;
22
+ this.Service = api.hap.Service;
23
+ this.Characteristic = api.hap.Characteristic;
24
+ this.api.on('didFinishLaunching', () => {
25
+ this.ready = Promise.resolve().then(() => this.start());
26
+ });
27
+ this.api.on('shutdown', () => {
28
+ for (const handler of this.handlers) {
29
+ handler.stop();
30
+ }
31
+ });
32
+ }
33
+ configureAccessory(accessory) {
34
+ this.cachedAccessories.push(accessory);
35
+ this.log.debug(`Restoring cached accessory ${accessory.displayName}`);
36
+ }
37
+ start() {
38
+ const devices = this.readDevices();
39
+ if (devices.length === 0) {
40
+ this.log.error('Add at least one Switcher plug under devices in the plugin config');
41
+ return;
42
+ }
43
+ const activeIds = new Set();
44
+ for (const device of devices) {
45
+ const uuid = this.api.hap.uuid.generate(`${settings_1.PLUGIN_NAME}:${device.identifier}`);
46
+ activeIds.add(uuid);
47
+ const existing = this.cachedAccessories.find((item) => item.UUID === uuid);
48
+ const accessory = existing ?? new this.api.platformAccessory(device.name, uuid);
49
+ accessory.context.identifier = device.identifier;
50
+ const handler = new accessory_1.LaundryAccessory(this, accessory, device, this.powerFactory(device));
51
+ this.handlers.push(handler);
52
+ if (!existing) {
53
+ this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
54
+ }
55
+ this.log.info(`Watching ${device.name} (${device.identifier}) start≥${device.startWatts}W/${device.startSeconds}s stop≤${device.stopWatts}W/${device.stopSeconds}s`);
56
+ }
57
+ const stale = this.cachedAccessories.filter((item) => !activeIds.has(item.UUID));
58
+ if (stale.length > 0) {
59
+ this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, stale);
60
+ }
61
+ }
62
+ readDevices() {
63
+ const raw = Array.isArray(this.config.devices) ? this.config.devices : [];
64
+ return raw
65
+ .map((entry) => this.normalizeDevice(entry))
66
+ .filter((entry) => entry != null);
67
+ }
68
+ normalizeDevice(entry) {
69
+ if (!entry || typeof entry !== 'object') {
70
+ return null;
71
+ }
72
+ const item = entry;
73
+ const identifier = String(item.identifier ?? '').trim();
74
+ if (!identifier) {
75
+ this.log.error('Each laundry device needs an identifier (Switcher id, IP, or name)');
76
+ return null;
77
+ }
78
+ const name = String(item.name ?? identifier).trim() || identifier;
79
+ return {
80
+ name,
81
+ identifier,
82
+ startWatts: positiveNumber(item.startWatts, settings_1.DEFAULT_START_WATTS),
83
+ stopWatts: positiveNumber(item.stopWatts, settings_1.DEFAULT_STOP_WATTS),
84
+ startSeconds: positiveNumber(item.startSeconds, settings_1.DEFAULT_START_SECONDS),
85
+ stopSeconds: positiveNumber(item.stopSeconds, settings_1.DEFAULT_STOP_SECONDS),
86
+ };
87
+ }
88
+ }
89
+ exports.SwitcherLaundryPlatform = SwitcherLaundryPlatform;
90
+ function positiveNumber(value, fallback) {
91
+ const number = Number(value);
92
+ return Number.isFinite(number) && number >= 0 ? number : fallback;
93
+ }
94
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export declare const PLATFORM_NAME = "SwitcherLaundry";
2
+ export declare const PLUGIN_NAME = "homebridge-switcher-laundry";
3
+ export declare const DEFAULT_START_WATTS = 5;
4
+ export declare const DEFAULT_STOP_WATTS = 2;
5
+ export declare const DEFAULT_START_SECONDS = 60;
6
+ export declare const DEFAULT_STOP_SECONDS = 180;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_STOP_SECONDS = exports.DEFAULT_START_SECONDS = exports.DEFAULT_STOP_WATTS = exports.DEFAULT_START_WATTS = exports.PLUGIN_NAME = exports.PLATFORM_NAME = void 0;
4
+ exports.PLATFORM_NAME = 'SwitcherLaundry';
5
+ exports.PLUGIN_NAME = 'homebridge-switcher-laundry';
6
+ exports.DEFAULT_START_WATTS = 5;
7
+ exports.DEFAULT_STOP_WATTS = 2;
8
+ exports.DEFAULT_START_SECONDS = 60;
9
+ exports.DEFAULT_STOP_SECONDS = 180;
10
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":";;;AAAa,QAAA,aAAa,GAAG,iBAAiB,CAAC;AAClC,QAAA,WAAW,GAAG,6BAA6B,CAAC;AAE5C,QAAA,mBAAmB,GAAG,CAAC,CAAC;AACxB,QAAA,kBAAkB,GAAG,CAAC,CAAC;AACvB,QAAA,qBAAqB,GAAG,EAAE,CAAC;AAC3B,QAAA,oBAAoB,GAAG,GAAG,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { EventEmitter } from 'node:events';
2
+ export interface PowerSample {
3
+ deviceId: string;
4
+ deviceIp: string;
5
+ name: string;
6
+ watts: number;
7
+ on: boolean;
8
+ }
9
+ export interface PowerSource {
10
+ start(): void;
11
+ stop(): void;
12
+ on(event: 'power', listener: (sample: PowerSample) => void): this;
13
+ on(event: 'error', listener: (error: Error) => void): this;
14
+ off(event: 'power', listener: (sample: PowerSample) => void): this;
15
+ off(event: 'error', listener: (error: Error) => void): this;
16
+ }
17
+ /**
18
+ * Listens for Switcher UDP status broadcasts and emits watt samples.
19
+ * Power Plug devices advertise consumption without a TCP poll.
20
+ */
21
+ export declare class SwitcherBroadcastPowerSource extends EventEmitter implements PowerSource {
22
+ private readonly identifier;
23
+ private readonly log;
24
+ private listener?;
25
+ private started;
26
+ constructor(identifier: string, log?: (message: string) => void);
27
+ start(): void;
28
+ stop(): void;
29
+ private handleMessage;
30
+ }
31
+ /** Test double that feeds watt samples into the same PowerSource contract. */
32
+ export declare class FakePowerSource extends EventEmitter implements PowerSource {
33
+ start(): void;
34
+ stop(): void;
35
+ push(watts: number, extras?: Partial<PowerSample>): void;
36
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FakePowerSource = exports.SwitcherBroadcastPowerSource = void 0;
4
+ const node_events_1 = require("node:events");
5
+ // switcher-js2 ships as CommonJS without TypeScript types.
6
+ const Switcher = require('switcher-js2');
7
+ /**
8
+ * Listens for Switcher UDP status broadcasts and emits watt samples.
9
+ * Power Plug devices advertise consumption without a TCP poll.
10
+ */
11
+ class SwitcherBroadcastPowerSource extends node_events_1.EventEmitter {
12
+ identifier;
13
+ log;
14
+ listener;
15
+ started = false;
16
+ constructor(identifier, log = () => undefined) {
17
+ super();
18
+ this.identifier = identifier;
19
+ this.log = log;
20
+ }
21
+ start() {
22
+ if (this.started) {
23
+ return;
24
+ }
25
+ this.started = true;
26
+ this.listener = Switcher.listen((message) => this.log(message), this.identifier);
27
+ this.listener.on('message', (message) => this.handleMessage(message));
28
+ }
29
+ stop() {
30
+ if (!this.started) {
31
+ return;
32
+ }
33
+ this.started = false;
34
+ const closing = this.listener?.close();
35
+ this.listener = undefined;
36
+ if (closing && typeof closing.then === 'function') {
37
+ void closing.catch((error) => {
38
+ this.emit('error', error instanceof Error ? error : new Error(String(error)));
39
+ });
40
+ }
41
+ }
42
+ handleMessage(message) {
43
+ const watts = Number(message.state?.power_consumption);
44
+ if (!Number.isFinite(watts)) {
45
+ return;
46
+ }
47
+ this.emit('power', {
48
+ deviceId: message.device_id,
49
+ deviceIp: message.device_ip,
50
+ name: message.name,
51
+ watts,
52
+ on: Number(message.state?.power) === 1,
53
+ });
54
+ }
55
+ }
56
+ exports.SwitcherBroadcastPowerSource = SwitcherBroadcastPowerSource;
57
+ /** Test double that feeds watt samples into the same PowerSource contract. */
58
+ class FakePowerSource extends node_events_1.EventEmitter {
59
+ start() {
60
+ // No network work in tests.
61
+ }
62
+ stop() {
63
+ this.removeAllListeners();
64
+ }
65
+ push(watts, extras = {}) {
66
+ this.emit('power', {
67
+ deviceId: extras.deviceId ?? 'test',
68
+ deviceIp: extras.deviceIp ?? '127.0.0.1',
69
+ name: extras.name ?? 'Test Plug',
70
+ watts,
71
+ on: extras.on ?? true,
72
+ });
73
+ }
74
+ }
75
+ exports.FakePowerSource = FakePowerSource;
76
+ //# sourceMappingURL=switcher-power.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"switcher-power.js","sourceRoot":"","sources":["../src/switcher-power.ts"],"names":[],"mappings":";;;AAAA,6CAA2C;AAE3C,2DAA2D;AAC3D,MAAM,QAAQ,GAAG,OAAO,CAAC,cAAc,CAEtC,CAAC;AAmCF;;;GAGG;AACH,MAAa,4BAA6B,SAAQ,0BAAY;IAKzC;IACA;IALX,QAAQ,CAAoB;IAC5B,OAAO,GAAG,KAAK,CAAC;IAExB,YACmB,UAAkB,EAClB,MAAiC,GAAG,EAAE,CAAC,SAAS;QAEjE,KAAK,EAAE,CAAC;QAHS,eAAU,GAAV,UAAU,CAAQ;QAClB,QAAG,GAAH,GAAG,CAA6C;IAGnE,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QACjF,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,IAAI;QACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACrB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC;QACvC,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;QAC1B,IAAI,OAAO,IAAI,OAAQ,OAAyB,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACrE,KAAM,OAAyB,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;gBACvD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAChF,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAEO,aAAa,CAAC,OAAwB;QAC5C,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;YACjB,QAAQ,EAAE,OAAO,CAAC,SAAS;YAC3B,QAAQ,EAAE,OAAO,CAAC,SAAS;YAC3B,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,KAAK;YACL,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC;SACjB,CAAC,CAAC;IAC3B,CAAC;CACF;AA/CD,oEA+CC;AAED,8EAA8E;AAC9E,MAAa,eAAgB,SAAQ,0BAAY;IAC/C,KAAK;QACH,4BAA4B;IAC9B,CAAC;IAED,IAAI;QACF,IAAI,CAAC,kBAAkB,EAAE,CAAC;IAC5B,CAAC;IAED,IAAI,CAAC,KAAa,EAAE,SAA+B,EAAE;QACnD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;YACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,MAAM;YACnC,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,WAAW;YACxC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,WAAW;YAChC,KAAK;YACL,EAAE,EAAE,MAAM,CAAC,EAAE,IAAI,IAAI;SACA,CAAC,CAAC;IAC3B,CAAC;CACF;AAlBD,0CAkBC"}
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "homebridge-switcher-laundry",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "displayName": "Switcher Laundry",
4
+ "version": "1.0.0",
5
+ "description": "Detect when a washer or dryer finishes by watching a Switcher Power Plug wattage.",
6
+ "license": "Apache-2.0",
7
+ "author": "mirimalulkasif",
8
+ "homepage": "https://github.com/mirimalulkasif/hb-switcher-laundry#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/mirimalulkasif/hb-switcher-laundry.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/mirimalulkasif/hb-switcher-laundry/issues"
15
+ },
16
+ "keywords": [
17
+ "homebridge-plugin",
18
+ "supports-hap",
19
+ "switcher",
20
+ "laundry",
21
+ "washing-machine",
22
+ "power-plug"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18.0.0",
26
+ "homebridge": "^1.6.0 || ^2.0.0"
27
+ },
28
+ "main": "dist/index.js",
29
+ "files": [
30
+ "dist",
31
+ "config.schema.json",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "test": "npm run build && node --test test/*.test.js",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "dependencies": {
41
+ "switcher-js2": "^1.8.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.19.0",
45
+ "homebridge": "^1.11.0",
46
+ "typescript": "^5.9.0"
47
+ },
48
+ "peerDependencies": {
49
+ "homebridge": "^1.6.0 || ^2.0.0"
50
+ }
51
+ }