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/homebridge-samsung-dryer-1.0.0.tgz +0 -0
- package/index.js +193 -0
- package/package.json +9 -0
|
Binary file
|
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
|
+
}
|