homebridge-samsung-dryer 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/index.js ADDED
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_NAME = 'homebridge-samsung-dryer';
4
+ const PLATFORM_NAME = 'SamsungDryer';
5
+ const ST_BASE = 'https://api.smartthings.com';
6
+
7
+ module.exports = (api) => {
8
+ api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, SamsungDryerPlatform);
9
+ };
10
+
11
+ async function stGet(token, path) {
12
+ const r = await fetch(`${ST_BASE}${path}`, { headers: { Authorization: `Bearer ${token}` } });
13
+ return r.json();
14
+ }
15
+
16
+ async function stCommand(token, deviceId, commands) {
17
+ await fetch(`${ST_BASE}/v1/devices/${deviceId}/commands`, {
18
+ method: 'POST',
19
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
20
+ body: JSON.stringify({ commands })
21
+ });
22
+ }
23
+
24
+ // Dry level → fan rotation % mapping (6 levels across 0-100)
25
+ const DRY_LEVELS = ['dampdry', 'less', 'normal', 'more', 'veryDry', 'extremeDry'];
26
+ function dryLevelToSpeed(level) {
27
+ const i = DRY_LEVELS.indexOf(level);
28
+ return i < 0 ? 50 : Math.round(((i + 1) / DRY_LEVELS.length) * 100);
29
+ }
30
+ function speedToDryLevel(speed) {
31
+ const i = Math.min(DRY_LEVELS.length - 1, Math.max(0, Math.floor((speed / 100) * DRY_LEVELS.length)));
32
+ return DRY_LEVELS[i];
33
+ }
34
+
35
+ class SamsungDryerPlatform {
36
+ constructor(log, config, api) {
37
+ this.log = log;
38
+ this.config = config;
39
+ this.api = api;
40
+ this.accessories = new Map();
41
+ this.token = config.accessToken;
42
+ this.deviceId = config.deviceId;
43
+ this.pollMs = (config.pollInterval || 30) * 1000;
44
+
45
+ api.on('didFinishLaunching', () => {
46
+ this.log.info('Samsung Dryer plugin started');
47
+ this.discoverDevices().catch(e => this.log.error('Init error:', e.message));
48
+ setInterval(() => this.pollStatus().catch(e => this.log.error('Poll error:', e.message)), this.pollMs);
49
+ });
50
+ }
51
+
52
+ configureAccessory(accessory) {
53
+ this.accessories.set(accessory.UUID, accessory);
54
+ }
55
+
56
+ getOrCreate(uuid, displayName, category) {
57
+ let acc = this.accessories.get(uuid);
58
+ if (!acc) {
59
+ acc = new this.api.platformAccessory(displayName, uuid);
60
+ acc.category = category;
61
+ this.accessories.set(uuid, acc);
62
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [acc]);
63
+ }
64
+ acc.getService(this.api.hap.Service.AccessoryInformation)
65
+ .setCharacteristic(this.api.hap.Characteristic.Manufacturer, 'Samsung')
66
+ .setCharacteristic(this.api.hap.Characteristic.Model, 'Dryer')
67
+ .setCharacteristic(this.api.hap.Characteristic.SerialNumber, this.deviceId)
68
+ .setCharacteristic(this.api.hap.Characteristic.Name, displayName);
69
+ return acc;
70
+ }
71
+
72
+ async discoverDevices() {
73
+ const { Service, Characteristic } = this.api.hap;
74
+ // Numeric category values — Categories export unreliable across HB versions
75
+ const CAT = { SENSOR: 10, SWITCH: 8, FAN: 3 };
76
+ const uuid = id => this.api.hap.uuid.generate(`samsung-dryer-${this.deviceId}-${id}`);
77
+
78
+ // 1. ContactSensor "Dryer" — OPEN when running/paused, CLOSED when stopped/done
79
+ // Best for automations: "notify me when Dryer stops detecting (cycle complete)"
80
+ const runAcc = this.getOrCreate(uuid('running'), 'Dryer', CAT.SENSOR);
81
+ this.runningSvc = runAcc.getService(Service.ContactSensor) || runAcc.addService(Service.ContactSensor, 'Dryer');
82
+ this.runningSvc.getCharacteristic(Characteristic.ContactSensorState).updateValue(0);
83
+
84
+ // 2. Switch "Dryer Power" — start (on) / stop (off) the cycle
85
+ const swAcc = this.getOrCreate(uuid('power'), 'Dryer Power', CAT.SWITCH);
86
+ this.powerSvc = swAcc.getService(Service.Switch) || swAcc.addService(Service.Switch, 'Dryer Power');
87
+ this.powerSvc.getCharacteristic(Characteristic.On)
88
+ .updateValue(false)
89
+ .on('set', async (val, cb) => {
90
+ try {
91
+ await stCommand(this.token, this.deviceId, [
92
+ { component: 'main', capability: 'switch', command: val ? 'on' : 'off' }
93
+ ]);
94
+ } catch (e) { this.log.error('Power set error:', e.message); }
95
+ cb(null);
96
+ });
97
+
98
+ // 3. TemperatureSensor "Dryer Temperature" — drum temperature
99
+ const tempAcc = this.getOrCreate(uuid('temp'), 'Dryer Temperature', CAT.SENSOR);
100
+ this.tempSvc = tempAcc.getService(Service.TemperatureSensor) || tempAcc.addService(Service.TemperatureSensor, 'Dryer Temperature');
101
+ this.tempSvc.getCharacteristic(Characteristic.CurrentTemperature).updateValue(0);
102
+
103
+ // 4. Switch "Wrinkle Prevent" — post-cycle tumbling to prevent wrinkles
104
+ const wrinkleAcc = this.getOrCreate(uuid('wrinkle'), 'Wrinkle Prevent', CAT.SWITCH);
105
+ this.wrinkleSvc = wrinkleAcc.getService(Service.Switch) || wrinkleAcc.addService(Service.Switch, 'Wrinkle Prevent');
106
+ this.wrinkleSvc.getCharacteristic(Characteristic.On)
107
+ .updateValue(false)
108
+ .on('set', async (val, cb) => {
109
+ try {
110
+ await stCommand(this.token, this.deviceId, [
111
+ { component: 'main', capability: 'samsungce.dryerWrinklePrevent', command: val ? 'on' : 'off' }
112
+ ]);
113
+ } catch (e) { this.log.error('Wrinkle set error:', e.message); }
114
+ cb(null);
115
+ });
116
+
117
+ // 5. Fan "Dry Level" — rotation speed maps to dryness level (dampdry→extremeDry)
118
+ const dryAcc = this.getOrCreate(uuid('drylevel'), 'Dry Level', CAT.FAN);
119
+ this.dryLevelSvc = dryAcc.getService(Service.Fan) || dryAcc.addService(Service.Fan, 'Dry Level');
120
+ this.dryLevelSvc.getCharacteristic(Characteristic.On)
121
+ .updateValue(false)
122
+ .on('set', (val, cb) => cb(null)); // on/off state managed by poll
123
+ this.dryLevelSvc.getCharacteristic(Characteristic.RotationSpeed)
124
+ .setProps({ minValue: 0, maxValue: 100, minStep: 17 })
125
+ .updateValue(50)
126
+ .on('set', async (speed, cb) => {
127
+ try {
128
+ const level = speedToDryLevel(speed);
129
+ await stCommand(this.token, this.deviceId, [
130
+ { component: 'main', capability: 'custom.dryerDryLevel', command: 'setDryerDryLevel', arguments: [level] }
131
+ ]);
132
+ this.log.info(`Dry level set to ${level} (${speed}%)`);
133
+ } catch (e) { this.log.error('Dry level set error:', e.message); }
134
+ cb(null);
135
+ });
136
+
137
+ this.log.info('Dryer accessories registered, polling...');
138
+ await this.pollStatus();
139
+ }
140
+
141
+ async pollStatus() {
142
+ try {
143
+ const { Characteristic } = this.api.hap;
144
+ const status = await stGet(this.token, `/v1/devices/${this.deviceId}/components/main/status`);
145
+
146
+ // machineState: run | pause | stop | idle
147
+ const machineState = status['washerOperatingState']?.machineState?.value;
148
+ const switchState = status['switch']?.switch?.value;
149
+ const isRunning = machineState === 'run' || machineState === 'pause' || switchState === 'on';
150
+
151
+ if (this.runningSvc) {
152
+ // ContactSensor: 1=open(detected/running), 0=closed(clear/done)
153
+ this.runningSvc.updateCharacteristic(Characteristic.ContactSensorState, isRunning ? 1 : 0);
154
+ }
155
+ if (this.powerSvc) {
156
+ this.powerSvc.updateCharacteristic(Characteristic.On, isRunning);
157
+ }
158
+
159
+ // Temperature — Samsung dryers report in °F; convert to °C for HomeKit
160
+ const rawTemp = status['temperatureMeasurement']?.temperature?.value;
161
+ const tempUnit = status['temperatureMeasurement']?.temperature?.unit;
162
+ if (rawTemp !== undefined && this.tempSvc) {
163
+ const tempC = (tempUnit === 'F' || tempUnit === undefined) ? (rawTemp - 32) * 5 / 9 : rawTemp;
164
+ const clamped = Math.max(-270, Math.min(100, tempC));
165
+ this.tempSvc.updateCharacteristic(Characteristic.CurrentTemperature, clamped);
166
+ }
167
+
168
+ // Wrinkle prevent
169
+ const wrinkle = status['samsungce.dryerWrinklePrevent']?.wrinklePrevent?.value;
170
+ if (wrinkle !== undefined && this.wrinkleSvc) {
171
+ this.wrinkleSvc.updateCharacteristic(Characteristic.On, wrinkle === 'on' || wrinkle === true);
172
+ }
173
+
174
+ // Dry level → fan speed
175
+ const dryLevel = status['custom.dryerDryLevel']?.dryLevel?.value;
176
+ if (dryLevel && this.dryLevelSvc) {
177
+ const speed = dryLevelToSpeed(dryLevel);
178
+ this.dryLevelSvc.updateCharacteristic(Characteristic.On, isRunning);
179
+ this.dryLevelSvc.updateCharacteristic(Characteristic.RotationSpeed, speed);
180
+ }
181
+
182
+ // Log completion time if available
183
+ const completion = status['washerOperatingState']?.completionTime?.value;
184
+ if (completion && machineState === 'run') {
185
+ this.log.debug(`Completion time: ${completion}`);
186
+ }
187
+
188
+ this.log.debug(`Poll: state=${machineState || switchState}, temp=${rawTemp}${tempUnit || 'F'}, dryLevel=${dryLevel}`);
189
+ } catch (e) {
190
+ this.log.error('Poll error:', e.message);
191
+ }
192
+ }
193
+ }
package/package.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "homebridge-samsung-dryer",
3
+ "version": "1.0.0",
4
+ "description": "Homebridge plugin for Samsung dryer via SmartThings — exposes run state, temperature, dry level, wrinkle prevent",
5
+ "main": "index.js",
6
+ "keywords": ["homebridge-plugin","samsung","dryer","smartthings"],
7
+ "engines": { "homebridge": ">=1.3.0", "node": ">=14.0.0" },
8
+ "dependencies": {}
9
+ }