mirror of
https://github.com/koush/scrypted.git
synced 2026-09-18 09:40:44 +01:00
homekit: merge SecuritySystem & child Sirens as one Homekit accessory (#650)
* add OnOff to SecuritySystem accessory as a switch * BFS to reorder devices + merge and skip devices * add some comments * more safety checks * embarrassing moment where I forgot this isn't BFS
This commit is contained in:
@@ -13,6 +13,7 @@ import { addAccessoryDeviceInfo } from './info';
|
||||
import { randomPinCode } from './pincode';
|
||||
import './types';
|
||||
import { VIDEO_CLIPS_NATIVE_ID } from './types/camera/camera-recording-files';
|
||||
import { reorderDevicesByProvider } from './util';
|
||||
import { VideoClipsMixinProvider } from './video-clips-provider';
|
||||
|
||||
const hapStorage: Storage = {
|
||||
@@ -109,6 +110,7 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
description: 'The last home hub to request a recording. Internally used to determine if a streaming request is coming from remote wifi.',
|
||||
},
|
||||
});
|
||||
mergedDevices = new Set<string>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -171,6 +173,7 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
|
||||
async start() {
|
||||
this.log.clearAlerts();
|
||||
this.mergedDevices = new Set<string>();
|
||||
|
||||
let defaultIncluded: any;
|
||||
try {
|
||||
@@ -181,10 +184,27 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
}
|
||||
|
||||
const plugins = await systemManager.getComponent('plugins');
|
||||
|
||||
const accessoryIds = new Set<string>();
|
||||
const deviceIds = Object.keys(systemManager.getSystemState());
|
||||
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
// when creating accessories in order, some DeviceProviders may merge in
|
||||
// their child devices (and report back which devices are merged via
|
||||
// this.mergedDevices)
|
||||
// we need to ensure that the iteration processes DeviceProviders before
|
||||
// their children, so a reordering is necessary
|
||||
const reorderedDeviceIds = reorderDevicesByProvider(deviceIds);
|
||||
|
||||
// safety checks in case something went wrong
|
||||
if (deviceIds.length !== reorderedDeviceIds.length) {
|
||||
throw Error(`error in device reordering, expected ${deviceIds.length} devices but only got ${reorderedDeviceIds.length}!`);
|
||||
}
|
||||
const uniqueDeviceIds = new Set<string>(deviceIds);
|
||||
const uniqueReorderedIds = new Set<string>(reorderedDeviceIds);
|
||||
if (uniqueDeviceIds.size !== uniqueReorderedIds.size) {
|
||||
throw Error(`error in device reordering, expected ${uniqueDeviceIds.size} unique devices but only got ${uniqueReorderedIds.size} entries!`);
|
||||
}
|
||||
|
||||
for (const id of reorderedDeviceIds) {
|
||||
const device = systemManager.getDeviceById<Online>(id);
|
||||
const supportedType = supportedTypes[device.type];
|
||||
if (!supportedType?.probe(device))
|
||||
@@ -206,6 +226,11 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.mergedDevices.has(device.id)) {
|
||||
this.console.log(`${device.name} was merged into an existing Homekit accessory and will not be exposed independently`)
|
||||
continue;
|
||||
}
|
||||
|
||||
this.console.log('adding', device.name);
|
||||
|
||||
const accessory = await supportedType.getAccessory(device, this);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import sdk, { Fan, AirQuality, AirQualitySensor, CO2Sensor, NOXSensor, PM10Sensor, PM25Sensor, ScryptedDevice, ScryptedInterface, VOCSensor, FanMode, OnOff } from "@scrypted/sdk";
|
||||
import sdk, { Fan, AirQuality, AirQualitySensor, CO2Sensor, NOXSensor, PM10Sensor, PM25Sensor, ScryptedDevice, ScryptedInterface, VOCSensor, FanMode, OnOff, DeviceProvider, ScryptedDeviceType } from "@scrypted/sdk";
|
||||
import { bindCharacteristic } from "../common";
|
||||
import { Accessory, Characteristic, CharacteristicEventTypes, Service, uuid } from '../hap';
|
||||
import type { HomeKitPlugin } from "../main";
|
||||
import { getService as getOnOffService } from "./onoff-base";
|
||||
|
||||
const { deviceManager } = sdk;
|
||||
const { deviceManager, systemManager } = sdk;
|
||||
|
||||
export function makeAccessory(device: ScryptedDevice, homekitPlugin: HomeKitPlugin, suffix?: string): Accessory {
|
||||
const mixinStorage = deviceManager.getMixinStorage(device.id, homekitPlugin.nativeId);
|
||||
@@ -11,6 +12,12 @@ export function makeAccessory(device: ScryptedDevice, homekitPlugin: HomeKitPlug
|
||||
return new Accessory(device.name, uuid.generate(resetId + device.id + (suffix ? '-' + suffix : '')));
|
||||
}
|
||||
|
||||
export function getChildDevices(device: ScryptedDevice & DeviceProvider): ScryptedDevice[] {
|
||||
const ids = Object.keys(systemManager.getSystemState());
|
||||
const allDevices = ids.map(id => systemManager.getDeviceById(id));
|
||||
return allDevices.filter(d => d.providerId == device.id);
|
||||
}
|
||||
|
||||
export function addAirQualitySensor(device: ScryptedDevice & AirQualitySensor & PM10Sensor & PM25Sensor & VOCSensor & NOXSensor, accessory: Accessory): Service {
|
||||
if (!device.interfaces.includes(ScryptedInterface.AirQualitySensor))
|
||||
return undefined;
|
||||
@@ -34,7 +41,7 @@ export function addAirQualitySensor(device: ScryptedDevice & AirQualitySensor &
|
||||
const airQualityService = accessory.addService(Service.AirQualitySensor);
|
||||
bindCharacteristic(device, ScryptedInterface.AirQualitySensor, airQualityService, Characteristic.AirQuality,
|
||||
() => airQualityToHomekit(device.airQuality));
|
||||
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.PM10Sensor)) {
|
||||
bindCharacteristic(device, ScryptedInterface.PM10Sensor, airQualityService, Characteristic.PM10Density,
|
||||
() => device.pm10Density || 0);
|
||||
@@ -157,4 +164,32 @@ export function addFan(device: ScryptedDevice & Fan & OnOff, accessory: Accessor
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
/*
|
||||
* addChildSirens looks for siren-type child devices of the given device provider
|
||||
* and merges them as switches to the accessory represented by the device provider.
|
||||
*
|
||||
* Returns the services created as well as all of the child siren devices which have
|
||||
* been merged.
|
||||
*/
|
||||
export function addChildSirens(device: ScryptedDevice & DeviceProvider, accessory: Accessory): { services: Service[], devices: (ScryptedDevice & OnOff)[] } {
|
||||
if (!device.interfaces.includes(ScryptedInterface.DeviceProvider))
|
||||
return undefined;
|
||||
|
||||
const children = getChildDevices(device);
|
||||
const sirenDevices = [];
|
||||
const services = children.map((child: ScryptedDevice & OnOff) => {
|
||||
if (child.type !== ScryptedDeviceType.Siren || !child.interfaces.includes(ScryptedInterface.OnOff))
|
||||
return undefined;
|
||||
|
||||
const onOffService = getOnOffService(child, accessory, Service.Switch)
|
||||
sirenDevices.push(child);
|
||||
return onOffService;
|
||||
});
|
||||
|
||||
return {
|
||||
services: services.filter(service => !!service),
|
||||
devices: sirenDevices,
|
||||
};
|
||||
}
|
||||
@@ -8,9 +8,7 @@ export function probe(device: DummyDevice): boolean {
|
||||
return device.interfaces.includes(ScryptedInterface.OnOff);
|
||||
}
|
||||
|
||||
export function getAccessory(device: ScryptedDevice & OnOff, homekitPlugin: HomeKitPlugin, serviceType: any): { accessory: Accessory, service: Service } | undefined {
|
||||
const accessory = makeAccessory(device, homekitPlugin);
|
||||
|
||||
export function getService(device: ScryptedDevice & OnOff, accessory: Accessory, serviceType: any): Service {
|
||||
const service = accessory.addService(serviceType, device.name);
|
||||
service.getCharacteristic(Characteristic.On)
|
||||
.on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => {
|
||||
@@ -22,7 +20,12 @@ export function getAccessory(device: ScryptedDevice & OnOff, homekitPlugin: Home
|
||||
})
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.OnOff, service, Characteristic.On, () => !!device.on);
|
||||
return service;
|
||||
}
|
||||
|
||||
export function getAccessory(device: ScryptedDevice & OnOff, homekitPlugin: HomeKitPlugin, serviceType: any): { accessory: Accessory, service: Service } | undefined {
|
||||
const accessory = makeAccessory(device, homekitPlugin);
|
||||
const service = getService(device, accessory, serviceType);
|
||||
return {
|
||||
accessory,
|
||||
service,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SecuritySystem, SecuritySystemMode, SecuritySystemObstruction, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from '@scrypted/sdk';
|
||||
import { SecuritySystem, SecuritySystemMode, SecuritySystemObstruction, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, DeviceProvider } from '@scrypted/sdk';
|
||||
import { addSupportedType, bindCharacteristic, DummyDevice } from '../common';
|
||||
import { Characteristic, CharacteristicEventTypes, CharacteristicSetCallback, CharacteristicValue, Service } from '../hap';
|
||||
import { makeAccessory } from './common';
|
||||
import { makeAccessory, addChildSirens } from './common';
|
||||
import type { HomeKitPlugin } from "../main";
|
||||
|
||||
addSupportedType({
|
||||
@@ -44,7 +44,7 @@ addSupportedType({
|
||||
|
||||
return Characteristic.SecuritySystemCurrentState.DISARMED;
|
||||
}
|
||||
|
||||
|
||||
function toTargetState(mode: SecuritySystemMode) {
|
||||
switch(mode) {
|
||||
case SecuritySystemMode.AwayArmed:
|
||||
@@ -71,7 +71,7 @@ addSupportedType({
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.SecuritySystem, service, Characteristic.SecuritySystemCurrentState,
|
||||
() => toCurrentState(device.securitySystemState?.mode, device.securitySystemState?.triggered));
|
||||
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.SecuritySystem, service, Characteristic.SecuritySystemTargetState,
|
||||
() => toTargetState(device.securitySystemState?.mode));
|
||||
|
||||
@@ -89,6 +89,14 @@ addSupportedType({
|
||||
bindCharacteristic(device, ScryptedInterface.SecuritySystem, service, Characteristic.SecuritySystemAlarmType,
|
||||
() => !!device.securitySystemState?.triggered);
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.DeviceProvider)) {
|
||||
const { devices } = addChildSirens(device as ScryptedDevice as ScryptedDevice & DeviceProvider, accessory);
|
||||
|
||||
// ensure child devices are skipped by the rest of homekit by
|
||||
// reporting that they've been merged
|
||||
devices.map(device => homekitPlugin.mergedDevices.add(device.id));
|
||||
}
|
||||
|
||||
return accessory;
|
||||
},
|
||||
});
|
||||
|
||||
42
plugins/homekit/src/util.ts
Normal file
42
plugins/homekit/src/util.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import sdk, { ScryptedInterface } from '@scrypted/sdk';
|
||||
|
||||
const { systemManager } = sdk;
|
||||
|
||||
/*
|
||||
* flattenDeviceTree performs a modified DFS tree traversal of the given
|
||||
* device mapping to produce a list of device ids. deviceId is the node
|
||||
* of the tree currently being processed, where null is the root of the
|
||||
* tree.
|
||||
*/
|
||||
function flattenDeviceTree(deviceMap: Map<string, string[]>, deviceId: string): string[] {
|
||||
const result: string[] = [];
|
||||
if (!deviceMap.has(deviceId)) // no children
|
||||
return result;
|
||||
|
||||
const children = deviceMap.get(deviceId);
|
||||
result.push(...children);
|
||||
children.map(child => result.push(...flattenDeviceTree(deviceMap, child)))
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* reorderDevicesByProvider returns a new ordering of the provided deviceIds
|
||||
* where it is guaranteed that DeviceProviders are listed before their children.
|
||||
*/
|
||||
export function reorderDevicesByProvider(deviceIds: string[]): string[] {
|
||||
const providerDeviceIdMap = new Map<string, string[]>();
|
||||
|
||||
deviceIds.map(deviceId => {
|
||||
const device = systemManager.getDeviceById(deviceId);
|
||||
|
||||
// when provider id is equal to device id, this is a root-level device/plugin
|
||||
const providerId = device.providerId !== device.id ? device.providerId : null;
|
||||
if (providerDeviceIdMap.has(providerId)) {
|
||||
providerDeviceIdMap.get(providerId).push(device.id);
|
||||
} else {
|
||||
providerDeviceIdMap.set(providerId, [device.id]);
|
||||
}
|
||||
});
|
||||
|
||||
return flattenDeviceTree(providerDeviceIdMap, null);
|
||||
}
|
||||
Reference in New Issue
Block a user