mirror of
https://github.com/koush/scrypted.git
synced 2026-02-19 04:52:32 +00:00
170 lines
5.8 KiB
TypeScript
170 lines
5.8 KiB
TypeScript
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<string>) {
|
|
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;
|
|
}
|