homebridge-smartika 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.
@@ -0,0 +1,379 @@
1
+ 'use strict';
2
+
3
+ const { PLATFORM_NAME, PLUGIN_NAME } = require('./settings');
4
+ const SmartikaHubConnection = require('./SmartikaHubConnection');
5
+ const SmartikaDiscovery = require('./SmartikaDiscovery');
6
+ const SmartikaLightAccessory = require('./accessories/SmartikaLightAccessory');
7
+ const SmartikaFanAccessory = require('./accessories/SmartikaFanAccessory');
8
+ const SmartikaPlugAccessory = require('./accessories/SmartikaPlugAccessory');
9
+ const protocol = require('./SmartikaProtocol');
10
+
11
+ /**
12
+ * Smartika Platform Plugin for Homebridge
13
+ *
14
+ * This platform dynamically discovers and registers Smartika devices as HomeKit accessories.
15
+ * Communication is entirely local via TCP with AES-128-CBC encryption.
16
+ */
17
+ class SmartikaPlatform {
18
+ /**
19
+ * @param {import('homebridge').Logger} log
20
+ * @param {import('homebridge').PlatformConfig} config
21
+ * @param {import('homebridge').API} api
22
+ */
23
+ constructor(log, config, api) {
24
+ this.log = log;
25
+ this.config = config;
26
+ this.api = api;
27
+
28
+ // Store restored cached accessories
29
+ this.accessories = new Map();
30
+
31
+ // Hub connection instance
32
+ this.hub = null;
33
+
34
+ // Discovery instance
35
+ this.discovery = null;
36
+
37
+ // Device accessory handlers
38
+ this.deviceHandlers = new Map();
39
+
40
+ // Validate configuration
41
+ if (!config) {
42
+ this.log.error('No configuration found for Smartika platform');
43
+ return;
44
+ }
45
+
46
+ this.log.info('Smartika Platform initializing...');
47
+ if (config.hubHost) {
48
+ this.log.info(`Hub IP: ${config.hubHost}`);
49
+ } else {
50
+ this.log.info('No hub IP configured - will use auto-discovery');
51
+ }
52
+
53
+ // Wait for Homebridge to finish launching before initializing
54
+ this.api.on('didFinishLaunching', () => {
55
+ this.log.debug('didFinishLaunching');
56
+ this.initializeHub();
57
+ });
58
+
59
+ // Handle shutdown
60
+ this.api.on('shutdown', () => {
61
+ this.log.info('Shutting down Smartika platform...');
62
+ if (this.hub) {
63
+ this.hub.disconnect();
64
+ }
65
+ if (this.discovery) {
66
+ this.discovery.stopContinuousDiscovery();
67
+ }
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Initialize connection to Smartika hub and discover devices
73
+ */
74
+ async initializeHub() {
75
+ let hubHost = this.config.hubHost;
76
+
77
+ // If no hub IP configured, try auto-discovery
78
+ if (!hubHost) {
79
+ this.log.info('Starting hub auto-discovery...');
80
+ hubHost = await this.discoverHub();
81
+
82
+ if (!hubHost) {
83
+ this.log.error('No Smartika hub found on the network. Please configure hubHost manually.');
84
+ return;
85
+ }
86
+ }
87
+
88
+ try {
89
+ // Create hub connection
90
+ this.hub = new SmartikaHubConnection({
91
+ host: hubHost,
92
+ port: this.config.hubPort || protocol.HUB_PORT,
93
+ pollingInterval: this.config.pollingInterval || 5000,
94
+ log: this.log,
95
+ debug: this.config.debug || false,
96
+ });
97
+
98
+ // Set up event handlers
99
+ this.hub.on('connected', () => {
100
+ this.log.info(`Connected to Smartika hub at ${hubHost}`);
101
+ });
102
+
103
+ this.hub.on('disconnected', () => {
104
+ this.log.warn('Disconnected from Smartika hub');
105
+ });
106
+
107
+ this.hub.on('deviceStatusUpdate', (devices) => {
108
+ this.handleDeviceStatusUpdate(devices);
109
+ });
110
+
111
+ this.hub.on('error', (error) => {
112
+ this.log.error('Hub error:', error.message);
113
+ });
114
+
115
+ // Connect to hub
116
+ await this.hub.connect();
117
+
118
+ // Discover devices
119
+ await this.discoverDevices();
120
+
121
+ // Start polling for status updates
122
+ this.hub.startPolling();
123
+
124
+ } catch (error) {
125
+ this.log.error('Failed to initialize hub:', error.message);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Discover Smartika hub on the network using UDP broadcast
131
+ * @returns {Promise<string|null>} - Hub IP address or null if not found
132
+ */
133
+ async discoverHub() {
134
+ try {
135
+ this.discovery = new SmartikaDiscovery({
136
+ log: this.log,
137
+ timeout: 15000, // 15 seconds for discovery
138
+ });
139
+
140
+ this.discovery.on('hubFound', (hubInfo) => {
141
+ this.log.info(`Found hub: ${hubInfo.hubId} at ${hubInfo.ip}`);
142
+ });
143
+
144
+ const hubs = await this.discovery.discover();
145
+
146
+ if (hubs.length === 0) {
147
+ return null;
148
+ }
149
+
150
+ // Use the first hub found (or filter by hubId if configured)
151
+ const hub = hubs[0];
152
+ this.log.info(`Using hub at ${hub.ip} (${hub.hubId})`);
153
+ return hub.ip;
154
+
155
+ } catch (error) {
156
+ this.log.error('Hub discovery failed:', error.message);
157
+ return null;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Discover devices from the hub and register them as accessories
163
+ */
164
+ async discoverDevices() {
165
+ try {
166
+ this.log.info('Discovering devices...');
167
+
168
+ // Get registered devices from hub database
169
+ const devices = await this.hub.listDevices();
170
+ this.log.info(`Found ${devices.length} registered device(s)`);
171
+
172
+ // Get groups and their members
173
+ const { groups, groupedDeviceIds } = await this.hub.getGroupsWithMembers();
174
+ if (groups.length > 0) {
175
+ this.log.info(`Found ${groups.length} group(s) containing ${groupedDeviceIds.size} device(s)`);
176
+ }
177
+
178
+ // Track which accessories we found
179
+ const foundUUIDs = new Set();
180
+
181
+ // First, add groups as accessories (virtual devices)
182
+ for (const group of groups) {
183
+ const uuid = this.api.hap.uuid.generate(`smartika-group-${group.groupId}`);
184
+ foundUUIDs.add(uuid);
185
+
186
+ // Create a virtual device object for the group
187
+ const groupDevice = {
188
+ shortAddress: group.groupId,
189
+ deviceType: 0x40000001, // Virtual Light (most groups are lights)
190
+ typeName: `Group ${group.groupId.toString(16).toUpperCase()}`,
191
+ category: protocol.DEVICE_CATEGORY.LIGHT,
192
+ isGroup: true,
193
+ memberCount: group.deviceIds.length,
194
+ };
195
+
196
+ const existingAccessory = this.accessories.get(uuid);
197
+
198
+ if (existingAccessory) {
199
+ this.log.info(`Restoring cached group: ${groupDevice.typeName} (${group.deviceIds.length} members)`);
200
+ existingAccessory.context.device = groupDevice;
201
+ this.setupAccessory(existingAccessory, groupDevice);
202
+ } else {
203
+ this.log.info(`Adding new group: ${groupDevice.typeName} (${group.deviceIds.length} members)`);
204
+ this.addAccessory(groupDevice, uuid);
205
+ }
206
+ }
207
+
208
+ // Then add standalone devices (not in any group)
209
+ for (const device of devices) {
210
+ // Skip remote controls - they don't need HomeKit accessories
211
+ if (device.category === protocol.DEVICE_CATEGORY.REMOTE) {
212
+ this.log.debug(`Skipping remote control: ${device.typeName} (0x${device.shortAddress.toString(16)})`);
213
+ continue;
214
+ }
215
+
216
+ // Skip devices that are part of a group - the group handles them
217
+ if (groupedDeviceIds.has(device.shortAddress)) {
218
+ this.log.debug(`Skipping grouped device: ${device.typeName} (0x${device.shortAddress.toString(16)}) - controlled via group`);
219
+ continue;
220
+ }
221
+
222
+ // Generate unique identifier for this device
223
+ const uuid = this.api.hap.uuid.generate(`smartika-${device.shortAddress}`);
224
+ foundUUIDs.add(uuid);
225
+
226
+ // Check if accessory already exists (from cache)
227
+ const existingAccessory = this.accessories.get(uuid);
228
+
229
+ if (existingAccessory) {
230
+ // Update existing accessory
231
+ this.log.info(`Restoring cached accessory: ${device.typeName} (0x${device.shortAddress.toString(16)})`);
232
+ existingAccessory.context.device = device;
233
+ this.setupAccessory(existingAccessory, device);
234
+ } else {
235
+ // Create new accessory
236
+ this.log.info(`Adding new accessory: ${device.typeName} (0x${device.shortAddress.toString(16)})`);
237
+ this.addAccessory(device, uuid);
238
+ }
239
+ }
240
+
241
+ // Remove accessories that are no longer present
242
+ for (const [uuid, accessory] of this.accessories) {
243
+ if (!foundUUIDs.has(uuid)) {
244
+ this.log.info(`Removing stale accessory: ${accessory.displayName}`);
245
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
246
+ this.accessories.delete(uuid);
247
+ this.deviceHandlers.delete(uuid);
248
+ }
249
+ }
250
+
251
+ } catch (error) {
252
+ this.log.error('Failed to discover devices:', error.message);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Add a new accessory to Homebridge
258
+ * @param {Object} device - Device info from hub
259
+ * @param {string} uuid - Unique identifier
260
+ */
261
+ addAccessory(device, uuid) {
262
+ // Determine accessory category based on device type
263
+ let category;
264
+ switch (device.category) {
265
+ case protocol.DEVICE_CATEGORY.LIGHT:
266
+ category = this.api.hap.Categories.LIGHTBULB;
267
+ break;
268
+ case protocol.DEVICE_CATEGORY.FAN:
269
+ category = this.api.hap.Categories.FAN;
270
+ break;
271
+ case protocol.DEVICE_CATEGORY.PLUG:
272
+ category = this.api.hap.Categories.OUTLET;
273
+ break;
274
+ default:
275
+ category = this.api.hap.Categories.OTHER;
276
+ }
277
+
278
+ // Create accessory
279
+ const accessory = new this.api.platformAccessory(
280
+ device.typeName,
281
+ uuid,
282
+ category,
283
+ );
284
+
285
+ // Store device info in context for persistence
286
+ accessory.context.device = device;
287
+
288
+ // Configure the accessory
289
+ this.setupAccessory(accessory, device);
290
+
291
+ // Register the accessory
292
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
293
+ this.accessories.set(uuid, accessory);
294
+ }
295
+
296
+ /**
297
+ * Handle device status updates from hub
298
+ * @param {Array} devices - Array of device status objects
299
+ */
300
+ handleDeviceStatusUpdate(devices) {
301
+ this.log.debug(`Received status update for ${devices.length} device(s)`);
302
+
303
+ for (const status of devices) {
304
+ // Try to find handler for regular device
305
+ let uuid = this.api.hap.uuid.generate(`smartika-${status.shortAddress}`);
306
+ let handler = this.deviceHandlers.get(uuid);
307
+
308
+ // If not found, try as a group (groups use 0xFFxx addresses)
309
+ if (!handler && status.shortAddress >= 0xFF00) {
310
+ uuid = this.api.hap.uuid.generate(`smartika-group-${status.shortAddress}`);
311
+ handler = this.deviceHandlers.get(uuid);
312
+ }
313
+
314
+ if (handler) {
315
+ handler.updateStatus(status);
316
+ }
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Setup an accessory with appropriate services and handlers
322
+ * @param {import('homebridge').PlatformAccessory} accessory
323
+ * @param {Object} device - Device info
324
+ */
325
+ setupAccessory(accessory, device) {
326
+ // Get device from context if not provided
327
+ if (!device) {
328
+ device = accessory.context.device;
329
+ }
330
+
331
+ if (!device) {
332
+ this.log.warn('No device info available for accessory:', accessory.displayName);
333
+ return;
334
+ }
335
+
336
+ // Set up AccessoryInformation service
337
+ const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation) ||
338
+ accessory.addService(this.api.hap.Service.AccessoryInformation);
339
+
340
+ infoService
341
+ .setCharacteristic(this.api.hap.Characteristic.Manufacturer, 'Smartika')
342
+ .setCharacteristic(this.api.hap.Characteristic.Model, device.typeName)
343
+ .setCharacteristic(this.api.hap.Characteristic.SerialNumber, device.macAddress || `0x${device.shortAddress.toString(16)}`);
344
+
345
+ // Create appropriate handler based on device category
346
+ let handler;
347
+ switch (device.category) {
348
+ case protocol.DEVICE_CATEGORY.LIGHT:
349
+ handler = new SmartikaLightAccessory(this, accessory, device);
350
+ break;
351
+ case protocol.DEVICE_CATEGORY.FAN:
352
+ handler = new SmartikaFanAccessory(this, accessory, device);
353
+ break;
354
+ case protocol.DEVICE_CATEGORY.PLUG:
355
+ handler = new SmartikaPlugAccessory(this, accessory, device);
356
+ break;
357
+ default:
358
+ this.log.warn(`Unknown device category: ${device.category} for ${device.typeName}`);
359
+ return;
360
+ }
361
+
362
+ // Store handler reference
363
+ const uuid = accessory.UUID;
364
+ this.deviceHandlers.set(uuid, handler);
365
+ this.accessories.set(uuid, accessory);
366
+ }
367
+
368
+ /**
369
+ * REQUIRED - Called by Homebridge for each cached accessory on startup
370
+ * @param {import('homebridge').PlatformAccessory} accessory
371
+ */
372
+ configureAccessory(accessory) {
373
+ this.log.debug('Restoring accessory from cache:', accessory.displayName);
374
+ this.accessories.set(accessory.UUID, accessory);
375
+ // Note: setupAccessory will be called later in discoverDevices with fresh device data
376
+ }
377
+ }
378
+
379
+ module.exports = SmartikaPlatform;