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.
Files changed (50) hide show
  1. package/.editorconfig +10 -0
  2. package/CHANGELOG.md +33 -0
  3. package/LICENSE +176 -0
  4. package/README.md +67 -0
  5. package/config.schema.json +39 -0
  6. package/dist/cync/config-client.d.ts +77 -0
  7. package/dist/cync/config-client.js +222 -0
  8. package/dist/cync/config-client.js.map +1 -0
  9. package/dist/cync/cync-client.d.ts +76 -0
  10. package/dist/cync/cync-client.js +236 -0
  11. package/dist/cync/cync-client.js.map +1 -0
  12. package/dist/cync/tcp-client.d.ts +33 -0
  13. package/dist/cync/tcp-client.js +59 -0
  14. package/dist/cync/tcp-client.js.map +1 -0
  15. package/dist/cync/token-store.d.ts +16 -0
  16. package/dist/cync/token-store.js +39 -0
  17. package/dist/cync/token-store.js.map +1 -0
  18. package/dist/cync/types.d.ts +1 -0
  19. package/dist/cync/types.js +2 -0
  20. package/dist/cync/types.js.map +1 -0
  21. package/dist/index.d.ts +7 -0
  22. package/dist/index.js +10 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/platform.d.ts +29 -0
  25. package/dist/platform.js +143 -0
  26. package/dist/platform.js.map +1 -0
  27. package/dist/platformAccessory.d.ts +13 -0
  28. package/dist/platformAccessory.js +17 -0
  29. package/dist/platformAccessory.js.map +1 -0
  30. package/dist/settings.d.ts +2 -0
  31. package/dist/settings.js +3 -0
  32. package/dist/settings.js.map +1 -0
  33. package/docs/cync-api-notes.md +168 -0
  34. package/docs/cync-client-contract.md +172 -0
  35. package/docs/cync-device-model.md +129 -0
  36. package/eslint.config.js +41 -0
  37. package/homebridge-cync-app-v0.0.1.zip +0 -0
  38. package/nodemon.json +12 -0
  39. package/package.json +56 -0
  40. package/src/@types/homebridge-lib.d.ts +14 -0
  41. package/src/cync/config-client.ts +370 -0
  42. package/src/cync/cync-client.ts +408 -0
  43. package/src/cync/tcp-client.ts +88 -0
  44. package/src/cync/token-store.ts +50 -0
  45. package/src/cync/types.ts +0 -0
  46. package/src/index.ts +12 -0
  47. package/src/platform.ts +209 -0
  48. package/src/platformAccessory.ts +18 -0
  49. package/src/settings.ts +3 -0
  50. package/tsconfig.json +24 -0
@@ -0,0 +1,209 @@
1
+ // src/platform.ts
2
+ import type {
3
+ API,
4
+ DynamicPlatformPlugin,
5
+ Logger,
6
+ PlatformAccessory,
7
+ PlatformConfig,
8
+ } from 'homebridge';
9
+
10
+ import { PLATFORM_NAME } from './settings.js';
11
+ import { CyncClient } from './cync/cync-client.js';
12
+ import { ConfigClient } from './cync/config-client.js';
13
+ import type { CyncCloudConfig } from './cync/config-client.js';
14
+ import { TcpClient } from './cync/tcp-client.js';
15
+ import type { CyncLogger } from './cync/config-client.js';
16
+
17
+ const toCyncLogger = (log: Logger): CyncLogger => ({
18
+ debug: log.debug.bind(log),
19
+ info: log.info.bind(log),
20
+ warn: log.warn.bind(log),
21
+ error: log.error.bind(log),
22
+ });
23
+
24
+ interface CyncAccessoryContext {
25
+ cync?: {
26
+ meshId: string;
27
+ deviceId: string;
28
+ productId?: string;
29
+ };
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ /**
34
+ * CyncAppPlatform
35
+ *
36
+ * Homebridge platform class responsible for:
37
+ * - Initializing the Cync client
38
+ * - Managing cached accessories
39
+ * - Kicking off device discovery from Cync cloud
40
+ */
41
+ export class CyncAppPlatform implements DynamicPlatformPlugin {
42
+ public readonly accessories: PlatformAccessory[] = [];
43
+
44
+ private readonly log: Logger;
45
+ private readonly api: API;
46
+ private readonly config: PlatformConfig;
47
+ private readonly client: CyncClient;
48
+
49
+ private cloudConfig: CyncCloudConfig | null = null;
50
+
51
+ constructor(log: Logger, config: PlatformConfig, api: API) {
52
+ this.log = log;
53
+ this.config = config;
54
+ this.api = api;
55
+
56
+ // Extract login config from platform config
57
+ const cfg = this.config as Record<string, unknown>;
58
+ const username = (cfg.username ?? cfg.email) as string | undefined;
59
+ const password = cfg.password as string | undefined;
60
+ const twoFactor = cfg.twoFactor as string | undefined;
61
+
62
+ // Initialize the Cync client with platform logger so all messages
63
+ // appear in the Homebridge log.
64
+ this.client = new CyncClient(
65
+ new ConfigClient(toCyncLogger(this.log)),
66
+ new TcpClient(toCyncLogger(this.log)),
67
+ {
68
+ email: username ?? '',
69
+ password: password ?? '',
70
+ twoFactor,
71
+ },
72
+ this.api.user.storagePath(),
73
+ toCyncLogger(this.log),
74
+ );
75
+
76
+ this.log.info(this.config.name ?? PLATFORM_NAME, 'initialized');
77
+
78
+ this.api.on('didFinishLaunching', () => {
79
+ this.log.info(PLATFORM_NAME, 'didFinishLaunching');
80
+ void this.loadCync();
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Called when cached accessories are restored from disk.
86
+ */
87
+ configureAccessory(accessory: PlatformAccessory): void {
88
+ this.log.info('Restoring cached accessory', accessory.displayName);
89
+ this.accessories.push(accessory);
90
+ }
91
+
92
+ private async loadCync(): Promise<void> {
93
+ try {
94
+ const cfg = this.config as Record<string, unknown>;
95
+ const username = (cfg.username ?? cfg.email) as string | undefined;
96
+ const password = cfg.password as string | undefined;
97
+
98
+ if (!username || !password) {
99
+ this.log.warn('Cync: credentials missing in config.json; skipping cloud login.');
100
+ return;
101
+ }
102
+
103
+ // Let CyncClient handle 2FA bootstrap + token persistence.
104
+ const loggedIn = await this.client.ensureLoggedIn();
105
+ if (!loggedIn) {
106
+ // We either just requested a 2FA code or hit a credential error.
107
+ // In the "code requested" case, the log already tells the user
108
+ // to add it to config and restart.
109
+ return;
110
+ }
111
+
112
+ const cloudConfig = await this.client.loadConfiguration();
113
+ this.cloudConfig = cloudConfig;
114
+
115
+ this.log.info(
116
+ 'Cync: cloud configuration loaded; mesh count=%d',
117
+ cloudConfig?.meshes?.length ?? 0,
118
+ );
119
+
120
+ this.discoverDevices(cloudConfig);
121
+ } catch (err) {
122
+ this.log.error(
123
+ 'Cync: cloud login failed: %s',
124
+ (err as Error).message ?? String(err),
125
+ );
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Discover devices from the Cync cloud config and register them as
131
+ * Homebridge accessories. For now, each device is exposed as a simple
132
+ * dummy Switch that logs state changes.
133
+ */
134
+ private discoverDevices(cloudConfig: CyncCloudConfig): void {
135
+ if (!cloudConfig.meshes?.length) {
136
+ this.log.warn('Cync: no meshes returned from cloud; nothing to discover.');
137
+ return;
138
+ }
139
+
140
+ for (const mesh of cloudConfig.meshes) {
141
+ const meshName = mesh.name || mesh.id;
142
+ this.log.info('Cync: processing mesh %s', meshName);
143
+
144
+ const devices = mesh.devices ?? [];
145
+ if (!devices.length) {
146
+ this.log.info('Cync: mesh %s has no devices.', meshName);
147
+ continue;
148
+ }
149
+
150
+ for (const device of devices) {
151
+ const deviceId = `${device.id ??
152
+ device.device_id ??
153
+ device.mac ??
154
+ device.sn ??
155
+ `${mesh.id}-${device.product_id ?? 'unknown'}`}`;
156
+
157
+ const preferredName =
158
+ (device.name as string | undefined) ??
159
+ (device.displayName as string | undefined) ??
160
+ undefined;
161
+
162
+ const deviceName = preferredName || `Cync Device ${deviceId}`;
163
+ const uuidSeed = `cync-${mesh.id}-${deviceId}`;
164
+ const uuid = this.api.hap.uuid.generate(uuidSeed);
165
+
166
+ const existing = this.accessories.find(acc => acc.UUID === uuid);
167
+ if (existing) {
168
+ this.log.info('Cync: using cached accessory for %s (%s)', deviceName, uuidSeed);
169
+ continue;
170
+ }
171
+
172
+ this.log.info('Cync: registering new accessory for %s (%s)', deviceName, uuidSeed);
173
+
174
+ const accessory = new this.api.platformAccessory(deviceName, uuid);
175
+
176
+ // Simple Switch service for now
177
+ const service =
178
+ accessory.getService(this.api.hap.Service.Switch) ||
179
+ accessory.addService(this.api.hap.Service.Switch, deviceName);
180
+
181
+ service
182
+ .getCharacteristic(this.api.hap.Characteristic.On)
183
+ .onGet(() => {
184
+ this.log.info('Cync: On.get -> false for %s', deviceName);
185
+ return false;
186
+ })
187
+ .onSet((value) => {
188
+ this.log.info('Cync: On.set -> %s for %s', String(value), deviceName);
189
+ });
190
+
191
+ // Context for later TCP control
192
+ const ctx = accessory.context as CyncAccessoryContext;
193
+ ctx.cync = {
194
+ meshId: mesh.id,
195
+ deviceId,
196
+ productId: device.product_id,
197
+ };
198
+
199
+ this.api.registerPlatformAccessories(
200
+ 'homebridge-cync-app',
201
+ 'CyncAppPlatform',
202
+ [accessory],
203
+ );
204
+
205
+ this.accessories.push(accessory);
206
+ }
207
+ }
208
+ }
209
+ }
@@ -0,0 +1,18 @@
1
+ import type { PlatformAccessory } from 'homebridge';
2
+ import type { CyncAppPlatform } from './platform.js';
3
+
4
+ /**
5
+ * CyncAccessory
6
+ *
7
+ * Placeholder accessory class for future Cync devices.
8
+ * Currently unused; exists only to provide a typed scaffold.
9
+ */
10
+ export class CyncAccessory {
11
+ constructor(
12
+ private readonly platform: CyncAppPlatform,
13
+ private readonly accessory: PlatformAccessory,
14
+ ) {
15
+ // TODO: implement Cync-specific services and characteristics.
16
+ // This placeholder exists to keep the project compiling during early scaffolding.
17
+ }
18
+ }
@@ -0,0 +1,3 @@
1
+ export const PLATFORM_NAME = 'CyncAppPlatform';
2
+
3
+ export const PLUGIN_NAME = 'homebridge-cync-app';
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
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
+ "allowSyntheticDefaultImports": true,
16
+ "esModuleInterop": true,
17
+ "forceConsistentCasingInFileNames": true
18
+ },
19
+ "include": [
20
+ "eslint.config.js",
21
+ "homebridge-ui",
22
+ "src"
23
+ ]
24
+ }