@boozilla/homebridge-shome 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.
Files changed (44) hide show
  1. package/.idea/aws.xml +11 -0
  2. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  3. package/.idea/homebridge-shome.iml +9 -0
  4. package/.idea/misc.xml +6 -0
  5. package/.idea/modules.xml +8 -0
  6. package/.idea/vcs.xml +6 -0
  7. package/README.md +66 -0
  8. package/config.schema.json +32 -0
  9. package/dist/accessories/doorlockAccessory.d.ts +10 -0
  10. package/dist/accessories/doorlockAccessory.js +55 -0
  11. package/dist/accessories/doorlockAccessory.js.map +1 -0
  12. package/dist/accessories/heaterAccessory.d.ts +16 -0
  13. package/dist/accessories/heaterAccessory.js +75 -0
  14. package/dist/accessories/heaterAccessory.js.map +1 -0
  15. package/dist/accessories/lightAccessory.d.ts +10 -0
  16. package/dist/accessories/lightAccessory.js +32 -0
  17. package/dist/accessories/lightAccessory.js.map +1 -0
  18. package/dist/accessories/ventilatorAccessory.d.ts +12 -0
  19. package/dist/accessories/ventilatorAccessory.js +65 -0
  20. package/dist/accessories/ventilatorAccessory.js.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.js +6 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/platform.d.ts +16 -0
  25. package/dist/platform.js +98 -0
  26. package/dist/platform.js.map +1 -0
  27. package/dist/settings.d.ts +8 -0
  28. package/dist/settings.js +9 -0
  29. package/dist/settings.js.map +1 -0
  30. package/dist/shomeClient.d.ts +30 -0
  31. package/dist/shomeClient.js +163 -0
  32. package/dist/shomeClient.js.map +1 -0
  33. package/eslint.config.js +35 -0
  34. package/nodemon.json +12 -0
  35. package/package.json +50 -0
  36. package/src/accessories/doorlockAccessory.ts +62 -0
  37. package/src/accessories/heaterAccessory.ts +94 -0
  38. package/src/accessories/lightAccessory.ts +41 -0
  39. package/src/accessories/ventilatorAccessory.ts +78 -0
  40. package/src/index.ts +7 -0
  41. package/src/platform.ts +113 -0
  42. package/src/settings.ts +9 -0
  43. package/src/shomeClient.ts +196 -0
  44. package/tsconfig.json +23 -0
@@ -0,0 +1,113 @@
1
+ import { API, Characteristic, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service } from 'homebridge';
2
+ import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
3
+ import { ShomeClient, MainDevice, SubDevice } from './shomeClient.js';
4
+ import { LightAccessory } from './accessories/lightAccessory.js';
5
+ import { VentilatorAccessory } from './accessories/ventilatorAccessory.js';
6
+ import { HeaterAccessory } from './accessories/heaterAccessory.js';
7
+ import { DoorlockAccessory } from './accessories/doorlockAccessory.js';
8
+
9
+ const CONTROLLABLE_MULTI_DEVICE_TYPES = ['LIGHT', 'HEATER', 'VENTILATOR'];
10
+ const SPECIAL_CONTROLLABLE_TYPES = ['DOORLOCK'];
11
+
12
+ export class ShomePlatform implements DynamicPlatformPlugin {
13
+ public readonly Service: typeof Service;
14
+ public readonly Characteristic: typeof Characteristic;
15
+ public readonly accessories: PlatformAccessory[] = [];
16
+ public readonly shomeClient: ShomeClient;
17
+
18
+ constructor(
19
+ public readonly log: Logger,
20
+ public readonly config: PlatformConfig,
21
+ public readonly api: API,
22
+ ) {
23
+ this.Service = this.api.hap.Service;
24
+ this.Characteristic = this.api.hap.Characteristic;
25
+
26
+ this.shomeClient = new ShomeClient(
27
+ this.log,
28
+ this.config.username,
29
+ this.config.password,
30
+ this.config.deviceId,
31
+ );
32
+
33
+ this.api.on('didFinishLaunching', () => {
34
+ this.discoverDevices();
35
+ });
36
+ }
37
+
38
+ configureAccessory(accessory: PlatformAccessory) {
39
+ this.accessories.push(accessory);
40
+ }
41
+
42
+ async discoverDevices() {
43
+ const devices = await this.shomeClient.getDeviceList();
44
+ const foundAccessories: PlatformAccessory[] = [];
45
+
46
+ for (const device of devices) {
47
+ if (CONTROLLABLE_MULTI_DEVICE_TYPES.includes(device.thngModelTypeName)) {
48
+ const deviceInfoList = await this.shomeClient.getDeviceInfo(device.thngId, device.thngModelTypeName);
49
+
50
+ if (deviceInfoList) {
51
+ for (const subDevice of deviceInfoList) {
52
+ const uuid = this.api.hap.uuid.generate(`${device.thngId}-${subDevice.deviceId}`);
53
+ const accessory = this.setupAccessory(device, subDevice, uuid);
54
+ foundAccessories.push(accessory);
55
+ }
56
+ }
57
+ } else if (SPECIAL_CONTROLLABLE_TYPES.includes(device.thngModelTypeName)) {
58
+ const uuid = this.api.hap.uuid.generate(device.thngId);
59
+ const accessory = this.setupAccessory(device, null, uuid);
60
+ foundAccessories.push(accessory);
61
+ } else {
62
+ this.log.info(`Ignoring device: ${device.nickname} (Type: ${device.thngModelTypeName})`);
63
+ }
64
+ }
65
+
66
+ const accessoriesToRemove = this.accessories.filter(cachedAccessory =>
67
+ !foundAccessories.some(foundAccessory => foundAccessory.UUID === cachedAccessory.UUID),
68
+ );
69
+ if (accessoriesToRemove.length > 0) {
70
+ this.log.info('Removing stale accessories:', accessoriesToRemove.map(a => a.displayName));
71
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessoriesToRemove);
72
+ }
73
+ }
74
+
75
+ setupAccessory(mainDevice: MainDevice, subDevice: SubDevice | null, uuid: string): PlatformAccessory {
76
+ const displayName = subDevice ? subDevice.nickname : mainDevice.nickname;
77
+ const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
78
+
79
+ if (existingAccessory) {
80
+ this.log.info('Restoring existing accessory from cache:', displayName);
81
+ existingAccessory.context.device = mainDevice;
82
+ existingAccessory.context.subDevice = subDevice;
83
+ this.createAccessory(existingAccessory);
84
+ return existingAccessory;
85
+ } else {
86
+ this.log.info('Adding new accessory:', displayName);
87
+ const accessory = new this.api.platformAccessory(displayName, uuid);
88
+ accessory.context.device = mainDevice;
89
+ accessory.context.subDevice = subDevice;
90
+ this.createAccessory(accessory);
91
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
92
+ return accessory;
93
+ }
94
+ }
95
+
96
+ createAccessory(accessory: PlatformAccessory) {
97
+ const device = accessory.context.device;
98
+ switch (device.thngModelTypeName) {
99
+ case 'LIGHT':
100
+ new LightAccessory(this, accessory);
101
+ break;
102
+ case 'VENTILATOR':
103
+ new VentilatorAccessory(this, accessory);
104
+ break;
105
+ case 'HEATER':
106
+ new HeaterAccessory(this, accessory);
107
+ break;
108
+ case 'DOORLOCK':
109
+ new DoorlockAccessory(this, accessory);
110
+ break;
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * This is the name of the platform that users will use to register the plugin in the Homebridge config.json
3
+ */
4
+ export const PLATFORM_NAME = 'sHome';
5
+
6
+ /**
7
+ * This must match the name of your plugin as defined the package.json
8
+ */
9
+ export const PLUGIN_NAME = 'homebridge-shome';
@@ -0,0 +1,196 @@
1
+ import axios from 'axios';
2
+ import CryptoJS from 'crypto-js';
3
+ import { Logger } from 'homebridge';
4
+
5
+ const BASE_URL = 'https://shome-api.samsung-ihp.com';
6
+ const APP_REGST_ID = '6110736314d9eef6baf393f3e43a5342f9ccde6ef300d878385acd9264cf14d5';
7
+ const CHINA_APP_REGST_ID = 'SHOME==6110736314d9eef6baf393f3e43a5342f9ccde6ef300d878385acd9264cf14d5';
8
+ const LANGUAGE = 'KOR';
9
+
10
+ // Define and export interfaces for device types
11
+ export interface MainDevice {
12
+ thngId: string;
13
+ thngModelTypeName: string;
14
+ nickname: string;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ export interface SubDevice {
19
+ deviceId: string;
20
+ nickname: string;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ export class ShomeClient {
25
+ private cachedAccessToken: string | null = null;
26
+ private ihdId: string | null = null;
27
+ private tokenExpiry: number = 0;
28
+
29
+ constructor(
30
+ private readonly log: Logger,
31
+ private readonly username: string,
32
+ private readonly password: string,
33
+ private readonly deviceId: string,
34
+ ) {
35
+ }
36
+
37
+ async login(): Promise<string | null> {
38
+ if (!this.isTokenExpired()) {
39
+ return this.cachedAccessToken;
40
+ }
41
+
42
+ try {
43
+ const createDate = this.getDateTime();
44
+ const hashedPassword = this.sha512(this.password);
45
+ const hashData = this.sha512(`IHRESTAPI${this.username}${hashedPassword}${this.deviceId}` +
46
+ `${APP_REGST_ID}${CHINA_APP_REGST_ID}${LANGUAGE}${createDate}`);
47
+
48
+ const response = await axios.put(`${BASE_URL}/v18/users/login`, null, {
49
+ params: {
50
+ appRegstId: APP_REGST_ID,
51
+ chinaAppRegstId: CHINA_APP_REGST_ID,
52
+ createDate: createDate,
53
+ hashData: hashData,
54
+ language: LANGUAGE,
55
+ mobileDeviceIdno: this.deviceId,
56
+ password: hashedPassword,
57
+ userId: this.username,
58
+ },
59
+ });
60
+
61
+ if (response.data && response.data.accessToken) {
62
+ this.cachedAccessToken = response.data.accessToken;
63
+ this.ihdId = response.data.ihdId;
64
+
65
+ // Decode token to find expiry
66
+ const payload = JSON.parse(Buffer.from(this.cachedAccessToken!.split('.')[1], 'base64').toString());
67
+ this.tokenExpiry = payload.exp * 1000;
68
+
69
+ this.log.info('Successfully logged in to sHome API.');
70
+ return this.cachedAccessToken;
71
+ } else {
72
+ this.log.error('Login failed: Invalid credentials or API error.');
73
+ return null;
74
+ }
75
+ } catch (error) {
76
+ this.log.error(`Login error: ${error}`);
77
+ return null;
78
+ }
79
+ }
80
+
81
+ async getDeviceList(): Promise<MainDevice[]> {
82
+ const token = await this.login();
83
+ if (!token || !this.ihdId) {
84
+ return [];
85
+ }
86
+
87
+ try {
88
+ const createDate = this.getDateTime();
89
+ const hashData = this.sha512(`IHRESTAPI${this.ihdId}${createDate}`);
90
+
91
+ const response = await axios.get(`${BASE_URL}/v16/settings/${this.ihdId}/devices/`, {
92
+ params: { createDate, hashData },
93
+ headers: { 'Authorization': `Bearer ${token}` },
94
+ });
95
+
96
+ return response.data.deviceList || [];
97
+ } catch (error) {
98
+ this.log.error(`Error getting device list: ${error}`);
99
+ return [];
100
+ }
101
+ }
102
+
103
+ async getDeviceInfo(thingId: string, type: string): Promise<SubDevice[] | null> {
104
+ const token = await this.login();
105
+ if (!token) {
106
+ return null;
107
+ }
108
+
109
+ try {
110
+ const createDate = this.getDateTime();
111
+ const hashData = this.sha512(`IHRESTAPI${thingId}${createDate}`);
112
+ const typePath = type.toLowerCase().replace(/_/g, '');
113
+
114
+ const response = await axios.get(`${BASE_URL}/v18/settings/${typePath}/${thingId}`, {
115
+ params: { createDate, hashData },
116
+ headers: { 'Authorization': `Bearer ${token}` },
117
+ });
118
+
119
+ return response.data.deviceInfoList || null;
120
+ } catch (error) {
121
+ this.log.error(`Error getting device info for ${thingId}: ${error}`);
122
+ return null;
123
+ }
124
+ }
125
+
126
+ async setDevice(thingId: string, deviceId: string, type: string, controlType: string, state: string): Promise<boolean> {
127
+ const token = await this.login();
128
+ if (!token) {
129
+ return false;
130
+ }
131
+
132
+ try {
133
+ const createDate = this.getDateTime();
134
+ const hashData = this.sha512(`IHRESTAPI${thingId}${deviceId}${state}${createDate}`);
135
+ const typePath = type.toLowerCase().replace(/_/g, '');
136
+ const controlPath = controlType.toLowerCase().replace(/_/g, '-');
137
+
138
+ await axios.put(`${BASE_URL}/v18/settings/${typePath}/${thingId}/${deviceId}/${controlPath}`, null, {
139
+ params: {
140
+ createDate,
141
+ [controlType === 'WINDSPEED' ? 'mode' : 'state']: state,
142
+ hashData,
143
+ },
144
+ headers: { 'Authorization': `Bearer ${token}` },
145
+ });
146
+
147
+ this.log.info(`Set ${type} [${thingId}/${deviceId}] to ${state}`);
148
+ return true;
149
+ } catch (error) {
150
+ this.log.error(`Error setting device ${thingId}: ${error}`);
151
+ return false;
152
+ }
153
+ }
154
+
155
+ async unlockDoorlock(thingId: string): Promise<boolean> {
156
+ const token = await this.login();
157
+ if (!token) {
158
+ return false;
159
+ }
160
+
161
+ try {
162
+ const createDate = this.getDateTime();
163
+ const hashData = this.sha512(`IHRESTAPI${thingId}${createDate}`);
164
+
165
+ await axios.put(`${BASE_URL}/v16/settings/doorlocks/${thingId}/open-mode`, null, {
166
+ params: {
167
+ createDate,
168
+ pin: '',
169
+ hashData,
170
+ },
171
+ headers: { 'Authorization': `Bearer ${token}` },
172
+ });
173
+
174
+ this.log.info(`Unlocked doorlock [${thingId}]`);
175
+ return true;
176
+ } catch (error) {
177
+ this.log.error(`Error unlocking doorlock ${thingId}: ${error}`);
178
+ return false;
179
+ }
180
+ }
181
+
182
+ private sha512(input: string): string {
183
+ return CryptoJS.SHA512(input).toString();
184
+ }
185
+
186
+ private isTokenExpired(): boolean {
187
+ return !this.cachedAccessToken || Date.now() >= this.tokenExpiry;
188
+ }
189
+
190
+ private getDateTime(): string {
191
+ const now = new Date();
192
+ const pad = (num: number) => num.toString().padStart(2, '0');
193
+ return `${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}` +
194
+ `${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}`;
195
+ }
196
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": [
5
+ "DOM",
6
+ "ES2022"
7
+ ],
8
+ "rootDir": "src",
9
+ "module": "nodenext",
10
+ "moduleResolution": "nodenext",
11
+ "strict": true,
12
+ "declaration": true,
13
+ "outDir": "dist",
14
+ "sourceMap": true,
15
+ "esModuleInterop": true,
16
+ "forceConsistentCasingInFileNames": true
17
+ },
18
+ "include": [
19
+ "eslint.config.js",
20
+ "homebridge-ui",
21
+ "src"
22
+ ]
23
+ }