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
package/src/platform.ts
ADDED
|
@@ -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
|
+
}
|
package/src/settings.ts
ADDED
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
|
+
}
|