import { closeQuiet, createBindZero } from '@scrypted/common/src/listen-cluster'; import sdk, { ScryptedDeviceType } from '@scrypted/sdk'; import { StorageSettingsDict } from "@scrypted/sdk/storage-settings"; import crypto, { randomBytes } from 'crypto'; import { once } from 'events'; import os from 'os'; import { Categories, EventedHTTPServer, HAPStorage } from './hap'; import { randomPinCode } from './pincode'; import './types'; class HAPLocalStorage { initSync() { } getItem(key: string): any { const data = localStorage.getItem(key); if (!data) return; return JSON.parse(data); } setItemSync(key: string, value: any) { localStorage.setItem(key, JSON.stringify(value)); } removeItemSync(key: string) { localStorage.removeItem(key); } persistSync() { } } // HAP storage seems to be global? export function initializeHapStorage() { HAPStorage.setStorage(new HAPLocalStorage()); } export function createHAPUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } export function getHAPUUID(storage: Storage) { let uuid = storage.getItem('uuid'); if (!uuid) { uuid = createHAPUUID(); storage.setItem('uuid', uuid); } return uuid; } export function typeToCategory(type: ScryptedDeviceType): Categories { switch (type) { case ScryptedDeviceType.Camera: return Categories.CAMERA; case ScryptedDeviceType.Doorbell: return Categories.VIDEO_DOORBELL; case ScryptedDeviceType.Fan: return Categories.FAN; case ScryptedDeviceType.Garage: return Categories.GARAGE_DOOR_OPENER; case ScryptedDeviceType.Irrigation: return Categories.SPRINKLER; case ScryptedDeviceType.Light: return Categories.LIGHTBULB; case ScryptedDeviceType.Lock: return Categories.DOOR_LOCK; case ScryptedDeviceType.Display: return Categories.TELEVISION; case ScryptedDeviceType.Outlet: return Categories.OUTLET; case ScryptedDeviceType.Sensor: return Categories.SENSOR; case ScryptedDeviceType.Switch: return Categories.SWITCH; case ScryptedDeviceType.Thermostat: return Categories.THERMOSTAT; case ScryptedDeviceType.Vacuum: return Categories.OUTLET; } } export function createHAPUsername() { const buffers = []; for (let i = 0; i < 6; i++) { buffers.push(randomBytes(1).toString('hex')); } return buffers.join(':'); } export function getAddresses() { const addresses = Object.entries(os.networkInterfaces()).filter(([iface]) => iface.startsWith('en') || iface.startsWith('eth') || iface.startsWith('wlan')).map(([_, addr]) => addr).flat().map(info => info.address).filter(address => address); return addresses; } export function getRandomPort() { return Math.round(30000 + Math.random() * 20000); } export function createHAPUsernameStorageSettingsDict(device: { storage: Storage, name?: string }, group: string, networkGroup = group): StorageSettingsDict<'mac' | 'qrCode' | 'pincode' | 'portOverride' | 'resetAccessory'> { const alertReload = () => { sdk.log.a(`You must reload the HomeKit plugin for the changes to ${device.name} to take effect.`); } return { qrCode: { group, title: "Pairing QR Code", type: 'qrcode', readonly: true, description: "Scan with your iOS camera to pair this Scrypted with HomeKit.", }, portOverride: { group: networkGroup, title: 'Bridge Port', persistedDefaultValue: getRandomPort(), description: 'Optional: The TCP port used by the Scrypted bridge. If none is specified, a random port will be chosen.', type: 'number', }, pincode: { group, title: "Manual Pairing Code", persistedDefaultValue: randomPinCode(), readonly: true, }, mac: { group, hide: true, title: "Username Override", persistedDefaultValue: createHAPUsername(), }, resetAccessory: { group, title: 'Reset Pairing', description: 'Resetting the pairing will resync it to HomeKit as a new device. Bridged devices will automatically relink as a new device. Accessory devices must be manually removed from the Home app and re-paired. Enter RESET to reset the pairing.', placeholder: 'RESET', mapPut: (oldValue, newValue) => { if (newValue === 'RESET') { device.storage.removeItem('mac'); alertReload(); // generate a new reset accessory random value. return crypto.randomBytes(8).toString('hex'); } throw new Error('HomeKit Accessory Reset cancelled.'); }, mapGet: () => '', }, } } export function logConnections(console: Console, accessory: any, seenConnections: Set) { const server: EventedHTTPServer = accessory._server.httpServer; server.on('connection-opened', connection => { connection.on('authenticated', () => { console.log('HomeKit Connection', connection.remoteAddress); seenConnections.add(connection.remoteAddress); }); }); } export async function pickPort() { const { port, server: tempSocket } = await createBindZero(); const closePromise = once(tempSocket, 'close'); closeQuiet(tempSocket); await closePromise; return port; }