homebridge-cync-app 0.0.2
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/.editorconfig +10 -0
- package/CHANGELOG.md +33 -0
- package/LICENSE +176 -0
- package/README.md +67 -0
- package/config.schema.json +39 -0
- package/dist/cync/config-client.d.ts +77 -0
- package/dist/cync/config-client.js +222 -0
- package/dist/cync/config-client.js.map +1 -0
- package/dist/cync/cync-client.d.ts +76 -0
- package/dist/cync/cync-client.js +236 -0
- package/dist/cync/cync-client.js.map +1 -0
- package/dist/cync/tcp-client.d.ts +33 -0
- package/dist/cync/tcp-client.js +59 -0
- package/dist/cync/tcp-client.js.map +1 -0
- package/dist/cync/token-store.d.ts +16 -0
- package/dist/cync/token-store.js +39 -0
- package/dist/cync/token-store.js.map +1 -0
- package/dist/cync/types.d.ts +1 -0
- package/dist/cync/types.js +2 -0
- package/dist/cync/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +29 -0
- package/dist/platform.js +143 -0
- package/dist/platform.js.map +1 -0
- package/dist/platformAccessory.d.ts +13 -0
- package/dist/platformAccessory.js +17 -0
- package/dist/platformAccessory.js.map +1 -0
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +3 -0
- package/dist/settings.js.map +1 -0
- package/docs/cync-api-notes.md +168 -0
- package/docs/cync-client-contract.md +172 -0
- package/docs/cync-device-model.md +129 -0
- package/eslint.config.js +41 -0
- package/homebridge-cync-app-v0.0.1.zip +0 -0
- package/nodemon.json +12 -0
- package/package.json +56 -0
- package/src/@types/homebridge-lib.d.ts +14 -0
- package/src/cync/config-client.ts +370 -0
- package/src/cync/cync-client.ts +408 -0
- package/src/cync/tcp-client.ts +88 -0
- package/src/cync/token-store.ts +50 -0
- package/src/cync/types.ts +0 -0
- package/src/index.ts +12 -0
- package/src/platform.ts +209 -0
- package/src/platformAccessory.ts +18 -0
- package/src/settings.ts +3 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ConfigClient, CyncCloudConfig, CyncLoginSession, CyncLogger } from './config-client.js';
|
|
2
|
+
import { TcpClient } from './tcp-client.js';
|
|
3
|
+
export declare class CyncClient {
|
|
4
|
+
private readonly log;
|
|
5
|
+
private readonly configClient;
|
|
6
|
+
private readonly tcpClient;
|
|
7
|
+
private readonly tokenStore;
|
|
8
|
+
private tokenData;
|
|
9
|
+
private session;
|
|
10
|
+
private cloudConfig;
|
|
11
|
+
private readonly loginConfig;
|
|
12
|
+
constructor(configClient: ConfigClient, tcpClient: TcpClient, loginConfig: {
|
|
13
|
+
email: string;
|
|
14
|
+
password: string;
|
|
15
|
+
twoFactor?: string;
|
|
16
|
+
}, storagePath: string, logger?: CyncLogger);
|
|
17
|
+
/**
|
|
18
|
+
* Ensure we are logged in:
|
|
19
|
+
* 1) Try stored token.
|
|
20
|
+
* 2) If none/invalid, run 2FA flow (request or complete).
|
|
21
|
+
* Returns true on successful login, false if we need user input (2FA).
|
|
22
|
+
*/
|
|
23
|
+
ensureLoggedIn(): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Internal helper: request a 2FA email code using existing authenticate().
|
|
26
|
+
*/
|
|
27
|
+
private requestTwoFactorCode;
|
|
28
|
+
/**
|
|
29
|
+
* Internal helper: complete 2FA login using existing submitTwoFactor().
|
|
30
|
+
* This converts CyncLoginSession into the richer shape we want for token storage.
|
|
31
|
+
*/
|
|
32
|
+
private completeTwoFactorLogin;
|
|
33
|
+
/**
|
|
34
|
+
* Apply an access token (and associated metadata) to the underlying ConfigClient,
|
|
35
|
+
* and hydrate our local session snapshot so ensureSession() passes.
|
|
36
|
+
*/
|
|
37
|
+
private applyAccessToken;
|
|
38
|
+
/**
|
|
39
|
+
* Step 1 of 2FA flow:
|
|
40
|
+
*
|
|
41
|
+
* Trigger an email with a one-time code to the Cync account email.
|
|
42
|
+
*
|
|
43
|
+
* Call sequence:
|
|
44
|
+
* await client.authenticate(username, password); // sends email
|
|
45
|
+
* // user reads email, gets code…
|
|
46
|
+
* await client.submitTwoFactor(username, password, code); // completes login
|
|
47
|
+
*/
|
|
48
|
+
authenticate(username: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Step 2 of 2FA flow:
|
|
51
|
+
*
|
|
52
|
+
* Use the emailed OTP code to complete login.
|
|
53
|
+
* This method is stateless: it does not rely on prior calls to authenticate()
|
|
54
|
+
* in the same process, so it works across Homebridge restarts.
|
|
55
|
+
*/
|
|
56
|
+
submitTwoFactor(email: string, password: string, code: string): Promise<CyncLoginSession>;
|
|
57
|
+
/**
|
|
58
|
+
* Fetch and cache the cloud configuration (meshes/devices) for the logged-in user.
|
|
59
|
+
*/
|
|
60
|
+
loadConfiguration(): Promise<CyncCloudConfig>;
|
|
61
|
+
/**
|
|
62
|
+
* Start the LAN/TCP transport (stub for now).
|
|
63
|
+
*/
|
|
64
|
+
startTransport(config: CyncCloudConfig, loginCode: Uint8Array): Promise<void>;
|
|
65
|
+
stopTransport(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* High-level helper for toggling a switch/plug.
|
|
68
|
+
*/
|
|
69
|
+
setSwitchState(deviceId: string, params: {
|
|
70
|
+
on: boolean;
|
|
71
|
+
[key: string]: unknown;
|
|
72
|
+
}): Promise<void>;
|
|
73
|
+
getSessionSnapshot(): CyncLoginSession | null;
|
|
74
|
+
getCloudConfigSnapshot(): CyncCloudConfig | null;
|
|
75
|
+
private ensureSession;
|
|
76
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { CyncTokenStore } from './token-store.js';
|
|
2
|
+
const defaultLogger = {
|
|
3
|
+
debug: (...args) => console.debug('[cync-client]', ...args),
|
|
4
|
+
info: (...args) => console.info('[cync-client]', ...args),
|
|
5
|
+
warn: (...args) => console.warn('[cync-client]', ...args),
|
|
6
|
+
error: (...args) => console.error('[cync-client]', ...args),
|
|
7
|
+
};
|
|
8
|
+
export class CyncClient {
|
|
9
|
+
log;
|
|
10
|
+
configClient;
|
|
11
|
+
tcpClient;
|
|
12
|
+
tokenStore;
|
|
13
|
+
tokenData = null;
|
|
14
|
+
// Populated after successful login.
|
|
15
|
+
session = null;
|
|
16
|
+
cloudConfig = null;
|
|
17
|
+
// Credentials from config.json, used to drive 2FA bootstrap.
|
|
18
|
+
loginConfig;
|
|
19
|
+
constructor(configClient, tcpClient, loginConfig, storagePath, logger) {
|
|
20
|
+
this.configClient = configClient;
|
|
21
|
+
this.tcpClient = tcpClient;
|
|
22
|
+
this.log = logger ?? defaultLogger;
|
|
23
|
+
this.loginConfig = loginConfig;
|
|
24
|
+
this.tokenStore = new CyncTokenStore(storagePath);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Ensure we are logged in:
|
|
28
|
+
* 1) Try stored token.
|
|
29
|
+
* 2) If none/invalid, run 2FA flow (request or complete).
|
|
30
|
+
* Returns true on successful login, false if we need user input (2FA).
|
|
31
|
+
*/
|
|
32
|
+
async ensureLoggedIn() {
|
|
33
|
+
// 1) Try stored token/session
|
|
34
|
+
const stored = await this.tokenStore.load();
|
|
35
|
+
if (stored) {
|
|
36
|
+
this.log.info('CyncClient: using stored token for userId=%s (expiresAt=%s)', stored.userId, stored.expiresAt ? new Date(stored.expiresAt).toISOString() : 'unknown');
|
|
37
|
+
this.tokenData = stored;
|
|
38
|
+
// Hydrate ConfigClient + session snapshot.
|
|
39
|
+
this.applyAccessToken(stored);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
// 2) No stored token – run 2FA bootstrap
|
|
43
|
+
const { email, password, twoFactor } = this.loginConfig;
|
|
44
|
+
if (!email || !password) {
|
|
45
|
+
this.log.error('CyncClient: email and password are required to obtain a new token.');
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (!twoFactor || String(twoFactor).trim() === '') {
|
|
49
|
+
// No 2FA code – request one
|
|
50
|
+
this.log.info('Cync: starting 2FA handshake for %s', email);
|
|
51
|
+
await this.requestTwoFactorCode(email);
|
|
52
|
+
this.log.info('Cync: 2FA code sent to your email. Enter the code as "twoFactor" in the plugin config and restart Homebridge to complete login.');
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
// We have a 2FA code – complete login and persist token
|
|
56
|
+
this.log.info('Cync: completing 2FA login for %s', email);
|
|
57
|
+
const loginResult = await this.completeTwoFactorLogin(email, password, String(twoFactor).trim());
|
|
58
|
+
const tokenData = {
|
|
59
|
+
userId: String(loginResult.userId),
|
|
60
|
+
accessToken: loginResult.accessToken,
|
|
61
|
+
refreshToken: loginResult.refreshToken,
|
|
62
|
+
expiresAt: loginResult.expiresAt ?? undefined,
|
|
63
|
+
};
|
|
64
|
+
await this.tokenStore.save(tokenData);
|
|
65
|
+
this.tokenData = tokenData;
|
|
66
|
+
// Hydrate ConfigClient + session snapshot from the freshly obtained token.
|
|
67
|
+
this.applyAccessToken(tokenData);
|
|
68
|
+
this.log.info('Cync login successful; userId=%s (token stored)', tokenData.userId);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Internal helper: request a 2FA email code using existing authenticate().
|
|
73
|
+
*/
|
|
74
|
+
async requestTwoFactorCode(email) {
|
|
75
|
+
await this.authenticate(email);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Internal helper: complete 2FA login using existing submitTwoFactor().
|
|
79
|
+
* This converts CyncLoginSession into the richer shape we want for token storage.
|
|
80
|
+
*/
|
|
81
|
+
async completeTwoFactorLogin(email, password, code) {
|
|
82
|
+
const session = await this.submitTwoFactor(email, password, code);
|
|
83
|
+
const s = session;
|
|
84
|
+
const access = s.accessToken ?? s.jwt;
|
|
85
|
+
if (!access) {
|
|
86
|
+
throw new Error('CyncClient: login session did not return an access token.');
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
...session,
|
|
90
|
+
accessToken: access,
|
|
91
|
+
refreshToken: s.refreshToken ?? s.refreshJwt,
|
|
92
|
+
expiresAt: s.expiresAt,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Apply an access token (and associated metadata) to the underlying ConfigClient,
|
|
97
|
+
* and hydrate our local session snapshot so ensureSession() passes.
|
|
98
|
+
*/
|
|
99
|
+
applyAccessToken(tokenData) {
|
|
100
|
+
if (!tokenData.accessToken || !tokenData.userId) {
|
|
101
|
+
this.log.warn('CyncClient: applyAccessToken called with missing userId or accessToken; tokenData=%o', {
|
|
102
|
+
userId: tokenData.userId,
|
|
103
|
+
hasAccessToken: !!tokenData.accessToken,
|
|
104
|
+
expiresAt: tokenData.expiresAt,
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Push into ConfigClient so cloud calls can use it.
|
|
109
|
+
this.configClient.restoreSession(tokenData.accessToken, tokenData.userId);
|
|
110
|
+
// Hydrate our own session snapshot so ensureSession() passes.
|
|
111
|
+
this.session = {
|
|
112
|
+
accessToken: tokenData.accessToken,
|
|
113
|
+
userId: tokenData.userId,
|
|
114
|
+
raw: {
|
|
115
|
+
source: 'tokenStore',
|
|
116
|
+
expiresAt: tokenData.expiresAt,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
this.log.debug('CyncClient: access token applied from %s; userId=%s, expiresAt=%s', 'tokenStore', tokenData.userId, tokenData.expiresAt ? new Date(tokenData.expiresAt).toISOString() : 'unknown');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Step 1 of 2FA flow:
|
|
123
|
+
*
|
|
124
|
+
* Trigger an email with a one-time code to the Cync account email.
|
|
125
|
+
*
|
|
126
|
+
* Call sequence:
|
|
127
|
+
* await client.authenticate(username, password); // sends email
|
|
128
|
+
* // user reads email, gets code…
|
|
129
|
+
* await client.submitTwoFactor(username, password, code); // completes login
|
|
130
|
+
*/
|
|
131
|
+
async authenticate(username) {
|
|
132
|
+
const email = username.trim();
|
|
133
|
+
this.log.info('CyncClient: requesting 2FA code for %s', email);
|
|
134
|
+
await this.configClient.sendTwoFactorCode(email);
|
|
135
|
+
this.log.info('CyncClient: 2FA email requested; call submitTwoFactor() once the user has the code.');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Step 2 of 2FA flow:
|
|
139
|
+
*
|
|
140
|
+
* Use the emailed OTP code to complete login.
|
|
141
|
+
* This method is stateless: it does not rely on prior calls to authenticate()
|
|
142
|
+
* in the same process, so it works across Homebridge restarts.
|
|
143
|
+
*/
|
|
144
|
+
async submitTwoFactor(email, password, code) {
|
|
145
|
+
const trimmedEmail = email.trim();
|
|
146
|
+
const trimmedCode = code.trim();
|
|
147
|
+
this.log.info('CyncClient: completing 2FA login for %s', trimmedEmail);
|
|
148
|
+
const session = await this.configClient.loginWithTwoFactor(trimmedEmail, password, trimmedCode);
|
|
149
|
+
this.session = session;
|
|
150
|
+
this.log.debug('CyncClient: session snapshot after login; hasAccessToken=%s userId=%s', !!session.accessToken, session.userId);
|
|
151
|
+
this.log.info('CyncClient: login successful; userId=%s', session.userId);
|
|
152
|
+
return session;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Fetch and cache the cloud configuration (meshes/devices) for the logged-in user.
|
|
156
|
+
*/
|
|
157
|
+
async loadConfiguration() {
|
|
158
|
+
this.ensureSession();
|
|
159
|
+
this.log.info('CyncClient: loading Cync cloud configuration…');
|
|
160
|
+
const cfg = await this.configClient.getCloudConfig();
|
|
161
|
+
// Debug: inspect per-mesh properties so we can find the real devices.
|
|
162
|
+
for (const mesh of cfg.meshes) {
|
|
163
|
+
const meshName = mesh.name ?? mesh.id;
|
|
164
|
+
this.log.debug('CyncClient: probing properties for mesh %s (id=%s, product_id=%s)', meshName, mesh.id, mesh.product_id);
|
|
165
|
+
try {
|
|
166
|
+
const props = await this.configClient.getDeviceProperties(mesh.product_id, mesh.id);
|
|
167
|
+
this.log.debug('CyncClient: mesh %s properties keys=%o', meshName, Object.keys(props));
|
|
168
|
+
const bulbsArray = props.bulbsArray;
|
|
169
|
+
if (Array.isArray(bulbsArray)) {
|
|
170
|
+
this.log.info('CyncClient: mesh %s bulbsArray length=%d; first item keys=%o', meshName, bulbsArray.length, bulbsArray[0] ? Object.keys(bulbsArray[0]) : []);
|
|
171
|
+
const rawDevices = bulbsArray;
|
|
172
|
+
mesh.devices = rawDevices.map((raw) => {
|
|
173
|
+
const d = raw;
|
|
174
|
+
const displayName = d.displayName;
|
|
175
|
+
const deviceID = (d.deviceID ?? d.deviceId);
|
|
176
|
+
const wifiMac = d.wifiMac;
|
|
177
|
+
const productId = d.product_id ?? mesh.product_id;
|
|
178
|
+
// Use deviceID first, then wifiMac (stripped), then a mesh-based fallback.
|
|
179
|
+
const id = deviceID ??
|
|
180
|
+
(wifiMac ? wifiMac.replace(/:/g, '') : undefined) ??
|
|
181
|
+
`${mesh.id}-${productId ?? 'unknown'}`;
|
|
182
|
+
return {
|
|
183
|
+
id,
|
|
184
|
+
name: displayName ?? undefined,
|
|
185
|
+
product_id: productId,
|
|
186
|
+
device_id: deviceID,
|
|
187
|
+
mac: wifiMac,
|
|
188
|
+
raw: d,
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
this.log.info('CyncClient: mesh %s has no bulbsArray in properties; props keys=%o', meshName, Object.keys(props));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
this.log.warn('CyncClient: getDeviceProperties failed for mesh %s (%s): %s', meshName, mesh.id, err.message ?? String(err));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
this.cloudConfig = cfg;
|
|
201
|
+
this.log.info('CyncClient: cloud configuration loaded; meshes=%d', cfg.meshes.length);
|
|
202
|
+
return cfg;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Start the LAN/TCP transport (stub for now).
|
|
206
|
+
*/
|
|
207
|
+
async startTransport(config, loginCode) {
|
|
208
|
+
this.ensureSession();
|
|
209
|
+
this.log.info('CyncClient: starting TCP transport (stub)…');
|
|
210
|
+
await this.tcpClient.connect(loginCode, config);
|
|
211
|
+
}
|
|
212
|
+
async stopTransport() {
|
|
213
|
+
this.log.info('CyncClient: stopping TCP transport…');
|
|
214
|
+
await this.tcpClient.disconnect();
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* High-level helper for toggling a switch/plug.
|
|
218
|
+
*/
|
|
219
|
+
async setSwitchState(deviceId, params) {
|
|
220
|
+
this.ensureSession();
|
|
221
|
+
this.log.debug('CyncClient: setSwitchState stub; deviceId=%s params=%o', deviceId, params);
|
|
222
|
+
await this.tcpClient.setSwitchState(deviceId, params);
|
|
223
|
+
}
|
|
224
|
+
getSessionSnapshot() {
|
|
225
|
+
return this.session;
|
|
226
|
+
}
|
|
227
|
+
getCloudConfigSnapshot() {
|
|
228
|
+
return this.cloudConfig;
|
|
229
|
+
}
|
|
230
|
+
ensureSession() {
|
|
231
|
+
if (!this.session) {
|
|
232
|
+
throw new Error('Cync session not initialised; complete 2FA login first.');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=cync-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cync-client.js","sourceRoot":"","sources":["../../src/cync/cync-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CyncCloudConfig, CyncLogger } from './config-client.js';
|
|
2
|
+
export type DeviceUpdateCallback = (payload: unknown) => void;
|
|
3
|
+
export declare class TcpClient {
|
|
4
|
+
private readonly log;
|
|
5
|
+
private deviceUpdateCb;
|
|
6
|
+
private roomUpdateCb;
|
|
7
|
+
private motionUpdateCb;
|
|
8
|
+
private ambientUpdateCb;
|
|
9
|
+
constructor(logger?: CyncLogger);
|
|
10
|
+
/**
|
|
11
|
+
* Establish a TCP session to one or more Cync devices.
|
|
12
|
+
*
|
|
13
|
+
* In the full implementation, loginCode will be the authentication blob used
|
|
14
|
+
* by the LAN devices, and config will contain the mesh/network information
|
|
15
|
+
* needed to discover and connect to the correct hosts.
|
|
16
|
+
*
|
|
17
|
+
* For now this is a no-op that simply logs the request.
|
|
18
|
+
*/
|
|
19
|
+
connect(loginCode: Uint8Array, config: CyncCloudConfig): Promise<void>;
|
|
20
|
+
disconnect(): Promise<void>;
|
|
21
|
+
onDeviceUpdate(cb: DeviceUpdateCallback): void;
|
|
22
|
+
onRoomUpdate(cb: DeviceUpdateCallback): void;
|
|
23
|
+
onMotionUpdate(cb: DeviceUpdateCallback): void;
|
|
24
|
+
onAmbientUpdate(cb: DeviceUpdateCallback): void;
|
|
25
|
+
/**
|
|
26
|
+
* High-level API to change switch state. The actual encoding and TCP send
|
|
27
|
+
* will be filled in once the LAN protocol is implemented.
|
|
28
|
+
*/
|
|
29
|
+
setSwitchState(deviceId: string, params: {
|
|
30
|
+
on: boolean;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/cync/tcp-client.ts
|
|
2
|
+
// Thin TCP client stub for talking to Cync WiFi devices.
|
|
3
|
+
// The binary protocol is non-trivial; for now this class only logs calls so
|
|
4
|
+
// that higher layers can be wired up and tested without crashing.
|
|
5
|
+
const defaultLogger = {
|
|
6
|
+
debug: (...args) => console.debug('[cync-tcp]', ...args),
|
|
7
|
+
info: (...args) => console.info('[cync-tcp]', ...args),
|
|
8
|
+
warn: (...args) => console.warn('[cync-tcp]', ...args),
|
|
9
|
+
error: (...args) => console.error('[cync-tcp]', ...args),
|
|
10
|
+
};
|
|
11
|
+
export class TcpClient {
|
|
12
|
+
log;
|
|
13
|
+
deviceUpdateCb = null;
|
|
14
|
+
roomUpdateCb = null;
|
|
15
|
+
motionUpdateCb = null;
|
|
16
|
+
ambientUpdateCb = null;
|
|
17
|
+
constructor(logger) {
|
|
18
|
+
this.log = logger ?? defaultLogger;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Establish a TCP session to one or more Cync devices.
|
|
22
|
+
*
|
|
23
|
+
* In the full implementation, loginCode will be the authentication blob used
|
|
24
|
+
* by the LAN devices, and config will contain the mesh/network information
|
|
25
|
+
* needed to discover and connect to the correct hosts.
|
|
26
|
+
*
|
|
27
|
+
* For now this is a no-op that simply logs the request.
|
|
28
|
+
*/
|
|
29
|
+
async connect(loginCode, config) {
|
|
30
|
+
this.log.info('TcpClient.connect() stub called with loginCode length=%d meshes=%d', loginCode.length, config.meshes.length);
|
|
31
|
+
}
|
|
32
|
+
async disconnect() {
|
|
33
|
+
this.log.info('TcpClient.disconnect() stub called.');
|
|
34
|
+
}
|
|
35
|
+
onDeviceUpdate(cb) {
|
|
36
|
+
this.deviceUpdateCb = cb;
|
|
37
|
+
}
|
|
38
|
+
onRoomUpdate(cb) {
|
|
39
|
+
this.roomUpdateCb = cb;
|
|
40
|
+
}
|
|
41
|
+
onMotionUpdate(cb) {
|
|
42
|
+
this.motionUpdateCb = cb;
|
|
43
|
+
}
|
|
44
|
+
onAmbientUpdate(cb) {
|
|
45
|
+
this.ambientUpdateCb = cb;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* High-level API to change switch state. The actual encoding and TCP send
|
|
49
|
+
* will be filled in once the LAN protocol is implemented.
|
|
50
|
+
*/
|
|
51
|
+
async setSwitchState(deviceId, params) {
|
|
52
|
+
this.log.info('TcpClient.setSwitchState() stub: deviceId=%s params=%o', deviceId, params);
|
|
53
|
+
// In a future implementation, this is where we would:
|
|
54
|
+
// 1. Look up the device in the current CyncCloudConfig.
|
|
55
|
+
// 2. Construct the appropriate binary payload.
|
|
56
|
+
// 3. Send via a net.Socket and handle the response.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=tcp-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tcp-client.js","sourceRoot":"","sources":["../../src/cync/tcp-client.ts"],"names":[],"mappings":"AAAA,yBAAyB;AACzB,yDAAyD;AACzD,4EAA4E;AAC5E,kEAAkE;AAIlE,MAAM,aAAa,GAAe;IACjC,KAAK,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC;IACnE,IAAI,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC;IACjE,IAAI,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC;IACjE,KAAK,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC;CACnE,CAAC;AAIF,MAAM,OAAO,SAAS;IACJ,GAAG,CAAa;IAEzB,cAAc,GAAgC,IAAI,CAAC;IACnD,YAAY,GAAgC,IAAI,CAAC;IACjD,cAAc,GAAgC,IAAI,CAAC;IACnD,eAAe,GAAgC,IAAI,CAAC;IAE5D,YAAY,MAAmB;QAC9B,IAAI,CAAC,GAAG,GAAG,MAAM,IAAI,aAAa,CAAC;IACpC,CAAC;IAED;;;;;;;;OAQG;IACI,KAAK,CAAC,OAAO,CACnB,SAAqB,EACrB,MAAuB;QAEvB,IAAI,CAAC,GAAG,CAAC,IAAI,CACZ,oEAAoE,EACpE,SAAS,CAAC,MAAM,EAChB,MAAM,CAAC,MAAM,CAAC,MAAM,CACpB,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,UAAU;QACtB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;IACtD,CAAC;IAEM,cAAc,CAAC,EAAwB;QAC7C,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;IAC1B,CAAC;IAEM,YAAY,CAAC,EAAwB;QAC3C,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;IACxB,CAAC;IAEM,cAAc,CAAC,EAAwB;QAC7C,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;IAC1B,CAAC;IAEM,eAAe,CAAC,EAAwB;QAC9C,IAAI,CAAC,eAAe,GAAG,EAAE,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,cAAc,CAC1B,QAAgB,EAChB,MAA+C;QAE/C,IAAI,CAAC,GAAG,CAAC,IAAI,CACZ,wDAAwD,EACxD,QAAQ,EACR,MAAM,CACN,CAAC;QAEF,sDAAsD;QACtD,wDAAwD;QACxD,+CAA+C;QAC/C,oDAAoD;IACrD,CAAC;CACD"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface CyncTokenData {
|
|
2
|
+
userId: string;
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken?: string;
|
|
5
|
+
expiresAt?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Simple JSON token store under the Homebridge storage path.
|
|
9
|
+
*/
|
|
10
|
+
export declare class CyncTokenStore {
|
|
11
|
+
private readonly filePath;
|
|
12
|
+
constructor(storagePath: string);
|
|
13
|
+
load(): Promise<CyncTokenData | null>;
|
|
14
|
+
save(data: CyncTokenData): Promise<void>;
|
|
15
|
+
clear(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/cync/token-store.ts
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Simple JSON token store under the Homebridge storage path.
|
|
6
|
+
*/
|
|
7
|
+
export class CyncTokenStore {
|
|
8
|
+
filePath;
|
|
9
|
+
constructor(storagePath) {
|
|
10
|
+
this.filePath = path.join(storagePath, 'cync-tokens.json');
|
|
11
|
+
}
|
|
12
|
+
async load() {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await fs.readFile(this.filePath, 'utf8');
|
|
15
|
+
const data = JSON.parse(raw);
|
|
16
|
+
// If expiresAt is set and in the past, treat as invalid
|
|
17
|
+
if (data.expiresAt && data.expiresAt <= Date.now()) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async save(data) {
|
|
27
|
+
const json = JSON.stringify(data, null, 2);
|
|
28
|
+
await fs.writeFile(this.filePath, json, 'utf8');
|
|
29
|
+
}
|
|
30
|
+
async clear() {
|
|
31
|
+
try {
|
|
32
|
+
await fs.unlink(this.filePath);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// ignore if missing
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../src/cync/token-store.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AASlC;;GAEG;AACH,MAAM,OAAO,cAAc;IACT,QAAQ,CAAS;IAElC,YAAmB,WAAmB;QACrC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,kBAAkB,CAAC,CAAC;IAC5D,CAAC;IAEM,KAAK,CAAC,IAAI;QAChB,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YACrD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkB,CAAC;YAE9C,wDAAwD;YACxD,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;gBACpD,OAAO,IAAI,CAAC;YACb,CAAC;YAED,OAAO,IAAI,CAAC;QACb,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,IAAI,CAAC;QACb,CAAC;IACF,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,IAAmB;QACpC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC3C,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACjD,CAAC;IAEM,KAAK,CAAC,KAAK;QACjB,IAAI,CAAC;YACJ,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACR,oBAAoB;QACrB,CAAC;IACF,CAAC;CACD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/cync/types.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CyncAppPlatform } from './platform.js';
|
|
2
|
+
import { PLATFORM_NAME } from './settings.js';
|
|
3
|
+
/**
|
|
4
|
+
* Homebridge entry point.
|
|
5
|
+
* Registers the CyncAppPlatform with Homebridge under PLATFORM_NAME.
|
|
6
|
+
*/
|
|
7
|
+
export default (api) => {
|
|
8
|
+
api.registerPlatform(PLATFORM_NAME, CyncAppPlatform);
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C;;;GAGG;AACH,eAAe,CAAC,GAAQ,EAAE,EAAE;IAC3B,GAAG,CAAC,gBAAgB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC;AACtD,CAAC,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig } from 'homebridge';
|
|
2
|
+
/**
|
|
3
|
+
* CyncAppPlatform
|
|
4
|
+
*
|
|
5
|
+
* Homebridge platform class responsible for:
|
|
6
|
+
* - Initializing the Cync client
|
|
7
|
+
* - Managing cached accessories
|
|
8
|
+
* - Kicking off device discovery from Cync cloud
|
|
9
|
+
*/
|
|
10
|
+
export declare class CyncAppPlatform implements DynamicPlatformPlugin {
|
|
11
|
+
readonly accessories: PlatformAccessory[];
|
|
12
|
+
private readonly log;
|
|
13
|
+
private readonly api;
|
|
14
|
+
private readonly config;
|
|
15
|
+
private readonly client;
|
|
16
|
+
private cloudConfig;
|
|
17
|
+
constructor(log: Logger, config: PlatformConfig, api: API);
|
|
18
|
+
/**
|
|
19
|
+
* Called when cached accessories are restored from disk.
|
|
20
|
+
*/
|
|
21
|
+
configureAccessory(accessory: PlatformAccessory): void;
|
|
22
|
+
private loadCync;
|
|
23
|
+
/**
|
|
24
|
+
* Discover devices from the Cync cloud config and register them as
|
|
25
|
+
* Homebridge accessories. For now, each device is exposed as a simple
|
|
26
|
+
* dummy Switch that logs state changes.
|
|
27
|
+
*/
|
|
28
|
+
private discoverDevices;
|
|
29
|
+
}
|