homebridge#DynamicPlatformPlugin TypeScript Examples
The following examples show how to use
homebridge#DynamicPlatformPlugin.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: platform.d.ts From homebridge-tuya-ir with Apache License 2.0 | 6 votes |
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export declare class TuyaIRPlatform implements DynamicPlatformPlugin {
readonly log: Logger;
readonly config: PlatformConfig;
readonly api: API;
readonly Service: typeof Service;
readonly Characteristic: typeof Characteristic;
readonly accessories: PlatformAccessory[];
cachedAccessories: Map<any, any>;
constructor(log: Logger, config: PlatformConfig, api: API);
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory): void;
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices(): void;
discover(tuya: any, i: any, total: any): void;
}
Example #2
Source File: platform.d.ts From homebridge-plugin-govee with Apache License 2.0 | 6 votes |
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export declare class GoveeHomebridgePlatform implements DynamicPlatformPlugin {
readonly log: Logger;
readonly config: PlatformConfig;
readonly api: API;
readonly Service: typeof Service;
readonly Characteristic: typeof Characteristic;
readonly accessories: PlatformAccessory[];
private platformStatus?;
private readonly discoveryCache;
constructor(log: Logger, config: PlatformConfig, api: API);
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory): void;
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices(): void;
private goveeDiscoveredReading;
private goveeScanStarted;
private goveeScanStopped;
private sanitize;
}
Example #3
Source File: wled-platform.ts From homebridge-simple-wled with ISC License | 5 votes |
export class WLEDPlatform implements DynamicPlatformPlugin {
accessories: PlatformAccessory[] = [];
readonly log: Logging;
readonly api: API;
readonly config: PlatformConfig;
private readonly wleds: WLED[] = [];
constructor(log: Logging, config: PlatformConfig, api: API) {
this.api = api;
this.config = config;
this.log = log;
if (!config) {
return;
}
if (!config.wleds) {
this.log("No WLEDs have been configured.");
return;
}
api.on(APIEvent.DID_FINISH_LAUNCHING, this.launchWLEDs.bind(this));
}
configureAccessory(accessory: PlatformAccessory): void {
this.accessories.push(accessory);
}
private launchWLEDs(): void {
for (const wled of this.config.wleds) {
if (!wled.host) {
this.log("No host or IP address has been configured.");
return;
}
loadEffects(wled.host).then((effects) => {
this.wleds.push(new WLED(this, wled, effects));
}).catch((error) => {
console.log(error)
});
}
}
}
Example #4
Source File: platform.ts From homebridge-iRobot with Apache License 2.0 | 4 votes |
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class iRobotPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
constructor(
public readonly log: Logger,
public readonly config: PlatformConfig,
public readonly api: API,
) {
this.log.debug('Finished initializing platform:', this.config.name);
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', () => {
log.debug('Executed didFinishLaunching callback');
// run the method to discover / register your devices as accessories
this.discoverDevices();
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
if (accessory.context.pluginVersion === undefined || accessory.context.pluginVersion < 3) {
this.log.warn('Removing Old Accessory:', accessory.displayName);
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
} else {
this.accessories.push(accessory);
}
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices() {
// loop over the discovered devices and register each one if it has not already been registered
this.log.info('Logging into iRobot...');
getRoombas(this.config, this.log).then(devices => {
for (const device of devices) {
//this.log.debug('Configuring device: \n', JSON.stringify(device));
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
const uuid = this.api.hap.uuid.generate(device.blid);
//const accessoryType = 'iRobotPlatformAccesoryV'+device.ver;
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
// existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
//new iRobotPlatformAccessory(this, existingAccessory, device);
//new platformAccessory[accessoryType](this, existingAccessory);
switch (device.swMajor){
case 1:
new platformAccessory.iRobotPlatformAccessoryV1(this, existingAccessory);
break;
case 2:
new platformAccessory.iRobotPlatformAccessoryV2(this, existingAccessory);
break;
case 3:
new platformAccessory.iRobotPlatformAccessoryV3(this, existingAccessory);
break;
}
// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.:
// remove platform accessories when no longer present
// this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
// this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
} else {
// the accessory does not yet exist, so we need to create it
this.log.info('Adding new accessory:', device.name);
// create a new accessory
const accessory = new this.api.platformAccessory(device.name, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
accessory.context.pluginVersion = 3;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
//new iRobotPlatformAccessory(this, accessory, device);
//new platformAccessory[accessoryType](this, accessory);
switch (device.ver){
case '1':
new platformAccessory.iRobotPlatformAccessoryV1(this, accessory);
break;
case '2':
new platformAccessory.iRobotPlatformAccessoryV2(this, accessory);
break;
case '3':
new platformAccessory.iRobotPlatformAccessoryV3(this, accessory);
break;
}
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
}
}).catch((error) => {
this.log.error(error);
});
}
}
Example #5
Source File: platform.ts From homebridge-lg-thinq-ac with Apache License 2.0 | 4 votes |
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class HomebridgeLgThinqPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service
public readonly Characteristic: typeof Characteristic =
this.api.hap.Characteristic
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = []
public thinqAuth: ThinqAuth | undefined
public thinqApi: ThinqApi | undefined
private didFinishLaunching: Promise<void>
private handleFinishedLaunching?: () => void
constructor(
public readonly log: Logger,
public readonly config: PlatformConfig & HomebridgeLgThinqPlatformConfig,
public readonly api: API,
) {
this.didFinishLaunching = new Promise((resolve) => {
// Store the resolver locally.
// Steps that depend on this can `await didFinishLaunching`.
// When Homebridge is finishes launching, this will be called to resolve.
this.handleFinishedLaunching = resolve
})
this.log.debug('Finished initializing platform:', this.config.name)
this.initialize()
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on(APIEvent.DID_FINISH_LAUNCHING, () => {
this.log.debug('Executed didFinishLaunching callback')
if (this.handleFinishedLaunching) {
this.handleFinishedLaunching()
}
})
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.debug('Restoring accessory from cache:', accessory.displayName)
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory)
}
async initialize() {
try {
const thinqConfig = await this.initializeThinqConfig()
this.thinqAuth = ThinqAuth.fromConfig(this.log, thinqConfig, this.config)
this.thinqApi = new ThinqApi(thinqConfig, this.thinqAuth)
await this.inititializeAuth()
this.startRefreshTokenInterval()
this.discoverDevicesWhenReady()
} catch (error) {
this.log.error('Error initializing platform', `${error}`)
this.log.debug('Full error', error)
}
}
async initializeThinqConfig() {
const partialThinqConfig: PartialThinqConfig = {
// If a user installs via the homebridge UI, these values
// may not be guaranteed
countryCode: this.config.country_code || 'US',
languageCode: this.config.language_code || 'en-US',
}
const gatewayUri = await ThinqApi.getGatewayUri(partialThinqConfig)
const thinqConfig: ThinqConfig = {
apiBaseUri: gatewayUri.result.thinq2Uri,
accessTokenUri: `https://${partialThinqConfig.countryCode.toLowerCase()}.lgeapi.com/oauth/1.0/oauth2/token`,
redirectUri: `https://kr.m.lgaccount.com/login/iabClose`,
authorizationUri: `${gatewayUri.result.empSpxUri}/login/signIn`,
countryCode: partialThinqConfig.countryCode,
languageCode: partialThinqConfig.languageCode,
}
return thinqConfig
}
async inititializeAuth() {
this.updateAndReplaceConfig()
const redirectedUrl = this.config.auth_redirected_url as unknown
if (this.thinqAuth?.getIsLoggedIn()) {
this.log.info('Already logged into ThinQ')
await this.refreshAuth()
} else if (typeof redirectedUrl === 'string' && redirectedUrl !== '') {
this.log.info('Initiating auth with provided redirect URL')
try {
await this.thinqAuth!.processLoginResult(redirectedUrl)
this.updateAndReplaceConfig()
} catch (error) {
this.log.error('Error setting refresh token', error)
throw error
}
} else {
this.log.debug(
'Redirected URL not stored in config and no existing auth state. Skipping initializeAuth().',
)
throw new Error('Auth not ready yet, please log in.')
}
}
private startRefreshTokenInterval() {
setInterval(() => this.refreshAuth(), AUTH_REFRESH_INTERVAL)
}
private async refreshAuth() {
this.log.debug('refreshAuth()')
try {
await this.thinqAuth!.initiateRefreshToken()
this.updateAndReplaceConfig()
} catch (error) {
if (
error instanceof Object &&
// @ts-expect-error TS2339 from upgrade to Typescript 4.5, proven to work on-device regardless
error.body instanceof Object &&
// @ts-expect-error TS2339 from upgrade to Typescript 4.5, proven to work on-device regardless
error.body.error instanceof Object &&
// @ts-expect-error TS2339 from upgrade to Typescript 4.5, proven to work on-device regardless
error.body.error.code === 'LG.OAUTH.EC.4001'
) {
this.log.error(
'Login credentials have expired!\n\n' +
'Please re-configure the plugin:\n' +
' 1. Login again\n' +
' 2. Update the "redirected URL" in the config\n' +
' 3. Restart Homebridge\n',
)
this.thinqAuth?.clearStoredToken()
this.updateAndReplaceConfig()
} else {
this.log.error('Failed to refresh token', `${error}`)
}
}
}
private async discoverDevicesWhenReady() {
await this.didFinishLaunching
// run the method to discover / register your devices as accessories
try {
await this.discoverDevices()
} catch (error) {
const errorString = `${error}`
this.log.error('Error discovering devices', `${error}`)
if (errorString.includes('status code 400')) {
this.log.error(
'This can sometimes indicate the LG App has new agreements you must accept. If so:\n' +
' 1. Open the native LG App and sign in as usual\n' +
' 2. If an agreement pops up, review and accept it if appropriate\n' +
' 3. Restart Homebridge\n' +
'If there are no agreements to accept, try restarting Homebridge.\n' +
"If that still doesn't work, delete the config for this accessory and restart Homebridge to initiate a full reset.",
)
}
}
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
async discoverDevices() {
if (!this.thinqAuth?.getIsLoggedIn()) {
this.log.info('Not logged in; skipping discoverDevices()')
return
}
const dashboardResponse = await this.thinqApi!.getDashboard()
this.log.debug('dashboardResponse', dashboardResponse)
this.log.info(
`Discover found ${dashboardResponse.result.item.length} total devices`,
)
const devices = dashboardResponse.result.item.filter((item) => {
if (typeof item !== 'object') {
this.log.debug('Item is not an object, ignoring')
return false
}
if (item.deviceType !== 401) {
// Air Conditioners have a 401 device type
this.log.debug(`deviceType is ${item.deviceType}, ignoring`)
return false
}
if (item.platformType !== 'thinq2') {
this.log.error(
`"${item.alias}" (model ${item.modelName}) uses the ${item.platformType} platform, which is not supported. ` +
`Please see https://github.com/sman591/homebridge-lg-thinq-ac/issues/4 for updates.`,
)
return false
}
return true
})
// Keep a running list of all accessories we register or know were already registered
const matchedAccessories: PlatformAccessory[] = []
// loop over the discovered devices and register each one if it has not already been registered
for (const device of devices) {
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
const uuid = this.api.hap.uuid.generate(device.deviceId)
const matchingAccessories = this.accessories.filter(
(accessory) => accessory.UUID === uuid,
)
if (matchingAccessories.length > 0) {
this.log.info('Existing accessory:', device.alias)
// check that the device has not already been registered by checking the
// cached devices we stored in the `configureAccessory` method above
for (const accessory of matchingAccessories) {
accessory.context.device = device
matchedAccessories.push(accessory)
new LgAirConditionerPlatformAccessory(this, accessory)
}
} else if (!device.online) {
this.log.info(
`Accessory "${device.alias}" is offline and will not be added.`,
)
} else {
this.log.info('Registering new accessory:', device.alias)
// create a new accessory
const accessory = new this.api.platformAccessory(device.alias, uuid)
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device
// create the accessory handler
// this is imported from `platformAccessory.ts`
new LgAirConditionerPlatformAccessory(this, accessory)
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
])
// push into accessory cache
this.accessories.push(accessory)
matchedAccessories.push(accessory)
}
}
// Unregister offline accessories if desired (set by config)
if (this.config.remove_offline_devices_on_boot) {
this.log.info('Attempting to remove offline devices from HomeKit...')
// Only remove once when config is enabled
this.config.remove_offline_devices_on_boot = false
this.updateAndReplaceConfig()
let wasADeviceRemoved = false
this.accessories.forEach((accessory) => {
const deviceContext = accessory.context?.device
if (deviceContext?.online === false) {
this.log.info(
`Removing offline device "${accessory.displayName}" from HomeKit. If you need this device again, please restart Homebridge.`,
)
// @ts-expect-error This is a hack
const jsInstance = accessory.jsInstance as
| LgAirConditionerPlatformAccessory
| undefined
if (
jsInstance &&
jsInstance instanceof LgAirConditionerPlatformAccessory
) {
jsInstance?.unregisterAccessory()
wasADeviceRemoved = true
} else {
this.log.warn(
`Device "${accessory.displayName}" is offline, but could not be removed. Please file a bug with homebridge-lg-thinq-ac.`,
)
}
}
})
if (!wasADeviceRemoved) {
this.log.warn(
'remove_offline_devices_on_boot was attempted but no offline devices were found',
)
}
}
// Unregister accessories that weren't matched from the API response.
// This helps clean up devices which:
// - You no longer have connected to your account
// - Were mistakenly registered in an older version of this plugin but aren't actually supported
this.accessories.forEach((accessory) => {
const didMatchAccessory = matchedAccessories.some(
(matchedAccessory) => matchedAccessory.UUID === accessory.UUID,
)
if (!didMatchAccessory) {
this.log.info(
'Un-registering unknown accessory:',
accessory.displayName,
)
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
])
}
})
}
getRefreshIntervalMinutes() {
const fallbackDefault = 1
try {
const parsedValue = parseFloat(this.config.refresh_interval)
if (parsedValue > 0.1 && parsedValue < 100000) {
return parsedValue
}
} catch (error) {
this.log.error('Failed to parse refresh_interval from config', error)
}
this.log.debug('Using fallback refresh interval')
return fallbackDefault
}
updateAndReplaceConfig() {
const configPath = this.api.user.configPath()
const configString = readFileSync(configPath).toString()
try {
const config = JSON.parse(configString)
// this.log.debug('config', config) DO NOT COMMIT THIS -- it could accidentally leak into GitHub issue reports
const platforms = config.platforms.filter(
(platform: Record<string, string>) =>
platform.platform === 'LgThinqAirConditioner',
)
const authConfig = this.thinqAuth!.serializeToConfig()
const generalConfig = {
remove_offline_devices_on_boot:
this.config.remove_offline_devices_on_boot || false,
}
const platformConfig: Required<HomebridgeLgThinqPlatformConfig> = {
...authConfig,
...generalConfig,
}
for (const platform of platforms) {
Object.assign(platform, platformConfig)
}
writeFileSync(configPath, JSON.stringify(config))
} catch (error) {
this.log.error('Failed to store updated config', `${error}`)
this.log.debug('Full error:', error)
}
}
}
Example #6
Source File: platform.ts From homebridge-screenlogic with MIT License | 4 votes |
/**npm
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class ScreenLogicPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic
// this is used to track restored cached accessories
private restoredAccessories: PlatformAccessory[] = []
// bridge controller to talk to shades
private controller: Controller
// configuration
public config: ScreenLogicPlatformConfig
// fetched config
private poolConfig?: PoolConfig
// the time (from Date.now()) when we last initiated or did a refresh
private lastRefreshTime = 0
private poolTempAccessory?: TemperatureAccessory
private spaTempAccessory?: TemperatureAccessory
private airTempAccessory?: TemperatureAccessory
private poolThermostatAccessory?: ThermostatAccessory
private spaThermostatAccessory?: ThermostatAccessory
private circuitAccessories: CircuitAccessory[] = []
constructor(public readonly log: Logger, config: PlatformConfig, public readonly api: API) {
this.log.debug('Finished initializing platform', PLATFORM_NAME)
this.config = config as ScreenLogicPlatformConfig
// do this first to make sure we have proper defaults moving forward
this.applyConfigDefaults(config)
this.controller = new Controller({
log: this.log,
ip_address: this.config.ip_address,
port: this.config.port,
username: this.config.username,
password: this.config.password,
})
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', () => {
log.debug('Executed didFinishLaunching callback')
// run the method to discover / register your devices as accessories
this.discoverDevices(0)
})
}
private discoverDevices(retryAttempt: number) {
// test
this.log.info('discoverDevices')
const pollingInterval = 60 // TODO: get from config
this.controller
.getPoolConfig()
.then(config => {
this.poolConfig = config
this.log.debug('got pool config', this.poolConfig)
this.log.info(
`discoverDevices connected: ${this.poolConfig.deviceId} ${this.poolConfig.softwareVersion}`,
)
this.setupDiscoveredAccessories(this.poolConfig)
})
.catch(err => {
// on error, start another timeout with backoff
const timeout = this.backoff(retryAttempt, pollingInterval)
this.log.error(
`discoverDevices retryAttempt: ${retryAttempt} timeout: ${timeout} error: ${err}`,
)
setTimeout(() => this.discoverDevices(retryAttempt + 1), timeout * 1000)
})
}
private setupDiscoveredAccessories(poolConfig: PoolConfig) {
// this is used to track active accessories uuids
const activeAccessories = new Set<string>()
// make pool temperature sensor if needed
if (poolConfig.hasPool && !this.config.hidePoolTemperatureSensor) {
this.poolTempAccessory = this.configureAccessoryType(TemperatureAccessory.makeAdaptor(), {
displayName: POOL_TEMP_NAME,
type: POOL_TEMP_NAME,
} as TemperatureAccessoryContext)
activeAccessories.add(this.poolTempAccessory.UUID)
}
// make spa temperature sensor if needed
if (poolConfig.hasSpa && !this.config.hideSpaTemperatureSensor) {
this.spaTempAccessory = this.configureAccessoryType(TemperatureAccessory.makeAdaptor(), {
displayName: SPA_TEMP_NAME,
type: SPA_TEMP_NAME,
} as TemperatureAccessoryContext)
activeAccessories.add(this.spaTempAccessory.UUID)
}
// make air temperature sensor if needed
if (!this.config.hideAirTemperatureSensor) {
this.airTempAccessory = this.configureAccessoryType(TemperatureAccessory.makeAdaptor(), {
displayName: AIR_TEMP_NAME,
type: AIR_TEMP_NAME,
} as TemperatureAccessoryContext)
activeAccessories.add(this.airTempAccessory.UUID)
}
// make pool thermostat if needed
if (poolConfig.hasPool && !this.config.hidePoolThermostat) {
this.poolThermostatAccessory = this.configureAccessoryType(
ThermostatAccessory.makeAdaptor(),
{
displayName: POOL_THERMOSTAT_NAME,
bodyType: 0,
minSetPoint: this.normalizeTemperature(poolConfig.poolMinSetPoint),
maxSetPoint: this.normalizeTemperature(poolConfig.poolMaxSetPoint),
} as ThermostatAccessoryContext,
)
activeAccessories.add(this.poolThermostatAccessory.UUID)
}
// make spa thermostat if needed
if (poolConfig.hasSpa && !this.config.hideSpaThermostat) {
this.spaThermostatAccessory = this.configureAccessoryType(ThermostatAccessory.makeAdaptor(), {
displayName: SPA_THERMOSTAT_NAME,
bodyType: 1,
minSetPoint: this.normalizeTemperature(poolConfig.spaMinSetPoint),
maxSetPoint: this.normalizeTemperature(poolConfig.spaMaxSetPoint),
} as ThermostatAccessoryContext)
activeAccessories.add(this.spaThermostatAccessory.UUID)
}
// filter out hidden circuits
const hiddenNames = (this.config.hidden_circuits as string) || ''
const hiddenCircuits = new Set(hiddenNames.split(',').map(item => item.trim()))
poolConfig.circuits = poolConfig.circuits.filter(circuit => {
return !hiddenCircuits.has(circuit.name)
})
for (const circuit of poolConfig.circuits) {
const accessory = this.configureAccessoryType(CircuitAccessory.makeAdaptor(), {
displayName: circuit.name,
id: circuit.id,
} as CircuitAccessoryContext)
this.circuitAccessories.push(accessory)
activeAccessories.add(accessory.UUID)
}
if (this.config.createLightColorSwitches) {
const accessory = this.configureAccessoryType(SetColorAccessory.makeAdaptor(), {
displayName: 'Light Colors',
id: 1,
} as CircuitAccessoryContext)
activeAccessories.add(accessory.UUID)
}
// unregister orphaned accessories
const staleAccessories = this.restoredAccessories.filter(
accessory => !activeAccessories.has(accessory.UUID),
)
if (staleAccessories.length) {
const staleNames = staleAccessories.map(accessory => accessory.displayName)
this.log.info('unregistering accessories', staleNames)
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, staleAccessories)
}
// start polling for status
this.pollForStatus(0)
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
public configureAccessory(accessory: PlatformAccessory) {
this.log.info('Loading accessory from cache:', accessory.displayName)
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.restoredAccessories.push(accessory)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private configureAccessoryType<T>(adaptor: AccessoryAdaptor<T>, context: Record<string, any>): T {
// generate a unique id for this shade based on context
const uuid = adaptor.generateUUID(this, context)
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.restoredAccessories.find(accessory => accessory.UUID === uuid)
if (existingAccessory) {
// the accessory already exists
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName)
// update the context if it has changed
if (!adaptor.sameContext(context, existingAccessory.context)) {
existingAccessory.context = context
this.log.info('Updating existing accessory:', context.displayName)
this.api.updatePlatformAccessories([existingAccessory])
}
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
return adaptor.factory(this, existingAccessory)
} else {
// the accessory does not yet exist, so we need to create it
this.log.info('Adding new accessory:', context.displayName)
// create a new accessory
const accessory = new this.api.platformAccessory(context.displayName, uuid)
// store a copy of the device object in the `accessory.context`
accessory.context = context
// create the accessory handler for the newly create accessory
const newAccessory = adaptor.factory(this, accessory)
// link the accessory to platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
return newAccessory
}
}
/** start polling process with truncated exponential backoff: https://cloud.google.com/storage/docs/exponential-backoff */
private pollForStatus(retryAttempt: number) {
const pollingInterval = this.config.statusPollingSeconds
this.refreshStatus()
.then(() => {
// on success, start another timeout at normal pollingInterval
this.log.debug('_pollForStatus success, retryAttempt:', retryAttempt)
setTimeout(() => this.pollForStatus(0), pollingInterval * 1000)
})
.catch(err => {
// on error, start another timeout with backoff
const timeout = this.backoff(retryAttempt, pollingInterval)
this.log.error('_pollForStatus retryAttempt:', retryAttempt, 'timeout:', timeout, err)
setTimeout(() => this.pollForStatus(retryAttempt + 1), timeout * 1000)
})
}
/** gets status, updates accessories, and resolves */
private async refreshStatus() {
try {
this.lastRefreshTime = Date.now()
const poolStatus = await this.controller.getPoolStatus()
this.log.debug('connected:', this.poolConfig?.deviceId, '(getStatus)')
// update all values
this.updateAccessories(poolStatus, undefined)
return null
} catch (err) {
this.updateAccessories(undefined, err)
throw err
}
}
private updateAccessories(status?: PoolStatus, _err?: Error) {
if (status) {
this.airTempAccessory?.updateCurrentTemperature(
this.normalizeTemperature(status.airTemperature),
)
this.airTempAccessory?.updateStatusActive(true)
this.poolTempAccessory?.updateCurrentTemperature(
this.normalizeTemperature(status.poolTemperature),
)
this.poolTempAccessory?.updateStatusActive(status.isPoolActive)
this.spaTempAccessory?.updateCurrentTemperature(
this.normalizeTemperature(status.spaTemperature),
)
this.spaTempAccessory?.updateStatusActive(status.isSpaActive)
if (this.poolThermostatAccessory) {
this.poolThermostatAccessory.updateCurrentTemperature(
this.normalizeTemperature(status.poolTemperature),
)
this.poolThermostatAccessory.updateTargetTemperature(
this.normalizeTemperature(status.poolSetPoint),
)
this.poolThermostatAccessory.updateCurrentHeatingCoolingState(
status.isPoolHeating
? this.Characteristic.CurrentHeatingCoolingState.HEAT
: this.Characteristic.CurrentHeatingCoolingState.OFF,
)
this.poolThermostatAccessory.updateTargetHeatingCoolingState(
this.mapHeatModeToTargetHeatingCoolingState(status.poolHeatMode),
)
}
if (this.spaThermostatAccessory) {
this.spaThermostatAccessory.updateCurrentTemperature(
this.normalizeTemperature(status.spaTemperature),
)
this.spaThermostatAccessory.updateTargetTemperature(
this.normalizeTemperature(status.spaSetPoint),
)
this.spaThermostatAccessory.updateCurrentHeatingCoolingState(
status.isSpaHeating
? this.Characteristic.CurrentHeatingCoolingState.HEAT
: this.Characteristic.CurrentHeatingCoolingState.OFF,
)
this.spaThermostatAccessory.updateTargetHeatingCoolingState(
this.mapHeatModeToTargetHeatingCoolingState(status.spaHeatMode),
)
}
for (const circuitAccessory of this.circuitAccessories) {
circuitAccessory.updateOn(
status.circuitState.get(circuitAccessory.context.id) ? true : false,
)
}
}
}
/** refresh if cached values are "stale" */
private refreshIfNeeded(): boolean {
const now = Date.now()
if (now - this.lastRefreshTime > 15 * 1000) {
// set now, so we don't trigger multiple...
this.lastRefreshTime = now
this.refreshStatus().catch(err => {
this.log.error('refreshIfNeeded', err)
})
return true
} else {
return false
}
}
/** convenience function to add an `on('get')` handler which refreshes accessory values if needed */
public triggersRefreshIfNeded(service: Service, type: CharacteristicType): void {
const characteristic = service.getCharacteristic(type)
characteristic.on('get', (callback: CharacteristicGetCallback) => {
// just return current cached value, and refresh if needed
callback(null, characteristic.value)
if (this.refreshIfNeeded()) {
this.log.debug('triggered refresh on get', service.displayName, characteristic.displayName)
}
})
}
public setTargetTemperature(context: ThermostatAccessoryContext, temperature: number): void {
if (this.poolConfig === undefined) {
this.log.warn('setTargetTemperature failed: poolConfig is undefined')
return
}
// need to convert from Celsius to what pool is conifigured for
const heatPoint = this.poolConfig.isCelsius ? temperature : Math.round(temperature * 1.8 + 32)
this.controller
.setHeatPoint(context.bodyType, heatPoint)
.then(() => {
this.log.debug(
'setTargetTemperature: successfully set target temperature: ',
context.bodyType,
heatPoint,
)
})
.catch(err => {
this.log.error('setTargetTemperature', err)
})
}
public setTargetHeatingCoolingState(context: ThermostatAccessoryContext, state: number): void {
this.controller
.setHeatMode(context.bodyType, this.mapTargetHeatingCoolingStateToHeatMode(state))
.then(() => {
this.log.debug(
'setTargetHeatingCoolingState: successfully set target heating/cooling state: ',
context.bodyType,
state,
)
})
.catch(err => {
this.log.error('setTargetHeatingCoolingState', err)
})
}
public setCircuitState(context: CircuitAccessoryContext, state: boolean): void {
this.controller
.setCircuitState(context.id, state)
.then(() => {
this.log.debug('setCircuitState: successfully set circuit state: ', context.id, state)
})
.catch(err => {
this.log.error('setCircuitState', err)
})
}
public sendLightCommand(_context: SetColorAccessoryContext, cmd: number): void {
this.controller
.sendLightCommand(cmd)
.then(() => {
this.log.debug('sendLightCommand: successfully sent light command: ', cmd)
// sending the light command will turn on pool/spa lights, so refresh
setTimeout(() => {
this.refreshStatus().catch(err => {
this.log.error('sendLightCommand refresh', err)
})
}, 2500)
})
.catch(err => {
this.log.error('sendLightCommand', err)
})
}
public generateUUID(accessorySalt: string) {
if (this.poolConfig !== undefined) {
return this.api.hap.uuid.generate(this.poolConfig.deviceId + ':' + accessorySalt)
} else {
this.log.error('poolConfig is undefined')
return ''
}
}
private backoff(retryAttempt: number, maxTime: number): number {
retryAttempt = Math.max(retryAttempt, 1)
return Math.min(Math.pow(retryAttempt - 1, 2) + Math.random(), maxTime)
}
public accessoryInfo(): {
manufacturer: string
model: string
serialNumber: string
} {
if (this.poolConfig) {
return {
manufacturer: 'Pentair',
// store software version in model, since it doesn't follow
// proper n.n.n format Apple requires and model is a string
model: this.poolConfig.softwareVersion,
serialNumber: this.poolConfig.deviceId,
}
} else {
this.log.error('poolConfig is null getting accessoryInfo')
return {
manufacturer: 'unknown',
model: 'unknown',
serialNumber: '',
}
}
}
private applyConfigDefaults(config: PlatformConfig) {
// config.ip_address
config.port = config.port ?? 80
// config.username
// config.password
// config.hidden_circuits
config.hideAirTemperatureSensor = config.hideAirTemperatureSensor ?? false
config.hidePoolTemperatureSensor = config.hidePoolTemperatureSensor ?? false
config.hideSpaTemperatureSensor = config.hideSpaTemperatureSensor ?? false
config.hidePoolThermostat = config.hidePoolThermostat ?? false
config.hideSpaThermostat = config.hideSpaThermostat ?? false
config.statusPollingSeconds = config.statusPollingSeconds ?? 60
config.createLightColorSwitches = config.createLightColorSwitches ?? false
config.disabledLightColors = config.disabledLightColors ?? []
this.log.debug('config', this.config)
}
/** normalize temperature to celsius for homekit */
normalizeTemperature(temperature: number): number {
return this.poolConfig?.isCelsius ? temperature : (temperature - 32) / 1.8
}
/** map pool heat mode to thermostat target heating/coooling state */
mapHeatModeToTargetHeatingCoolingState(poolHeatMode: number) {
switch (poolHeatMode) {
case Controller.HEAT_MODE_OFF:
return this.Characteristic.TargetHeatingCoolingState.OFF
case Controller.HEAT_MODE_HEAT_PUMP:
return this.Characteristic.TargetHeatingCoolingState.HEAT
case Controller.HEAT_MODE_SOLAR_PREFERRED:
return this.Characteristic.TargetHeatingCoolingState.AUTO
case Controller.HEAT_MODE_SOLAR:
return this.Characteristic.TargetHeatingCoolingState.COOL
default:
return this.Characteristic.TargetHeatingCoolingState.OFF
}
}
/** map thermostat target heating/coooling state to pool heat mode */
mapTargetHeatingCoolingStateToHeatMode(targetHeatingCoolingState: number) {
switch (targetHeatingCoolingState) {
case this.Characteristic.TargetHeatingCoolingState.OFF:
return Controller.HEAT_MODE_OFF
case this.Characteristic.TargetHeatingCoolingState.HEAT:
return Controller.HEAT_MODE_HEAT_PUMP
case this.Characteristic.TargetHeatingCoolingState.AUTO:
return Controller.HEAT_MODE_SOLAR_PREFERRED
case this.Characteristic.TargetHeatingCoolingState.COOL:
return Controller.HEAT_MODE_SOLAR
default:
return Controller.HEAT_MODE_UNCHANGED
}
}
}
Example #7
Source File: platform.ts From homebridge-eufy-security with Apache License 2.0 | 4 votes |
export class EufySecurityPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
public eufyClient: EufySecurity;
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
public readonly config: EufySecurityPlatformConfig;
private eufyConfig: EufySecurityConfig;
public log;
constructor(
public readonly hblog: Logger,
config: PlatformConfig,
public readonly api: API,
) {
this.config = config as EufySecurityPlatformConfig;
this.eufyConfig = {
username: this.config.username,
password: this.config.password,
country: 'US',
language: 'en',
persistentDir: api.user.storagePath(),
p2pConnectionSetup: 0,
pollingIntervalMinutes: this.config.pollingIntervalMinutes ?? 10,
eventDurationSeconds: 10,
} as EufySecurityConfig;
this.config.ignoreStations = this.config.ignoreStations || [];
this.config.ignoreDevices = this.config.ignoreDevices || [];
if (this.config.enableDetailedLogging >= 1) {
const plugin = require('../package.json');
this.log = bunyan.createLogger({
name: '[EufySecurity-' + plugin.version + ']',
hostname: '',
streams: [{
level: (this.config.enableDetailedLogging === 2) ? 'trace' : 'debug',
type: 'raw',
stream: bunyanDebugStream({
forceColor: true,
showProcess: false,
showPid: false,
showDate: (time) => {
return '[' + time.toLocaleString('en-US') + ']';
},
}),
}],
serializers: bunyanDebugStream.serializers,
});
this.log.info('Eufy Security Plugin: enableDetailedLogging on');
} else {
this.log = hblog;
}
this.eufyClient = (this.config.enableDetailedLogging === 2)
? new EufySecurity(this.eufyConfig, this.log)
: new EufySecurity(this.eufyConfig);
// Removing the ability to set Off(6) waiting Bropat feedback bropat/eufy-security-client#27
this.config.hkOff = (this.config.hkOff === 6) ? 63 : this.config.hkOff;
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', async () => {
// await this.createConnection();
// run the method to discover / register your devices as accessories
await this.discoverDevices();
});
this.log.info('Finished initializing Eufy Security Platform');
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.debug('Loading accessory from cache:', accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
async discoverDevices() {
this.log.debug('discoveringDevices');
try {
await this.eufyClient.connect();
this.log.debug('EufyClient connected ' + this.eufyClient.isConnected());
} catch (e) {
this.log.error('Error authenticating Eufy : ', e);
}
if (!this.eufyClient.isConnected()) {
this.log.error('Not connected can\'t continue!');
return;
}
await this.refreshData(this.eufyClient);
this.eufyClient.on('push connect', () => {
this.log.debug('Push Connected!');
});
this.eufyClient.on('push close', () => {
this.log.warn('Push Closed!');
});
const eufyStations = await this.eufyClient.getStations();
this.log.debug('Found ' + eufyStations.length + ' stations.');
const devices: Array<DeviceContainer> = [];
for (const station of eufyStations) {
this.log.debug(
'Found Station',
station.getSerial(),
station.getName(),
DeviceType[station.getDeviceType()],
station.getLANIPAddress(),
);
if (this.config.ignoreStations.indexOf(station.getSerial()) !== -1) {
this.log.debug('Device ignored');
continue;
}
const deviceContainer: DeviceContainer = {
deviceIdentifier: {
uniqueId: station.getSerial(),
displayName: station.getName(),
type: station.getDeviceType(),
station: true,
} as DeviceIdentifier,
eufyDevice: station,
};
devices.push(deviceContainer);
}
const eufyDevices = await this.eufyClient.getDevices();
this.log.debug('Found ' + eufyDevices.length + ' devices.');
for (const device of eufyDevices) {
this.log.debug(
'Found device',
device.getSerial(),
device.getName(),
DeviceType[device.getDeviceType()],
);
if (this.config.ignoreStations.indexOf(device.getStationSerial()) !== -1) {
this.log.debug('Device ignored because station is ignored');
continue;
}
if (this.config.ignoreDevices.indexOf(device.getSerial()) !== -1) {
this.log.debug('Device ignored');
continue;
}
const deviceContainer: DeviceContainer = {
deviceIdentifier: {
uniqueId: device.getSerial(),
displayName: device.getName(),
type: device.getDeviceType(),
station: false,
} as DeviceIdentifier,
eufyDevice: device,
};
devices.push(deviceContainer);
}
const activeAccessoryIds: string[] = [];
// loop over the discovered devices and register each one if it has not already been registered
for (const device of devices) {
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
let uuid = this.api.hap.uuid.generate(device.deviceIdentifier.uniqueId);
// Checking Device Type if it's not a station, it will be the same serial number we will find
// in Device list and it will create the same UUID
if (device.deviceIdentifier.type !== DeviceType.STATION && device.deviceIdentifier.station) {
uuid = this.api.hap.uuid.generate('s_' + device.deviceIdentifier.uniqueId);
this.log.debug('This device is not a station. Generating a new UUID to avoid any duplicate issue');
}
activeAccessoryIds.push(uuid);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(
(accessory) => accessory.UUID === uuid,
);
if (existingAccessory) {
// the accessory already exists
if (this.register_accessory(
existingAccessory,
device.deviceIdentifier.type,
device.eufyDevice,
device.deviceIdentifier.station,
)
) {
this.log.info(
'Restoring existing accessory from cache:',
existingAccessory.displayName,
);
// update accessory cache with any changes to the accessory details and information
this.api.updatePlatformAccessories([existingAccessory]);
}
} else {
// the accessory does not yet exist, so we need to create it
// create a new accessory
const accessory = new this.api.platformAccessory(
device.deviceIdentifier.displayName,
uuid,
);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device.deviceIdentifier;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
if (
this.register_accessory(
accessory,
device.deviceIdentifier.type,
device.eufyDevice,
device.deviceIdentifier.station,
)
) {
this.log.info(
'Adding new accessory:',
device.deviceIdentifier.displayName,
);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
}
}
}
// Cleaning cached accessory which are no longer exist
const staleAccessories = this.accessories.filter((item) => {
return activeAccessoryIds.indexOf(item.UUID) === -1;
});
staleAccessories.forEach((staleAccessory) => {
this.log.info(`Removing cached accessory ${staleAccessory.UUID} ${staleAccessory.displayName}`);
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [staleAccessory]);
});
}
private register_accessory(
accessory: PlatformAccessory,
type: number,
device,
station: boolean,
) {
this.log.debug(accessory.displayName, 'UUID:', accessory.UUID);
/* Under development area
This need to be rewrite
*/
if (station) {
if (type !== DeviceType.STATION) {
// Allowing camera but not the lock nor doorbell for now
if (!(type === DeviceType.LOCK_BASIC
|| type === DeviceType.LOCK_ADVANCED
|| type === DeviceType.LOCK_BASIC_NO_FINGER
|| type === DeviceType.LOCK_ADVANCED_NO_FINGER
|| type === DeviceType.DOORBELL
|| type === DeviceType.BATTERY_DOORBELL
|| type === DeviceType.BATTERY_DOORBELL_2)) {
this.log.warn(accessory.displayName, 'looks station but it\'s not could imply some errors', 'Type:', type);
new StationAccessory(this, accessory, device as Station);
return true;
} else {
return false;
}
}
}
switch (type) {
case DeviceType.STATION:
new StationAccessory(this, accessory, device as Station);
break;
case DeviceType.MOTION_SENSOR:
new MotionSensorAccessory(this, accessory, device as MotionSensor);
break;
case DeviceType.CAMERA:
case DeviceType.CAMERA2:
case DeviceType.CAMERA_E:
case DeviceType.CAMERA2C:
case DeviceType.INDOOR_CAMERA:
case DeviceType.INDOOR_PT_CAMERA:
case DeviceType.FLOODLIGHT:
case DeviceType.CAMERA2C_PRO:
case DeviceType.CAMERA2_PRO:
case DeviceType.INDOOR_CAMERA_1080:
case DeviceType.INDOOR_PT_CAMERA_1080:
case DeviceType.SOLO_CAMERA:
case DeviceType.SOLO_CAMERA_PRO:
case DeviceType.SOLO_CAMERA_SPOTLIGHT_1080:
case DeviceType.SOLO_CAMERA_SPOTLIGHT_2K:
case DeviceType.SOLO_CAMERA_SPOTLIGHT_SOLAR:
case DeviceType.INDOOR_OUTDOOR_CAMERA_1080P:
case DeviceType.INDOOR_OUTDOOR_CAMERA_1080P_NO_LIGHT:
case DeviceType.INDOOR_OUTDOOR_CAMERA_2K:
case DeviceType.FLOODLIGHT_CAMERA_8422:
case DeviceType.FLOODLIGHT_CAMERA_8423:
case DeviceType.FLOODLIGHT_CAMERA_8424:
new CameraAccessory(this, accessory, device as Camera);
break;
case DeviceType.DOORBELL:
case DeviceType.BATTERY_DOORBELL:
case DeviceType.BATTERY_DOORBELL_2:
new DoorbellCameraAccessory(this, accessory, device as DoorbellCamera);
break;
case DeviceType.SENSOR:
new EntrySensorAccessory(this, accessory, device as EntrySensor);
break;
case DeviceType.KEYPAD:
new KeypadAccessory(this, accessory, device as Keypad);
break;
case DeviceType.LOCK_BASIC:
case DeviceType.LOCK_ADVANCED:
case DeviceType.LOCK_BASIC_NO_FINGER:
case DeviceType.LOCK_ADVANCED_NO_FINGER:
new SmartLockAccessory(this, accessory, device as Lock);
break;
default:
this.log.warn(
'This accessory is not compatible with HomeBridge Eufy Security plugin:',
accessory.displayName,
'Type:',
type,
);
return false;
}
return true;
}
public async refreshData(client: EufySecurity): Promise<void> {
this.log.debug(
`PollingInterval: ${this.eufyConfig.pollingIntervalMinutes}`,
);
if (client) {
this.log.debug('Refresh data from cloud and schedule next refresh.');
try {
await client.refreshCloudData();
} catch (error) {
this.log.error('Error refreshing data from Eufy: ', error);
}
setTimeout(() => {
try {
this.refreshData(client);
} catch (error) {
this.log.error('Error refreshing data from Eufy: ', error);
}
}, this.eufyConfig.pollingIntervalMinutes * 60 * 1000);
}
}
public getStationById(id: string) {
return this.eufyClient.getStation(id);
}
}
Example #8
Source File: platform.ts From homebridge-tuya-ir with Apache License 2.0 | 4 votes |
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class TuyaIRPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public cachedAccessories: Map<any, any> = new Map();
constructor(
public readonly log: Logger,
public readonly config: PlatformConfig,
public readonly api: API,
) {
this.log.debug('Finished initializing platform:', this.config.name);
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', () => {
log.debug('Executed didFinishLaunching callback');
// run the method to discover / register your devices as accessories
this.discoverDevices();
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices() {
//if (!this.config.devices) return this.log.error("No devices configured. Please configure atleast one device.");
if (!this.config.client_id) return this.log.error("Client ID is not configured. Please check your config.json");
if (!this.config.secret) return this.log.error("Client Secret is not configured. Please check your config.json");
if (!this.config.region) return this.log.error("Region is not configured. Please check your config.json");
//if (!this.config.deviceId) return this.log.error("IR Blaster device ID is not configured. Please check your config.json");
this.log.info('Starting discovery...');
const tuya: TuyaIRDiscovery = new TuyaIRDiscovery(this.log, this.api);
this.discover(tuya, 0, this.config.smartIR.length);
}
discover(tuya, i, total) {
tuya.start(this.api, this.config, i, (devices, index) => {
this.log.debug(JSON.stringify(devices));
//loop over the discovered devices and register each one if it has not already been registered
for (const device of devices) {
if (device) {
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
device.ir_id = this.config.smartIR[index].deviceId;
const Accessory = CLASS_DEF[device.category] || GenericAccessory;
const uuid = this.api.hap.uuid.generate(device.id);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
// existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
if (Accessory) {
new Accessory(this, existingAccessory);
} else {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
this.log.warn(`Removing unsupported accessory '${existingAccessory.displayName}'...`);
}
// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.:
// remove platform accessories when no longer present
// this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
// this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
} else {
if (Accessory) {
// the accessory does not yet exist, so we need to create it
this.log.info('Adding new accessory:', device.name);
// create a new accessory
const accessory = new this.api.platformAccessory(device.name, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
new Accessory(this, accessory);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
} else {
this.log.warn(`Unsupported accessory '${device.name}'...`);
}
}
}
}
i++;
if(i < total) {
this.discover(tuya, i, total);
}
});
}
}
Example #9
Source File: platform.ts From homebridge-samsungtv-control2 with MIT License | 4 votes |
export class SamsungTVHomebridgePlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service
public readonly Characteristic: typeof Characteristic =
this.api.hap.Characteristic
public readonly tvAccessories: Array<PlatformAccessory> = []
private devices: Array<DeviceConfig> = []
constructor(
public readonly log: Logger,
public readonly config: PlatformConfig,
public readonly api: API,
) {
this.log = log
this.config = config
this.api = api
this.Service = api.hap.Service
this.Characteristic = api.hap.Characteristic
this.log.debug(`Got config`, this.config)
// Add devices
api.on(APIEvent.DID_FINISH_LAUNCHING, async () => {
const dir = path.join(api.user.storagePath(), `.${PLUGIN_NAME}`)
this.log.debug(`Using node-persist path:`, dir)
await storage.init({
dir,
logging: (...args) => this.log.debug(`${PLATFORM_NAME} db -`, ...args),
})
let devices = await this.discoverDevices()
devices = await this.applyConfig(devices)
this.devices = await this.checkDevicePairing(devices)
// Register all TV's
for (const device of this.devices) {
// Log all devices so that the user knows how to configure them
this.log.info(
chalk`Found device {blue ${device.name}} (${device.modelName}), usn: {green ${device.usn}}`,
)
this.log.debug(
`${device.name} - (ip: ${device.lastKnownIp}, mac: ${device.mac})`,
)
// Register it
this.registerTV(device.usn)
}
// Regularly discover upnp devices and update ip's, locations for registered devices
setInterval(async () => {
const devices = await this.discoverDevices()
this.devices = await this.applyConfig(devices)
/**
* @todo
* add previously not registered devices
*/
}, 1000 * 60 * 5 /* 5min */)
/**
* @TODO
* Add subscriptions to update getters
*/
})
}
/*
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(): void {
this.log.debug(`Configuring accessory`)
}
private async discoverDevices() {
let existingDevices: Array<DeviceConfig> = await storage.getItem(
DEVICES_KEY,
)
if (!Array.isArray(existingDevices)) {
existingDevices = []
}
const devices: Array<DeviceConfig> = []
const samsungTVs = await detectDevices(
// this.log,
this.config as SamsungPlatformConfig,
)
for (const tv of samsungTVs) {
const {
usn,
friendlyName: name,
modelName,
location: lastKnownLocation,
address: lastKnownIp,
mac,
capabilities,
} = tv
const device: DeviceConfig = {
name,
modelName,
lastKnownLocation,
lastKnownIp,
mac,
usn,
delay: 500,
capabilities,
}
// Check if the tv was in the devices list before
// if so, only replace the relevant parts
// const existingDevice = devices[usn];
const existingDevice = existingDevices.find((d) => d.usn === usn)
if (existingDevice) {
this.log.debug(
`Rediscovered previously seen device "${device.name}" (${device.modelName}), usn: "${device.usn}"`,
)
devices.push({
...existingDevice,
modelName: device.modelName,
lastKnownLocation: device.lastKnownLocation,
lastKnownIp: device.lastKnownIp,
token: device.token,
discovered: true,
})
} else {
this.log.debug(
`Discovered new device "${device.name}" (${device.modelName}), usn: "${device.usn}"`,
)
devices.push({ ...device, discovered: true })
}
}
// Add all existing devices that where not discovered
for (const existingDevice of existingDevices) {
const { usn } = existingDevice
const device = devices.find((d) => d.usn === usn)
if (!device) {
this.log.debug(
`Adding not discovered, previously seen device "${existingDevice.name}" (${existingDevice.modelName}), usn: "${existingDevice.usn}"`,
)
devices.push(existingDevice)
}
}
// Update devices
await storage.updateItem(DEVICES_KEY, devices)
return devices
}
/**
* Invokes pairing for all discovered devices.
*/
private async checkDevicePairing(devices: Array<DeviceConfig>) {
for (const device of devices) {
// Try pairing if the device was actually discovered and not paired already
if (!device.ignore && device.discovered) {
try {
const token = await remote.getPairing(device, this.log)
if (token) {
this.log.debug(
`Found pairing token "${token}" for "${device.name}" (${device.modelName}), usn: "${device.usn}".`,
)
}
} catch (err) {
this.log.warn(
`Did not receive pairing token. Either you did not click "Allow" in time or your TV might not be supported.` +
`You might just want to restart homebridge and retry.`,
)
}
}
}
return devices
}
/**
* Adds the user modifications to each of devices
*/
private async applyConfig(devices: Array<DeviceConfig>) {
// Get additional options from config
const configDevices = (this.config as SamsungPlatformConfig).devices || []
for (const configDevice of configDevices) {
// Search for the device in the persistent devices and overwrite the values
const { usn } = configDevice
const deviceIdx = devices.findIndex((d) => d.usn === usn)
if (deviceIdx === -1) {
this.log.debug(
`Found config for unknown device usn: "${configDevice.usn}"`,
configDevice,
)
continue
}
const device = devices[deviceIdx]
this.log.debug(
`Found config for device "${device.name}" (${device.modelName}), usn: "${device.usn}"`,
)
devices[deviceIdx] = {
...device,
...configDevice,
}
}
return devices
}
private getDevice(usn) {
const device = this.devices.find((d) => d.usn === usn)
return device as DeviceConfig
}
private registerTV(usn: string) {
const device = this.getDevice(usn)
if (!device || device.ignore) {
return
}
// generate a UUID
const uuid = this.api.hap.uuid.generate(device.usn)
// create the accessory
const tvAccessory = new this.api.platformAccessory(device.name, uuid)
tvAccessory.context = device
this.tvAccessories.push(tvAccessory)
// get the name
const tvName = device.name
// set the accessory category
tvAccessory.category = this.api.hap.Categories.TELEVISION
// add the tv service
const tvService = tvAccessory.addService(this.Service.Television)
// set the tv name, manufacturer etc.
tvService.setCharacteristic(this.Characteristic.ConfiguredName, tvName)
const accessoryService =
tvAccessory.getService(this.Service.AccessoryInformation) ||
new this.Service.AccessoryInformation()
accessoryService
.setCharacteristic(this.Characteristic.Model, device.modelName)
.setCharacteristic(
this.Characteristic.Manufacturer,
`Samsung Electronics`,
)
.setCharacteristic(this.Characteristic.Name, device.name)
.setCharacteristic(this.Characteristic.SerialNumber, device.usn)
// set sleep discovery characteristic
tvService.setCharacteristic(
this.Characteristic.SleepDiscoveryMode,
this.Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE,
)
// handle on / off events using the Active characteristic
tvService
.getCharacteristic(this.Characteristic.Active)
.on(`get`, async (callback) => {
this.log.debug(`${tvName} - GET Active`)
// callback(null, false)
try {
const isActive = await remote.getActive(this.getDevice(usn))
// tvService.updateCharacteristic(this.Characteristic.Active, isActive)
callback(null, isActive)
} catch (err) {
this.log.warn(`${tvName} - Could not check active state`)
callback(err)
}
})
.on(`set`, async (newValue, callback) => {
this.log.debug(`${tvName} - SET Active => setNewValue: ${newValue}`)
try {
await remote.setActive(this.getDevice(usn), newValue as boolean)
tvService.updateCharacteristic(
this.Characteristic.Active,
newValue
? this.Characteristic.Active.ACTIVE
: this.Characteristic.Active.INACTIVE,
)
callback(null)
} catch (err) {
this.log.warn(`${tvName} - Could not update active state`)
callback(err)
}
})
// Update the active state every 15 seconds
setInterval(async () => {
let newState = this.Characteristic.Active.ACTIVE
try {
const isActive = await remote.getActive(this.getDevice(usn))
if (!isActive) {
newState = this.Characteristic.Active.INACTIVE
}
} catch (err) {
newState = this.Characteristic.Active.INACTIVE
}
// this.log.debug('Polled tv active state', newState);
tvService.updateCharacteristic(this.Characteristic.Active, newState)
}, 1000 * 15)
const canGetBrightness = hasCapability(device, `GetBrightness`)
const canSetBrightness = hasCapability(device, `SetBrightness`)
if (canGetBrightness) {
tvService
.getCharacteristic(this.Characteristic.Brightness)
.on(`get`, async (callback) => {
this.log.debug(`${tvName} - GET Brightness`)
try {
const brightness = await remote.getBrightness(this.getDevice(usn))
callback(null, brightness)
} catch (err) {
callback(err)
}
})
}
if (canSetBrightness) {
tvService
.getCharacteristic(this.Characteristic.Brightness)
.on(`set`, async (newValue, callback) => {
this.log.debug(
`${tvName} - SET Brightness => setNewValue: ${newValue}`,
)
try {
await remote.setBrightness(this.getDevice(usn), newValue as number)
tvService.updateCharacteristic(
this.Characteristic.Brightness,
newValue,
)
callback(null)
} catch (err) {
callback(err)
}
})
}
// handle remote control input
tvService
.getCharacteristic(this.Characteristic.RemoteKey)
.on(`set`, async (newValue, callback) => {
try {
switch (newValue) {
case this.Characteristic.RemoteKey.REWIND: {
this.log.debug(`${tvName} - SET Remote Key Pressed: REWIND`)
await remote.rewind(this.getDevice(usn))
break
}
case this.Characteristic.RemoteKey.FAST_FORWARD: {
this.log.debug(`${tvName} - SET Remote Key Pressed: FAST_FORWARD`)
await remote.fastForward(this.getDevice(usn))
break
}
case this.Characteristic.RemoteKey.NEXT_TRACK: {
this.log.debug(`${tvName} - SET Remote Key Pressed: NEXT_TRACK`)
break
}
case this.Characteristic.RemoteKey.PREVIOUS_TRACK: {
this.log.debug(
`${tvName} - SET Remote Key Pressed: PREVIOUS_TRACK`,
)
break
}
case this.Characteristic.RemoteKey.ARROW_UP: {
this.log.debug(`${tvName} - SET Remote Key Pressed: ARROW_UP`)
await remote.arrowUp(this.getDevice(usn))
break
}
case this.Characteristic.RemoteKey.ARROW_DOWN: {
this.log.debug(`${tvName} - SET Remote Key Pressed: ARROW_DOWN`)
await remote.arrowDown(this.getDevice(usn))
break
}
case this.Characteristic.RemoteKey.ARROW_LEFT: {
this.log.debug(`${tvName} - SET Remote Key Pressed: ARROW_LEFT`)
await remote.arrowLeft(this.getDevice(usn))
break
}
case this.Characteristic.RemoteKey.ARROW_RIGHT: {
this.log.debug(`${tvName} - SET Remote Key Pressed: ARROW_RIGHT`)
await remote.arrowRight(this.getDevice(usn))
break
}
case this.Characteristic.RemoteKey.SELECT: {
this.log.debug(`${tvName} - SET Remote Key Pressed: SELECT`)
await remote.select(this.getDevice(usn))
break
}
case this.Characteristic.RemoteKey.BACK: {
this.log.debug(`${tvName} - SET Remote Key Pressed: BACK`)
await remote.back(this.getDevice(usn))
break
}
case this.Characteristic.RemoteKey.EXIT: {
this.log.debug(`${tvName} - SET Remote Key Pressed: EXIT`)
await remote.exit(this.getDevice(usn))
break
}
case this.Characteristic.RemoteKey.PLAY_PAUSE: {
this.log.debug(`${tvName} - SET Remote Key Pressed: PLAY_PAUSE`)
break
}
case this.Characteristic.RemoteKey.INFORMATION: {
this.log.debug(`${tvName} - SET Remote Key Pressed: INFORMATION`)
await remote.info(this.getDevice(usn))
break
}
}
} catch (err) {
callback(err)
return
}
callback(null)
})
/**
* Create a speaker service to allow volume control
*/
const speakerService = tvAccessory.addService(
this.Service.TelevisionSpeaker,
)
/**
* We have these scenarios
* 1. GetVolume + SetVolume:
* => VolumeControlType.Absolute
* => Add Volume Characteristic with get/set
* => Also add VolumeSelector
* (2.) GetVolume but (no SetVolume)
* ...same as 1. because SetVolume can be simulated
* by inc./decr. volume step by step
* => ~~VolumeControlType.RELATIVE_WITH_CURRENT~~
* => ~~Add Volume Characteristic with getter only~~
* 3. No GetVolume upnp capabilities:
* => VolumeControlType.RELATIVE
* => Add VolumeSelector Characteristic
*/
let volumeControlType = this.Characteristic.VolumeControlType.ABSOLUTE
const canGetVolume = hasCapability(device, `GetVolume`)
if (!canGetVolume) {
volumeControlType = this.Characteristic.VolumeControlType.RELATIVE
this.log.debug(`${tvName} - VolumeControlType RELATIVE`)
} else {
this.log.debug(`${tvName} - VolumeControlType ABSOLUTE`)
}
speakerService
.setCharacteristic(
this.Characteristic.Active,
this.Characteristic.Active.ACTIVE,
)
.setCharacteristic(
this.Characteristic.VolumeControlType,
volumeControlType,
)
if (canGetVolume) {
speakerService
.getCharacteristic(this.Characteristic.Volume)
.on(`get`, async (callback) => {
this.log.debug(`${tvName} - GET Volume`)
callback(null, 0)
try {
const volume = await remote.getVolume(this.getDevice(usn))
tvService.updateCharacteristic(this.Characteristic.Volume, volume)
// callback(null, volume)
} catch (err) {
// callback(err)
}
})
}
// When we can get the volume, we can always set the volume
// directly or simulate it by multiple volup/downs
if (canGetVolume) {
speakerService
.getCharacteristic(this.Characteristic.Volume)
.on(`set`, async (newValue, callback) => {
this.log.debug(`${tvName} - SET Volume => setNewValue: ${newValue}`)
try {
await remote.setVolume(this.getDevice(usn), newValue as number)
speakerService
.getCharacteristic(this.Characteristic.Mute)
.updateValue(false)
callback(null)
} catch (err) {
callback(err)
}
})
}
// VolumeSelector can be used in all scenarios
speakerService
.getCharacteristic(this.Characteristic.VolumeSelector)
.on(`set`, async (newValue, callback) => {
this.log.debug(
`${tvName} - SET VolumeSelector => setNewValue: ${newValue}`,
)
try {
if (newValue === this.Characteristic.VolumeSelector.INCREMENT) {
await remote.volumeUp(this.getDevice(usn))
} else {
await remote.volumeDown(this.getDevice(usn))
}
const volume = await remote.getVolume(this.getDevice(usn))
speakerService
.getCharacteristic(this.Characteristic.Mute)
.updateValue(false)
speakerService
.getCharacteristic(this.Characteristic.Volume)
.updateValue(volume)
callback(null)
} catch (err) {
callback(err)
}
})
const canGetMute = hasCapability(device, `GetMute`)
speakerService
.getCharacteristic(this.Characteristic.Mute)
.on(`get`, async (callback) => {
this.log.debug(`${tvName} - GET Mute`)
// When mute cannot be fetched always pretend not to be muted
// for now...
if (!canGetMute) {
callback(null, false)
return
}
callback(null, false)
try {
const muted = await remote.getMute(this.getDevice(usn))
tvService.updateCharacteristic(this.Characteristic.Mute, muted)
// callback(null, muted)
} catch (err) {
// callback(err)
}
})
.on(`set`, async (value, callback) => {
this.log.debug(`${tvName} - SET Mute: ${value}`)
try {
await remote.setMute(this.getDevice(usn), value as boolean)
tvService.updateCharacteristic(
this.Characteristic.Mute,
value as boolean,
)
callback(null)
} catch (err) {
callback(err)
}
})
tvService.addLinkedService(speakerService)
const inputSources = [
{ label: `-`, type: this.Characteristic.InputSourceType.OTHER },
{
label: `TV`,
type: this.Characteristic.InputSourceType.TUNER,
fn: remote.openTV,
},
]
const sources = [...inputSources]
const { inputs = [] } = device
for (const cInput of inputs) {
// Opening apps
if (APPS[cInput.keys]) {
sources.push({
label: cInput.name,
type: this.Characteristic.InputSourceType.APPLICATION,
fn: async (config: DeviceConfig) => {
await remote.openApp(config, APPS[cInput.keys])
},
})
continue
}
// Sending keys
const keys = parseKeys(cInput, device, this.log)
const type =
keys.length === 1 && /^KEY_HDMI[0-4]?$/.test(keys[0])
? this.Characteristic.InputSourceType.HDMI
: this.Characteristic.InputSourceType.OTHER
sources.push({
label: cInput.name,
type,
fn: async (config: DeviceConfig) => {
await remote.sendKeys(config, keys as KEYS[])
},
})
}
// Set current input source to 0 = tv
tvService.updateCharacteristic(this.Characteristic.ActiveIdentifier, 0)
// handle input source changes
let resetActiveIdentifierTimer: NodeJS.Timeout
tvService
.getCharacteristic(this.Characteristic.ActiveIdentifier)
.on(`set`, async (newValue, callback) => {
// Clear old timeout if not cleared already
clearTimeout(resetActiveIdentifierTimer)
// the value will be the value you set for the Identifier Characteristic
// on the Input Source service that was selected - see input sources below.
const inputSource = sources[newValue as any]
this.log.debug(
`${tvName} - SET Active Identifier => setNewValue: ${newValue} (${inputSource.label})`,
)
try {
if (typeof inputSource.fn === `function`) {
await inputSource.fn(this.getDevice(usn))
}
tvService.updateCharacteristic(
this.Characteristic.ActiveIdentifier,
newValue,
)
} catch (err) {
callback(err)
return
}
// Switch back to "TV" input source after 3 seconds
resetActiveIdentifierTimer = setTimeout(() => {
tvService.updateCharacteristic(
this.Characteristic.ActiveIdentifier,
0,
)
}, 3000)
callback(null)
})
for (let i = 0; i < sources.length; ++i) {
const { label, type } = sources[i]
const inputService = tvAccessory.addService(
this.Service.InputSource,
/* `input-${i}` */ label,
label,
)
inputService
.setCharacteristic(this.Characteristic.Identifier, i)
.setCharacteristic(this.Characteristic.ConfiguredName, label)
.setCharacteristic(
this.Characteristic.IsConfigured,
this.Characteristic.IsConfigured.CONFIGURED,
)
.setCharacteristic(
this.Characteristic.CurrentVisibilityState,
this.Characteristic.CurrentVisibilityState.SHOWN,
)
.setCharacteristic(this.Characteristic.InputSourceType, type)
tvService.addLinkedService(inputService)
}
/**
* Publish as external accessory
* Only one TV can exist per bridge, to bypass this limitation, you should
* publish your TV as an external accessory.
*/
this.api.publishExternalAccessories(PLUGIN_NAME, [tvAccessory])
}
}
Example #10
Source File: platform.ts From homebridge-zigbee-nt with Apache License 2.0 | 4 votes |
export class ZigbeeNTHomebridgePlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
private readonly accessories: Map<string, PlatformAccessory>;
private readonly homekitAccessories: Map<string, ZigBeeAccessory>;
private permitJoinAccessory: PermitJoinAccessory;
public readonly PlatformAccessory: typeof PlatformAccessory;
private client: ZigBeeClient;
private httpServer: HttpServer;
private touchLinkAccessory: TouchlinkAccessory;
constructor(
public readonly log: Logger,
public readonly config: ZigBeeNTPlatformConfig,
public readonly api: API
) {
const packageJson = JSON.parse(
fs.readFileSync(`${path.resolve(__dirname, '../package.json')}`, 'utf-8')
);
this.accessories = new Map<string, PlatformAccessory>();
this.homekitAccessories = new Map<string, ZigBeeAccessory>();
this.permitJoinAccessory = null;
this.PlatformAccessory = this.api.platformAccessory;
this.log.info(
`Initializing platform: ${this.config.name} - v${packageJson.version} (API v${api.version})`
);
if (config.devices) {
config.devices.forEach(config => {
this.log.info(
`Registering custom configured device ${config.manufacturer} - ${config.models.join(
', '
)}`
);
registerAccessoryFactory(
config.manufacturer,
config.models,
(
platform: ZigbeeNTHomebridgePlatform,
accessory: PlatformAccessory,
client: ZigBeeClient,
device: Device
) => new ConfigurableAccessory(platform, accessory, client, device, config.services)
);
});
}
this.api.on(APIEvent.DID_FINISH_LAUNCHING, () => this.startZigBee());
this.api.on(APIEvent.SHUTDOWN, () => this.stopZigbee());
}
get zigBeeClient(): ZigBeeClient {
return this.client;
}
public async startZigBee(): Promise<void> {
// Create client
this.client = new ZigBeeClient(this.log, this.config.customDeviceSettings);
const panId =
this.config.panId && this.config.panId < 0xffff ? this.config.panId : DEFAULT_PAN_ID;
const database = this.config.database || path.join(this.api.user.storagePath(), './zigBee.db');
await this.client.start({
channel: this.config.channel,
secondaryChannel: this.config.secondaryChannel,
port: this.config.port,
panId,
database,
adapter: this.config.adapter,
});
this.zigBeeClient.on('deviceAnnounce', (message: DeviceAnnouncePayload) =>
this.handleDeviceAnnounce(message)
);
this.zigBeeClient.on('deviceInterview', (message: DeviceInterviewPayload) =>
this.handleZigBeeDevInterview(message)
);
this.zigBeeClient.on('deviceJoined', (message: DeviceJoinedPayload) =>
this.handleZigBeeDevJoined(message)
);
this.zigBeeClient.on('deviceLeave', (message: DeviceLeavePayload) =>
this.handleZigBeeDevLeaving(message)
);
this.zigBeeClient.on('message', (message: MessagePayload) => this.handleZigBeeMessage(message));
await this.handleZigBeeReady();
}
async stopZigbee(): Promise<void> {
try {
this.log.info('Stopping zigbee service');
await this.zigBeeClient?.stop();
this.log.info('Stopping http server');
await this.httpServer?.stop();
this.log.info('Successfully stopped ZigBee service');
} catch (e) {
this.log.error('Error while stopping ZigBee service', e);
}
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory): void {
this.log.info('Loading accessory from cache:', accessory.displayName);
this.accessories.set(accessory.UUID, accessory);
}
async handleZigBeeDevInterview(message: DeviceInterviewPayload): Promise<void> {
const ieeeAddr = message.device.ieeeAddr;
const status = message.status;
switch (status) {
case 'failed':
this.log.error(
`Interview progress ${status} for device ${this.getDeviceFriendlyName(ieeeAddr)}`
);
break;
case 'started':
this.log.info(
`Interview progress ${status} for device ${this.getDeviceFriendlyName(ieeeAddr)}`
);
break;
case 'successful':
this.log.info(
`Successfully interviewed device: ${message.device.manufacturerName} - ${message.device.modelID}`
);
await this.handleDeviceUpdate(message.device);
}
}
async handleZigBeeDevJoined(message: DeviceJoinedPayload): Promise<boolean> {
this.log.info(
`Device joined, Adding ${this.getDeviceFriendlyName(message.device.ieeeAddr)} (${
message.device.manufacturerName
} - ${message.device.modelID})`
);
return await this.handleDeviceUpdate(message.device);
}
private async handleDeviceUpdate(device: Device): Promise<boolean> {
// Ignore if the device exists
const accessory = this.getHomekitAccessoryByIeeeAddr(device.ieeeAddr);
if (!accessory) {
// Wait a little bit for a database sync
await sleep(1500);
const uuid = await this.initDevice(device);
return uuid !== null;
} else {
this.log.debug(
`Not initializing device ${this.getDeviceFriendlyName(
device.ieeeAddr
)}: already mapped in Homebridge`
);
accessory.internalUpdate({});
}
return false;
}
generateUUID(ieeeAddr: string): string {
return this.api.hap.uuid.generate(ieeeAddr);
}
async handleZigBeeDevLeaving(message: DeviceLeavePayload): Promise<boolean> {
const ieeeAddr = message.ieeeAddr;
// Stop permit join
await this.permitJoinAccessory.setPermitJoin(false);
this.log.info(`Device announced leaving and will be removed, id: ${ieeeAddr}`);
return await this.unpairDevice(ieeeAddr);
}
async handleZigBeeReady(): Promise<void> {
const info: Device = this.zigBeeClient.getCoordinator();
this.log.info(`ZigBee platform initialized @ ${info.ieeeAddr}`);
// Init permit join accessory
await this.initPermitJoinAccessory();
// Init switch to reset devices through Touchlink feature
this.initTouchLinkAccessory();
// Init devices
const paired = (
await Promise.all(
this.zigBeeClient.getAllPairedDevices().map(device => this.initDevice(device))
)
).filter(uuid => uuid !== null);
paired.push(this.permitJoinAccessory.accessory.UUID);
paired.push(this.touchLinkAccessory.accessory.UUID);
const missing = difference([...this.accessories.keys()], paired);
missing.forEach(uuid => {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
this.accessories.get(uuid),
]);
this.accessories.delete(uuid);
this.homekitAccessories.delete(uuid);
});
if (this.config.disableHttpServer !== true) {
try {
this.httpServer = new HttpServer(this.config.httpPort);
this.httpServer.start(this);
} catch (e) {
this.log.error('WEB UI failed to start.', e);
}
} else {
this.log.info('WEB UI disabled.');
}
}
public getAccessoryByIeeeAddr(ieeeAddr: string): PlatformAccessory {
return this.accessories.get(this.generateUUID(ieeeAddr));
}
public getAccessoryByUUID(uuid: string): PlatformAccessory {
return this.accessories.get(uuid);
}
public getHomekitAccessoryByIeeeAddr(ieeeAddr: string): ZigBeeAccessory {
return this.homekitAccessories.get(this.generateUUID(ieeeAddr));
}
public getHomekitAccessoryByUUID(uuid: string): ZigBeeAccessory {
return this.homekitAccessories.get(uuid);
}
private async initDevice(device: Device): Promise<string> {
const model = parseModelName(device.modelID);
const manufacturer = device.manufacturerName;
const ieeeAddr = device.ieeeAddr;
const deviceName = `${this.getDeviceFriendlyName(
ieeeAddr
)} - ${model} - ${manufacturer}`;
this.log.info(`Initializing ZigBee device: ${deviceName}`);
if (!isAccessorySupported(device)) {
this.log.info(
`Unsupported ZigBee device: ${this.getDeviceFriendlyName(
ieeeAddr
)} - ${model} - ${manufacturer}`
);
return null;
} else {
try {
const accessory = this.createHapAccessory(ieeeAddr);
const homeKitAccessory = createAccessoryInstance(this, accessory, this.client, device);
if (homeKitAccessory) {
this.log.info('Registered device:', homeKitAccessory.friendlyName, manufacturer, model);
await homeKitAccessory.initialize(); // init services
this.homekitAccessories.set(accessory.UUID, homeKitAccessory);
return accessory.UUID;
}
} catch (e) {
this.log.error(`Error initializing device ${deviceName}`, e);
}
return null;
}
}
private async initPermitJoinAccessory() {
try {
const accessory = this.createHapAccessory(PERMIT_JOIN_ACCESSORY_NAME);
this.permitJoinAccessory = new PermitJoinAccessory(this, accessory, this.zigBeeClient);
this.log.info('PermitJoin accessory successfully registered');
if (this.config.enablePermitJoin === true) {
await this.permitJoinAccessory.setPermitJoin(true);
}
} catch (e) {
this.log.error('PermitJoin accessory not registered: ', e);
}
}
private initTouchLinkAccessory() {
try {
const accessory = this.createHapAccessory(TOUCH_LINK_ACCESSORY_NAME);
this.touchLinkAccessory = new TouchlinkAccessory(this, accessory, this.zigBeeClient);
this.log.info('TouchLink accessory successfully registered');
} catch (e) {
this.log.error('TouchLink accessory not registered: ', e);
}
}
private createHapAccessory(name: string) {
const uuid = this.generateUUID(name);
const existingAccessory = this.getAccessoryByUUID(uuid);
if (existingAccessory) {
this.log.info(`Reuse accessory from cache with uuid ${uuid} and name ${name}`);
return existingAccessory;
} else {
const accessory = new this.PlatformAccessory(name, uuid);
this.log.warn(`Registering new accessory with uuid ${uuid} and name ${name}`);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.accessories.set(uuid, accessory);
return accessory;
}
}
private removeAccessory(ieeeAddr: string) {
const uuid = this.generateUUID(ieeeAddr);
const accessory = this.accessories.get(uuid);
if (accessory) {
this.accessories.delete(uuid);
this.homekitAccessories.delete(uuid);
}
}
public async unpairDevice(ieeeAddr: string): Promise<boolean> {
const result = await this.zigBeeClient.unpairDevice(ieeeAddr);
if (result) {
this.log.info('Device has been unpaired:', ieeeAddr);
const accessory = this.getAccessoryByIeeeAddr(ieeeAddr);
if (accessory) {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.removeAccessory(ieeeAddr);
return true;
}
} else {
this.log.error('Device has NOT been unpaired:', ieeeAddr);
}
return false;
}
private async handleDeviceAnnounce(message: DeviceAnnouncePayload): Promise<void> {
const ieeeAddr = message.device.ieeeAddr;
this.log.info(
`Device announce: ${this.getDeviceFriendlyName(ieeeAddr)} (${
message.device.manufacturerName
} - ${message.device.modelID})`
);
if (message.device.interviewCompleted) {
let uuid = this.getAccessoryByIeeeAddr(ieeeAddr)?.UUID;
if (!uuid) {
// Wait a little bit for a database sync
await sleep(1500);
uuid = await this.initDevice(message.device);
if (!uuid) {
this.log.warn(`Device not recognized: `, message);
return;
}
}
return this.getHomekitAccessoryByUUID(uuid).onDeviceMount();
} else {
this.log.warn(
`Not initializing device ${this.getDeviceFriendlyName(
ieeeAddr
)}: interview process still not completed`
);
}
}
private handleZigBeeMessage(message: MessagePayload) {
this.log.debug(
`Zigbee message from ${this.getDeviceFriendlyName(message.device.ieeeAddr)}`,
message
);
const zigBeeAccessory = this.getHomekitAccessoryByIeeeAddr(message.device.ieeeAddr);
if (zigBeeAccessory) {
this.client.decodeMessage(message, (ieeeAddr: string, state: DeviceState) => {
this.log.debug(`Decoded state from incoming message`, state);
zigBeeAccessory.internalUpdate(state);
}); // if the message is decoded, it will call the statePublisher function
}
}
public getDeviceFriendlyName(ieeeAddr: string): string {
return (
this.config.customDeviceSettings?.find(config => config.ieeeAddr === ieeeAddr)
?.friendlyName || ieeeAddr
);
}
public isDeviceOnline(ieeeAddr: string): boolean {
const zigBeeAccessory: ZigBeeAccessory = this.getHomekitAccessoryByIeeeAddr(ieeeAddr);
if (zigBeeAccessory) {
return zigBeeAccessory.isOnline;
}
return false;
}
}
Example #11
Source File: platform.ts From homebridge-esphome-ts with GNU General Public License v3.0 | 4 votes |
// milliseconds
export class EsphomePlatform implements DynamicPlatformPlugin {
protected readonly espDevices: EspDevice[] = [];
protected readonly blacklistSet: Set<string>;
protected readonly subscription: Subscription;
protected readonly accessories: PlatformAccessory[] = [];
constructor(
protected readonly log: Logging,
protected readonly config: IEsphomePlatformConfig,
protected readonly api: API,
) {
this.subscription = new Subscription();
this.log('starting esphome');
if (!Array.isArray(this.config.devices) && !this.config.discover) {
this.log.error(
'You did not specify a devices array and discovery is ' +
'disabled! Esphome will not provide any accessories',
);
this.config.devices = [];
}
this.blacklistSet = new Set<string>(this.config.blacklist ?? []);
this.api.on('didFinishLaunching', () => {
this.onHomebridgeDidFinishLaunching();
});
this.api.on('shutdown', () => {
this.espDevices.forEach((device: EspDevice) => device.terminate());
this.subscription.unsubscribe();
});
}
protected onHomebridgeDidFinishLaunching(): void {
let devices: Observable<IEsphomeDeviceConfig> = from(this.config.devices ?? []);
if (this.config.discover) {
const excludeConfigDevices: Set<string> = new Set();
devices = concat(
discoverDevices(this.config.discoveryTimeout ?? DEFAULT_DISCOVERY_TIMEOUT, this.log).pipe(
map((discoveredDevice) => {
const configDevice = this.config.devices?.find(({ host }) => host === discoveredDevice.host);
let deviceConfig = discoveredDevice;
if (configDevice) {
excludeConfigDevices.add(configDevice.host);
deviceConfig = { ...discoveredDevice, ...configDevice };
}
return {
...deviceConfig,
// Override hostname with ip address when available
// to avoid issues with mDNS resolution at OS level
host: discoveredDevice.address ?? discoveredDevice.host,
};
}),
),
// Feed into output remaining devices from config that haven't been discovered
devices.pipe(filter(({ host }) => !excludeConfigDevices.has(host))),
);
}
this.subscription.add(
devices
.pipe(
mergeMap((deviceConfig) => {
const device = new EspDevice(deviceConfig.host, deviceConfig.password, deviceConfig.port);
if (this.config.debug) {
this.log('Writing the raw data from your ESP Device to /tmp');
writeReadDataToLogFile(deviceConfig.host, device);
}
device.provideRetryObservable(
interval(deviceConfig.retryAfter ?? this.config.retryAfter ?? DEFAULT_RETRY_AFTER).pipe(
tap(() => this.log.info(`Trying to reconnect now to device ${deviceConfig.host}`)),
),
);
return device.discovery$.pipe(
filter((value: boolean) => value),
take(1),
timeout(10 * 1000),
tap(() => this.addAccessories(device)),
catchError((err) => {
if (err.name === 'TimeoutError') {
this.log.warn(
`The device under the host ${deviceConfig.host} could not be reached.`,
);
}
return of(err);
}),
);
}),
)
.subscribe(),
);
}
private addAccessories(device: EspDevice): void {
for (const key of Object.keys(device.components)) {
const component = device.components[key];
if (this.blacklistSet.has(component.name)) {
this.logIfDebug(`not processing ${component.name} because it was blacklisted`);
continue;
}
const componentHelper = componentHelpers.get(component.type);
if (!componentHelper) {
this.log(`${component.name} is currently not supported. You might want to file an issue on Github.`);
continue;
}
const uuid = UUIDGen.generate(component.name);
let newAccessory = false;
let accessory: PlatformAccessory | undefined = this.accessories.find(
(accessory) => accessory.UUID === uuid,
);
if (!accessory) {
this.logIfDebug(`${component.name} must be a new accessory`);
accessory = new Accessory(component.name, uuid);
newAccessory = true;
}
if (!componentHelper(component, accessory)) {
this.log(`${component.name} could not be mapped to HomeKit. Please file an issue on Github.`);
if (!newAccessory) {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
continue;
}
this.log(`${component.name} discovered and setup.`);
if (accessory && newAccessory) {
this.accessories.push(accessory);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
}
this.logIfDebug(device.components);
}
public configureAccessory(accessory: PlatformAccessory): void {
if (!this.blacklistSet.has(accessory.displayName)) {
this.accessories.push(accessory);
this.logIfDebug(`cached accessory ${accessory.displayName} was added`);
} else {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.logIfDebug(`unregistered ${accessory.displayName} because it was blacklisted`);
}
}
private logIfDebug(msg?: any, ...parameters: unknown[]): void {
if (this.config.debug) {
this.log(msg, parameters);
} else {
this.log.debug(msg, parameters);
}
}
}
Example #12
Source File: platform.ts From homebridge-konnected with MIT License | 4 votes |
/**
* HomebridgePlatform Class
*
* This class is the main constructor of the Konnected Homebridge plugin.
*
* The following operations are performed when the plugin is loaded:
* - parse the user config
* - retrieve existing accessories from cachedAccessories
* - set up a listening server to listen for requests from the Konnected alarm panels
* - set up
* - discovery of Konnected alarm panels on the network
* - add Konnected alarm panels to Homebridge config
* - provision Konnected alarm panels with zones configured if assigned
* - CRUD accessories with characteristics in Homebridge/HomeKit if zones configured/assigned
* - listen for zone changes and update states in runtime cache and Homebridge/Homekit
* = react to state change requests from Homebridge/HomeKit and send actuator payload to panel
*/
export class KonnectedHomebridgePlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
public readonly Accessory: typeof PlatformAccessory = this.api.platformAccessory;
// global array of references to restored Homebridge/HomeKit accessories from the cache
// (used in accessory cache disk reads - this is also updated when accessories are initialized)
public readonly accessories: PlatformAccessory[] = [];
// global object of references to initialized Homebridge/Homekit accessories
// (used in accessory cache disk writes - don't update this often)
public readonly konnectedPlatformAccessories = {};
// Sensor and actuator accessories can change often, we store a non-blocking state of them in a runtime cache.
// This avoids experiencing two performance problems:
// 1. a 'No Response' flag on accessory tiles in HomeKit when waiting for responses from the Konnected panels for states;
// 2. constantly/expensively reading and writing to Homebridge's accessories cache.
// NOTE: we do not store the security system accessory here, its state is maintained in the Homebridge accessories explicitly.
public accessoriesRuntimeCache: RuntimeCacheInterface[] = [];
// security system UUID (we only allow one security system per homebridge instance)
private securitySystemUUID: string = this.api.hap.uuid.generate(this.config.platform);
// define entry delay defaults
private entryTriggerDelay: number =
this.config.advanced?.entryDelaySettings?.delay !== null &&
typeof this.config.advanced?.entryDelaySettings?.delay !== 'undefined'
? Math.round(this.config.advanced?.entryDelaySettings?.delay) * 1000
: 30000; // zero = instant trigger
private entryTriggerDelayTimerHandle;
// define exit delay defaults
private exitTriggerDelay: number =
this.config.advanced?.exitDelaySettings?.delay !== null &&
typeof this.config.advanced?.exitDelaySettings?.delay !== 'undefined'
? Math.round(this.config.advanced?.exitDelaySettings?.delay) * 1000
: 30000; // zero = instant arming
private exitTriggerDelayTimerHandle1;
private exitTriggerDelayTimerHandle2;
private exitTriggerDelayTimerHandle3;
// define listening server variables
private listenerIP: string = this.config.advanced?.listenerIP ? this.config.advanced.listenerIP : ip.address(); // system defined primary network interface
private listenerPort: number = this.config.advanced?.listenerPort ? this.config.advanced.listenerPort : 0; // zero = autochoose
private ssdpTimeout: number = this.config.advanced?.discoveryTimeout
? this.config.advanced.discoveryTimeout * 1000
: 5000; // 5 seconds
private listenerAuth: string[] = []; // for storing random auth strings
private ssdpDiscovering = false; // for storing state of SSDP discovery process
private ssdpDiscoverAttempts = 0;
constructor(public readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API) {
this.log.debug('Finished initializing platform');
// Homebridge looks for and fires this event when it has retrieved all cached accessories from disk
// this event is also used to init other methods for this plugin
this.api.on('didFinishLaunching', () => {
log.debug('Executed didFinishLaunching callback. Accessories retreived from cache...');
// run the listening server & register the security system
this.listeningServer();
this.registerSecuritySystem();
this.discoverPanels();
});
}
/**
* Homebridge's startup restoration of cached accessories from disk.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.info(`Loading accessory from cache: ${accessory.displayName} (${accessory.context.device.serialNumber})`);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* Create a listening server for status and state changes from panels and zones.
* https://help.konnected.io/support/solutions/articles/32000026814-sensor-state-callbacks
*/
listeningServer() {
const app = express();
const server = http.createServer(app);
app.use(express.json());
server.listen(this.listenerPort, () => {
// store port to its global variable
this.listenerPort = server.address()!['port'];
this.log.info(`Listening for zone changes on ${this.listenerIP} port ${this.listenerPort}`);
});
// restart/crash cleanup
const cleanup = () => {
server.close();
this.log.info(`Listening port ${this.listenerPort} closed and released`);
};
process.on('SIGINT', cleanup).on('SIGTERM', cleanup);
const respond = (req, res) => {
// bearer auth token not provided
if (typeof req.headers.authorization === 'undefined') {
this.log.error(`Authentication failed for ${req.params.id}, token missing, with request body:`, req.body);
// send the following response
res.status(401).json({
success: false,
reason: 'Authorization failed, token missing',
});
return;
}
// validate provided bearer auth token
if (this.listenerAuth.includes(req.headers.authorization.split('Bearer ').pop())) {
if (['POST', 'PUT'].includes(req.method)) {
// panel request to SET the state of the switch in Homebridge/HomeKit
// send response with success to the panel
res.status(200).json({ success: true });
this.updateSensorAccessoryState(req);
} else if ('GET' === req.method) {
// panel request to GET the state of the switch in Homebridge/HomeKit
// send response with payload of states to the panel
// create type interface for responsePayload variable
interface ResponsePayload {
success: true;
pin?: string;
zone?: string;
state?: number;
}
// setup response payload to reply with
const responsePayload: ResponsePayload = {
success: true,
};
// default to zone for Pro panel, but may be replaced if V1-V1 panel
let requestPanelZone = req.query.zone;
// pins or zones assignment
if (req.query.pin) {
// V1-V2 panel
// change requestPanelZone variable to the zone equivalent of a pin on V1-V2 panels
Object.entries(ZONES_TO_PINS).find(([zone, pin]) => {
if (pin === Number(req.query.pin)) {
requestPanelZone = zone;
}
});
responsePayload.pin = req.query.pin;
} else if (req.query.zone) {
// Pro panel
responsePayload.zone = requestPanelZone;
}
// check the trigger state of switches based on their last runtime state in Homebridge
this.accessoriesRuntimeCache.find((runtimeCacheAccessory) => {
if (runtimeCacheAccessory.serialNumber === req.params.id + '-' + requestPanelZone) {
if (['beeper', 'siren', 'strobe', 'switch'].includes(runtimeCacheAccessory.type)) {
if (runtimeCacheAccessory.trigger === 'low' && runtimeCacheAccessory.state === 0) {
responsePayload.state = 1; // set to normally high (1), waiting to be triggered low (0)
} else if (
runtimeCacheAccessory.trigger === 'low' &&
(runtimeCacheAccessory.state === 1 || runtimeCacheAccessory.state === undefined)
) {
responsePayload.state = 0; // set to triggered low (0), waiting to be normally high (1)
} else if (
(runtimeCacheAccessory.trigger === 'high' || runtimeCacheAccessory.trigger === undefined) &&
(runtimeCacheAccessory.state === 0 || runtimeCacheAccessory.state === undefined)
) {
responsePayload.state = 0; // set to normally low (0), waiting to be triggered high (1)
} else if (
(runtimeCacheAccessory.trigger === 'high' || runtimeCacheAccessory.trigger === undefined) &&
runtimeCacheAccessory.state === 1
) {
responsePayload.state = 1; // set to triggered high (1), waiting to be normally low (0)
}
} else {
responsePayload.state =
typeof runtimeCacheAccessory.state !== 'undefined' ? Number(runtimeCacheAccessory.state) : 0;
}
}
});
this.log.debug(
`Panel (${req.params.id}) requested zone '${requestPanelZone}' initial state, sending value of ${responsePayload.state}`
);
// return response with payload of states
res.status(200).json(responsePayload);
}
} else {
// send the following response
res.status(401).json({
success: false,
reason: 'Authorization failed, token not valid',
});
// rediscover and reprovision panels
if (this.ssdpDiscovering === false) {
this.log.warn(`Received zone payload: ${req.body}`);
this.log.warn(`Authentication failed for ${req.params.id}, token not valid`);
this.log.warn('Authentication token:', req.headers.authorization.split('Bearer ').pop());
this.log.warn('Rediscovering and reprovisioning panels...');
this.discoverPanels();
}
}
};
// listen for requests at the following route/endpoint
app
.route('/api/konnected/device/:id')
.put(respond) // Alarm Panel V1-V2
.post(respond) // Alarm Panel Pro
.get(respond); // For Actuator Requests
}
/**
* Register the Security System
*
* There are two scenarios for the security system:
* 1. the security system logic is handled by the plugin, the installed home security system is just reporting sensor states;
* 2. the security system logic is handled by the installed home security system.
*
* We provide security system logic that allows each sensor (not temperature or humidity) to define what security mode it can trigger the alarm in,
* with the following considerations:
* - armed away: long countdown of beeps from piezo;
* - armed home: short countdown of beeps from piezo;
* - armed night: no countdown beeps from piezo;
* - disarmed: when contact sensors change state, check an option for momentary piezo beeps for change.
*/
registerSecuritySystem() {
const securitySystemObject = {
UUID: this.securitySystemUUID,
displayName: 'Konnected Alarm',
type: 'securitysystem',
model: 'Konnected Security System',
serialNumber: this.api.hap.uuid.toShortForm(this.securitySystemUUID),
state: 0,
};
const existingSecuritySystem = this.accessories.find((accessory) => accessory.UUID === this.securitySystemUUID);
if (existingSecuritySystem) {
// then the accessory already exists
this.log.info(
`Updating existing accessory: ${existingSecuritySystem.displayName} (${existingSecuritySystem.context.device.serialNumber})`
);
// store a direct reference to the initialized accessory with service and characteristics in the KonnectedPlatformAccessories object
this.konnectedPlatformAccessories[this.securitySystemUUID] = new KonnectedPlatformAccessory(
this,
existingSecuritySystem
);
// update security system accessory in Homebridge and HomeKit
this.api.updatePlatformAccessories([existingSecuritySystem]);
} else {
// otherwise we're adding a new accessory
this.log.info(`Adding new accessory: ${securitySystemObject.displayName} (${this.securitySystemUUID})`);
// build Homebridge/HomeKit platform accessory
const newSecuritySystemAccessory = new this.api.platformAccessory('Konnected Alarm', this.securitySystemUUID);
// store security system object in the platform accessory cache
newSecuritySystemAccessory.context.device = securitySystemObject;
// store a direct reference to the initialized accessory with service and characteristics in the KonnectedPlatformAccessories object
this.konnectedPlatformAccessories[this.securitySystemUUID] = new KonnectedPlatformAccessory(
this,
newSecuritySystemAccessory
);
// add security system accessory to Homebridge and HomeKit
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [newSecuritySystemAccessory]);
}
}
/**
* Discover alarm panels on the network.
* @reference https://help.konnected.io/support/solutions/articles/32000026805-discovery
*
* Konnected SSDP Search Targets:
* @reference Alarm Panel V1-V2: urn:schemas-konnected-io:device:Security:1
* @reference Alarm Panel Pro: urn:schemas-konnected-io:device:Security:2
*/
discoverPanels() {
const ssdpClient = new client.Client();
const ssdpUrnPartial = 'urn:schemas-konnected-io:device';
const ssdpDeviceIDs: string[] = []; // used later for deduping SSDP reflections
const excludedUUIDs: string[] = String(process.env.KONNECTED_EXCLUDES).split(','); // used for ignoring specific panels (mostly for development)
// set discovery state
this.ssdpDiscovering = true;
// begin discovery
ssdpClient.search('ssdp:all');
// on discovery
ssdpClient.on('response', (headers) => {
// check for only Konnected devices
if (headers.ST!.indexOf(ssdpUrnPartial) !== -1) {
// store reported URL of panel that responded
const ssdpHeaderLocation: string = headers.LOCATION || '';
// extract UUID of panel from the USN string
const panelUUID: string = headers.USN!.match(/^uuid:(.*)::.*$/i)![1] || '';
// dedupe responses, ignore excluded panels in environment variables, and then provision panel(s)
if (!ssdpDeviceIDs.includes(panelUUID) && !excludedUUIDs.includes(panelUUID)) {
// get panel status object (not using async await)
fetch(ssdpHeaderLocation.replace('Device.xml', 'status'))
// convert response to JSON
.then((fetchResponse) => fetchResponse.json())
.then((panelResponseObject) => {
// create listener object to pass back to panel when provisioning it
const listenerObject = {
ip: this.listenerIP,
port: this.listenerPort,
};
// use the above information to construct panel in Homebridge config
this.updateHomebridgeConfig(panelUUID, panelResponseObject);
// if the settings property does not exist in the response,
// then we have an unprovisioned panel
if (Object.keys(panelResponseObject.settings).length === 0) {
this.provisionPanel(panelUUID, panelResponseObject, listenerObject);
} else {
if (panelResponseObject.settings.endpoint_type === 'rest') {
const panelBroadcastEndpoint = new URL(panelResponseObject.settings.endpoint);
// if the IP address or port are not the same, reprovision endpoint component
if (
panelBroadcastEndpoint.host !== this.listenerIP ||
Number(panelBroadcastEndpoint.port) !== this.listenerPort
) {
this.provisionPanel(panelUUID, panelResponseObject, listenerObject);
}
} else if (panelResponseObject.settings.endpoint_type === 'aws_iot') {
this.log.error(
`ERROR: Cannot provision panel ${panelUUID} with Homebridge. Panel has previously been provisioned with another platform (Konnected Cloud, SmartThings, Home Assistant, Hubitat,. etc). Please factory reset your Konnected Alarm panel and disable any other platform connectors before associating the panel with Homebridge.`
);
}
}
});
// add the UUID to the deduping array
ssdpDeviceIDs.push(panelUUID);
}
}
});
// stop discovery after a number of seconds seconds, default is 5
setTimeout(() => {
ssdpClient.stop();
this.ssdpDiscovering = false;
if (ssdpDeviceIDs.length) {
this.log.debug('Discovery complete. Found panels:\n' + JSON.stringify(ssdpDeviceIDs, null, 2));
} else if (this.ssdpDiscoverAttempts < 5) {
this.ssdpDiscoverAttempts++;
this.log.debug(
`Discovery attempt ${this.ssdpDiscoverAttempts} could not find any panels on the network. Retrying...`
);
this.discoverPanels();
} else {
this.ssdpDiscoverAttempts = 0;
this.log.debug(
'Could not discover any panels on the network. Please check that your panel(s) are on the same network and that you have UPnP enabled. Visit https://help.konnected.io/support/solutions/articles/32000023644-device-discovery-troubleshooting for more information.'
);
}
}, this.ssdpTimeout);
}
/**
* Update Homebridge config.json with discovered panel information.
*
* @param panelUUID string UUID for the panel as reported in the USN on discovery.
* @param panelObject PanelObjectInterface The status response object of the plugin from discovery.
*/
updateHomebridgeConfig(panelUUID: string, panelObject: PanelObjectInterface) {
// homebridge constants
const config = this.api.user.configPath();
const storage = this.api.user.storagePath();
// get and clone config
const existingConfig = JSON.parse(fs.readFileSync(config).toString());
const modifiedConfig = JSON.parse(JSON.stringify(existingConfig));
// check backups/config-backups directory exists, if not use base storage directory
const backup = fs.existsSync(`${storage}/backups/config-backups/`)
? `${storage}/backups/config-backups/config.json.${new Date().getTime()}`
: `${storage}/config.json.${new Date().getTime()}`;
// get index of my platform
const platform = modifiedConfig.platforms.findIndex((config: { [key: string]: unknown }) => config.platform === 'konnected');
// if 'konnected' platform exists in the config
if (platform >= 0) {
// get the panels array or start with an empty array
modifiedConfig.platforms[platform].panels = modifiedConfig.platforms[platform].panels || [];
// find existing definition of the panel
const platformPanelPosition = modifiedConfig.platforms[platform].panels.findIndex((panel: { [key: string]: unknown }) => panel.uuid === panelUUID);
if (platformPanelPosition < 0) {
// if panel doesn't exist, push to panels array and write backup and config
modifiedConfig.platforms[platform].panels.push({
name: (panelObject.model && panelObject.model !== '' ? panelObject.model : 'Konnected V1-V2').replace(
/[^A-Za-z0-9\s/'":\-#.]/gi,
''
),
uuid: panelUUID,
ipAddress: panelObject.ip,
port: panelObject.port,
});
fs.writeFileSync(backup, JSON.stringify(existingConfig, null, 4));
fs.writeFileSync(config, JSON.stringify(modifiedConfig, null, 4));
} else if (
modifiedConfig.platforms[platform].panels[platformPanelPosition].uuid === panelUUID &&
(modifiedConfig.platforms[platform].panels[platformPanelPosition].ipAddress !== panelObject.ip ||
modifiedConfig.platforms[platform].panels[platformPanelPosition].port !== panelObject.port)
) {
// if the IP address or port is the same don't update the config
modifiedConfig.platforms[platform].panels[platformPanelPosition].name = (
panelObject.model && panelObject.model !== '' ? panelObject.model : 'Konnected V1-V2'
).replace(/[^A-Za-z0-9\s/'":\-#.]/gi, '');
modifiedConfig.platforms[platform].panels[platformPanelPosition].uuid = panelUUID;
modifiedConfig.platforms[platform].panels[platformPanelPosition].ipAddress = panelObject.ip;
modifiedConfig.platforms[platform].panels[platformPanelPosition].port = panelObject.port;
fs.writeFileSync(backup, JSON.stringify(existingConfig, null, 4));
fs.writeFileSync(config, JSON.stringify(modifiedConfig, null, 4));
}
}
}
/**
* Provision a Konnected panel with information to communicate with this plugin
* and to register the zones on the panel according to their configured settings in this plugin.
* @reference https://help.konnected.io/support/solutions/articles/32000026807-device-provisioning
*
* @param panelUUID string UUID for the panel as reported in the USN on discovery.
* @param panelObject PanelObjectInterface The status response object of the plugin from discovery.
* @param listenerObject object Details object for this plugin's listening server.
*/
provisionPanel(panelUUID: string, panelObject: PanelObjectInterface, listenerObject) {
let panelIP: string = panelObject.ip;
let panelPort: number = panelObject.port;
let panelBlink = true;
let panelName;
// if there are panels in the plugin config
if (typeof this.config.panels !== 'undefined') {
// loop through the available panels
for (const configPanel of this.config.panels) {
// isolate specific panel and make sure there are zones in that panel
if (configPanel.uuid === panelUUID) {
panelIP = configPanel.ipAddress ? configPanel.ipAddress : panelObject.ip;
panelPort = configPanel.port ? configPanel.port : panelObject.port;
panelBlink = typeof configPanel.blink !== 'undefined' ? configPanel.blink : true;
panelName = configPanel.name ? configPanel.name : '';
}
}
}
const listeningEndpoint = `http://${listenerObject.ip}:${listenerObject.port}/api/konnected`;
const panelSettingsEndpoint = `http://${panelIP}:${panelPort}/settings`;
const bearerAuthToken = uuidv4(); // generate an RFC4122 compliant UUID
this.listenerAuth.push(bearerAuthToken); // add to array for listening authorization
const panelPayloadCore = {
endpoint_type: 'rest',
endpoint: listeningEndpoint,
token: bearerAuthToken,
blink: panelBlink,
discovery: true,
platform: 'Homebridge',
};
const panelPayloadAccessories = this.configureZones(panelUUID, panelObject);
const panelConfigurationPayload = {
...panelPayloadCore,
...panelPayloadAccessories,
};
this.log.debug(`Panel ${panelName} ${panelSettingsEndpoint} rebooting with payload changes:\n` + JSON.stringify(panelConfigurationPayload, null, 2));
const provisionPanelResponse = async (url: string) => {
try {
await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(panelConfigurationPayload),
});
} catch (error: unknown) {
if (error instanceof Error) {
if (error['errno'] === 'ECONNRESET') {
this.log.info(
`The panel at ${url} has disconnected and is likely rebooting to apply new provisioning settings`
);
} else {
this.log.error(error['message']);
}
}
}
};
provisionPanelResponse(panelSettingsEndpoint);
}
/**
* Build the payload of zones for provisioning on a panel.
* Store the configuration of zones in the accessoriesRuntimeCache.
*
* @param panelUUID string The unique identifier for the panel itself.
* @param panelObject PanelObjectInterface The status response object of the plugin from discovery.
*/
configureZones(panelUUID: string, panelObject: PanelObjectInterface) {
const sensors: unknown[] = [];
const dht_sensors: unknown[] = [];
const ds18b20_sensors: unknown[] = [];
const actuators: unknown[] = [];
const retainedAccessories: unknown[] = [];
// if there are panels in the plugin config
if (typeof this.config.panels !== 'undefined') {
// loop through the available panels
this.config.panels.forEach((configPanel) => {
// If there's a chipId in the panelObject, use that, or use mac address.
// V1/V2 panels only have one interface (WiFi). Panels with chipId are Pro versions
// with two network interfaces (WiFi & Ethernet) with separate mac addresses.
// If one network interface goes down, the panel can fallback to the other
// interface and the accessories lose their associated UUID, which can
// result in duplicated accessories, half of which become non-responsive.
const panelShortUUID: string =
'chipId' in panelObject ? panelUUID.match(/([^-]+)$/i)![1] : panelObject.mac.replace(/:/g, '');
// isolate specific panel and make sure there are zones in that panel
if (configPanel.uuid === panelUUID && configPanel.zones) {
// variable for deduping zones with the same zoneNumber (use-case: if users don't use Config UI X to generate their config)
const existingPayloadZones: string[] = [];
configPanel.zones.forEach((configPanelZone) => {
// create type interface for panelZone variable
interface PanelZone {
pin?: string;
zone?: string;
trigger?: number;
poll_interval?: number;
}
let panelZone: PanelZone = {};
// assign the pin or zone
if ('model' in panelObject) {
// this is a Pro panel
// check if zone is improperly assigned as the V1-V2 panel 'out' zone
if (configPanelZone.zoneNumber === 'out') {
this.log.warn(
`Invalid Zone: Konnected Pro Alarm Panels do not have a zone named ${configPanelZone.zoneNumber}, change the zone assignment to 'alarm1', 'out1', or 'alarm2_out2'.`
);
} else if (ZONE_TYPES.actuators.includes(configPanelZone.zoneType)) {
// this zone is assigned as an actuator
// validate if zone can be an actuator/switch
if (ZONES[configPanelZone.zoneNumber].includes(configPanelZone.zoneType)) {
panelZone.zone = configPanelZone.zoneNumber;
} else {
this.log.warn(
`Invalid Zone: Konnected Pro Alarm Panels cannot have zone ${configPanelZone.zoneNumber} as an actuator/switch. Try zones 1-8, 'alarm1', 'out1', or 'alarm2_out2'.`
);
}
} else {
panelZone.zone = configPanelZone.zoneNumber;
}
} else {
// this is a V1-V2 panel
// convert zone to a pin
if (ZONES_TO_PINS[configPanelZone.zoneNumber]) {
// check if this zone is assigned as an actuator
if (ZONE_TYPES.actuators.includes(configPanelZone.zoneType)) {
// validate if zone can be an actuator/switch
if (configPanelZone.zoneNumber < 6 || configPanelZone.zoneNumber === 'out') {
panelZone.pin = ZONES_TO_PINS[configPanelZone.zoneNumber];
} else {
this.log.warn(
`Invalid Zone: Konnected V1-V2 Alarm Panels cannot have zone ${configPanelZone.zoneNumber} as an actuator/switch. Try zones 1-5 or 'out'.`
);
}
} else {
panelZone = {
pin: ZONES_TO_PINS[configPanelZone.zoneNumber],
};
}
} else {
this.log.warn(
`Invalid Zone: Konnected V1-V2 Alarm Panels do not have a zone '${configPanelZone.zoneNumber}'. Try zones 1-6 or 'out'.`
);
}
}
// assign the startup trigger value
if (configPanelZone.switchSettings?.trigger) {
panelZone.trigger = configPanelZone.switchSettings?.trigger === 'low' ? 0 : 1;
}
// assign the temperature/humidity poll interval
if (configPanelZone.environmentalSensorSettings?.pollInterval) {
panelZone.poll_interval = configPanelZone.environmentalSensorSettings.pollInterval;
}
// check if the panel object is not empty (this will cause a boot loop if it's empty)
if (Object.keys(panelZone).length > 0 && configPanelZone.enabled === true) {
// put panelZone into the correct device type for the panel
if (ZONE_TYPES.sensors.includes(configPanelZone.zoneType)) {
sensors.push(panelZone);
} else if (ZONE_TYPES.dht_sensors.includes(configPanelZone.zoneType)) {
dht_sensors.push(panelZone);
} else if (ZONE_TYPES.ds18b20_sensors.includes(configPanelZone.zoneType)) {
ds18b20_sensors.push(panelZone);
} else if (ZONE_TYPES.actuators.includes(configPanelZone.zoneType)) {
actuators.push(panelZone);
}
}
// genereate unique ID for zone
const zoneUUID: string = this.api.hap.uuid.generate(panelShortUUID + '-' + configPanelZone.zoneNumber);
// if there's a model in the panelObject, that means the panel is Pro
const panelModel: string = 'model' in panelObject ? 'Pro' : 'V1-V2';
// dedupe zones with the same zoneNumber
if (!existingPayloadZones.includes(zoneUUID)) {
// if not a duplicate, push the zone's UUID into the zoneCheck array
existingPayloadZones.push(zoneUUID);
const zoneLocation = configPanelZone.zoneLocation ? configPanelZone.zoneLocation + ' ' : '';
// standard zone object properties
const zoneObject: RuntimeCacheInterface = {
UUID: zoneUUID,
displayName: zoneLocation + TYPES_TO_ACCESSORIES[configPanelZone.zoneType][1],
enabled: configPanelZone.enabled,
type: configPanelZone.zoneType,
model: panelModel + ' ' + TYPES_TO_ACCESSORIES[configPanelZone.zoneType][1],
serialNumber: panelShortUUID + '-' + configPanelZone.zoneNumber,
panel: panelObject,
};
// add invert property if configured
if (configPanelZone.binarySensorSettings?.invert) {
zoneObject.invert = configPanelZone.binarySensorSettings.invert;
}
// add audibleBeep property if configured
if (configPanelZone.binarySensorSettings?.audibleBeep) {
zoneObject.audibleBeep = configPanelZone.binarySensorSettings.audibleBeep;
}
// add trigger property if configured
if (configPanelZone.switchSettings?.trigger) {
zoneObject.trigger = configPanelZone.switchSettings.trigger;
}
// add triggerableModes property if configured
if (configPanelZone.binarySensorSettings?.triggerableModes) {
zoneObject.triggerableModes = configPanelZone.binarySensorSettings.triggerableModes;
} else if (configPanelZone.switchSettings?.triggerableModes) {
zoneObject.triggerableModes = configPanelZone.switchSettings.triggerableModes;
}
// store previous state from existing Homebridge's platform accessory cache state
this.accessories.forEach((accessory) => {
if (accessory.UUID === zoneUUID) {
// binary state
if (typeof accessory.context.device.state !== 'undefined') {
zoneObject.state = accessory.context.device.state;
}
// humidity state
if (typeof accessory.context.device.humi !== 'undefined') {
zoneObject.humi = accessory.context.device.humi;
}
// temperature state
if (typeof accessory.context.device.temp !== 'undefined') {
zoneObject.humi = accessory.context.device.temp;
}
}
});
if (configPanelZone.enabled === true) {
this.accessoriesRuntimeCache.push(zoneObject);
// match this zone's UUID to the UUID of an accessory stored in the global accessories cache
// store accessory object in an array of retained accessories that we don't want unregistered in Homebridge and HomeKit
if (typeof this.accessories.find((accessory) => accessory.UUID === zoneUUID) !== 'undefined') {
retainedAccessories.push(this.accessories.find((accessory) => accessory.UUID === zoneUUID));
}
}
} else {
this.log.warn(
`Duplicate Zone: Zone number '${configPanelZone.zoneNumber}' is assigned in two or more zones, please check your Homebridge configuration for panel with UUID ${panelUUID}.`
);
}
}); // end forEach loop (zones)
// Now attempt to register the zones as accessories in Homebridge and HomeKit
this.registerAccessories(panelShortUUID, this.accessoriesRuntimeCache, retainedAccessories);
} else if (configPanel.uuid === panelUUID && typeof configPanel.zones === 'undefined') {
this.registerAccessories(panelShortUUID, [], []);
}
}); // end forEach loop (panels)
}
// if there are no zones defined then we use our default blank array variables above this block
const panelZonesPayload = {
sensors: sensors,
dht_sensors: dht_sensors,
ds18b20_sensors: ds18b20_sensors,
actuators: actuators,
};
return panelZonesPayload;
}
/**
* Control the registration of panel zones as accessories in Homebridge (and HomeKit).
*
* @param panelShortUUID string The panel short UUID for the panel of zones being passed in.
* @param zoneObjectsArray array An array of constructed zoneObjects.
* @param retainedAccessoriesArray array An array of retained accessory objects.
*/
registerAccessories(panelShortUUID, zoneObjectsArray, retainedAccessoriesArray) {
// console.log('zoneObjectsArray', zoneObjectsArray);
// console.log('retainedAccessoriesArray:', retainedAccessoriesArray);
// if (Array.isArray(retainedAccessoriesArray) && retainedAccessoriesArray.length > 0) {
// retainedAccessoriesArray.forEach((accessory) => {
// if (typeof accessory !== 'undefined') {
// this.log.debug(`Retained accessory: ${accessory.displayName} (${accessory.context.device.serialNumber})`);
// }
// });
// }
// remove any stale accessories
///////////////////////////////
const accessoriesToRemoveArray = this.accessories
.filter(
// filter in accessories with same panel
(accessory) => accessory.context.device.serialNumber.split('-')[0] === panelShortUUID
)
.filter(
// filter out retained accessories
(accessory) => !retainedAccessoriesArray.includes(accessory)
);
if (Array.isArray(accessoriesToRemoveArray) && accessoriesToRemoveArray.length > 0) {
// unregister stale or missing zones/accessories in Homebridge and HomeKit
accessoriesToRemoveArray.forEach((accessory) => {
this.log.info(`Removing accessory: ${accessory.displayName} (${accessory.context.device.serialNumber})`);
});
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessoriesToRemoveArray);
}
// update or create accessories
///////////////////////////////
const accessoriesToUpdateArray: PlatformAccessory[] = [];
const accessoriesToAddArray: PlatformAccessory[] = [];
zoneObjectsArray.forEach((panelZoneObject) => {
// find Homebridge cached accessories with the same uuid as those in the config
const existingAccessory = this.accessories.find((accessory) => accessory.UUID === panelZoneObject.UUID);
if (existingAccessory && existingAccessory.context.device.UUID === panelZoneObject.UUID) {
// then the accessory already exists
this.log.debug(
`Updating existing accessory: ${existingAccessory.context.device.displayName} (${existingAccessory.context.device.serialNumber})`
);
// update zone object in the platform accessory cache
existingAccessory.displayName = panelZoneObject.displayName;
existingAccessory.context.device = panelZoneObject;
// store a direct reference to the initialized accessory with service and characteristics in the KonnectedPlatformAccessories object
this.konnectedPlatformAccessories[panelZoneObject.UUID] = new KonnectedPlatformAccessory(
this,
existingAccessory
);
accessoriesToUpdateArray.push(existingAccessory);
} else {
// otherwise we're adding a new accessory
this.log.info(`Adding new accessory: ${panelZoneObject.displayName} (${panelZoneObject.serialNumber})`);
// build Homebridge/HomeKit platform accessory
const newAccessory = new this.api.platformAccessory(panelZoneObject.displayName, panelZoneObject.UUID);
// store zone object in the platform accessory cache
newAccessory.context.device = panelZoneObject;
// store a direct reference to the initialized accessory with service and characteristics in the KonnectedPlatformAccessories object
this.konnectedPlatformAccessories[panelZoneObject.UUID] = new KonnectedPlatformAccessory(this, newAccessory);
accessoriesToAddArray.push(newAccessory);
}
});
if (Array.isArray(accessoriesToUpdateArray) && accessoriesToUpdateArray.length > 0) {
// update zones/accessories in Homebridge and HomeKit
this.api.updatePlatformAccessories(accessoriesToUpdateArray);
}
if (Array.isArray(accessoriesToAddArray) && accessoriesToAddArray.length > 0) {
// add zones/accessories to Homebridge and HomeKit
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessoriesToAddArray);
}
}
/**
* Update the cache when a panel reports a change in the sensor zone's state.
* Panels only report state of sensors, so this will only fire for sensors and not actuators.
*
* @param inboundPayload object The request payload received for the zone at this plugin's listener REST endpoint.
*/
updateSensorAccessoryState(inboundPayload) {
let panelZone = '';
let zoneState = '';
if ('pin' in inboundPayload.body) {
// convert a pin to a zone
Object.entries(ZONES_TO_PINS).map(([key, value]) => {
if (value === inboundPayload.body.pin) {
panelZone = key;
zoneState = JSON.stringify(inboundPayload.body) + ` (zone: ${panelZone})`;
}
});
} else {
// use the zone
panelZone = inboundPayload.body.zone;
zoneState = JSON.stringify(inboundPayload.body);
}
const zoneUUID = this.api.hap.uuid.generate(inboundPayload.params.id + '-' + panelZone);
const existingAccessory = this.accessories.find((accessory) => accessory.UUID === zoneUUID);
// check if the accessory already exists
if (existingAccessory) {
this.log.debug(
`Panel sent update for [${existingAccessory.displayName}] (${existingAccessory.context.device.serialNumber}) with value:\n`,
zoneState
);
// loop through the accessories state cache and update state and service characteristic
this.accessoriesRuntimeCache.forEach((runtimeCacheAccessory) => {
if (runtimeCacheAccessory.UUID === zoneUUID) {
// this is the default state for all binary switches in HomeKit
let defaultStateValue = 0;
// incoming state from panel
const inboundStateValue = inboundPayload.body.state;
// set default result state
let resultStateValue = inboundStateValue;
if (!['humidtemp', 'temperature'].includes(runtimeCacheAccessory.type)) {
// invert the value if configured to have its value inverted
if (runtimeCacheAccessory.invert === true) {
defaultStateValue = 1;
resultStateValue = inboundStateValue === 0 ? 1 : 0;
this.log.debug(
`[${runtimeCacheAccessory.displayName}] (${runtimeCacheAccessory.serialNumber}) as ${runtimeCacheAccessory.type} inverted state from '${inboundStateValue}' to '${resultStateValue}'`
);
}
// now check if the accessory should do something: e.g., trigger the alarm, produce an audible beep, etc.
this.processSensorAccessoryActions(runtimeCacheAccessory, defaultStateValue, resultStateValue);
}
switch (TYPES_TO_ACCESSORIES[runtimeCacheAccessory.type][0]) {
case 'ContactSensor':
runtimeCacheAccessory.state = resultStateValue;
this.konnectedPlatformAccessories[runtimeCacheAccessory.UUID].service.updateCharacteristic(
this.Characteristic.ContactSensorState,
resultStateValue
);
break;
case 'MotionSensor':
runtimeCacheAccessory.state = resultStateValue;
this.konnectedPlatformAccessories[runtimeCacheAccessory.UUID].service.updateCharacteristic(
this.Characteristic.MotionDetected,
resultStateValue
);
break;
case 'LeakSensor':
runtimeCacheAccessory.state = resultStateValue;
this.konnectedPlatformAccessories[runtimeCacheAccessory.UUID].service.updateCharacteristic(
this.Characteristic.LeakDetected,
resultStateValue
);
break;
case 'SmokeSensor':
runtimeCacheAccessory.state = resultStateValue;
this.konnectedPlatformAccessories[runtimeCacheAccessory.UUID].service.updateCharacteristic(
this.Characteristic.SmokeDetected,
resultStateValue
);
break;
case 'TemperatureSensor':
runtimeCacheAccessory.temp = inboundPayload.body.temp;
this.konnectedPlatformAccessories[runtimeCacheAccessory.UUID].service.updateCharacteristic(
this.Characteristic.CurrentTemperature,
runtimeCacheAccessory.temp
);
break;
case 'HumiditySensor':
runtimeCacheAccessory.temp = inboundPayload.body.temp;
this.konnectedPlatformAccessories[runtimeCacheAccessory.UUID].service.updateCharacteristic(
this.Characteristic.CurrentTemperature,
runtimeCacheAccessory.temp
);
runtimeCacheAccessory.humi = inboundPayload.body.humi;
this.konnectedPlatformAccessories[runtimeCacheAccessory.UUID].service.updateCharacteristic(
this.Characteristic.CurrentRelativeHumidity,
runtimeCacheAccessory.humi
);
break;
default:
break;
}
}
});
}
}
/**
* Determine if the passed in sensor accessory should do something.
* E.g., trigger the alarm, produce an audible beep, etc.
*
* @param accessory RuntimeCacheInterface The accessory that we are basing our actions by.
* @param defaultStateValue number The original default state of the accessory.
* @param resultStateValue number The state of the accessory as updated.
*/
processSensorAccessoryActions(accessory: RuntimeCacheInterface, defaultStateValue: number, resultStateValue: number) {
// if the default state of the accessory is not the same as the updated state, we should process it
if (defaultStateValue !== resultStateValue) {
this.log.debug(
`[${accessory.displayName}] (${accessory.serialNumber}) as '${accessory.type}' changed from its default state of ${defaultStateValue} to ${resultStateValue}`
);
const securitySystemAccessory = this.accessories.find((accessory) => accessory.UUID === this.securitySystemUUID);
// check what modes the accessory has set to trigger the alarm
if (
accessory.triggerableModes?.includes(String(securitySystemAccessory?.context.device.state) as never) &&
typeof this.entryTriggerDelayTimerHandle === 'undefined'
) {
// accessory should trigger security system
// find beepers and actuate audible delay sound
this.accessoriesRuntimeCache.forEach((beeperAccessory) => {
if (beeperAccessory.type === 'beeper') {
if (this.config.advanced?.entryDelaySettings?.pulseDuration) {
this.actuateAccessory(beeperAccessory.UUID, true, this.config.advanced?.entryDelaySettings);
} else {
this.actuateAccessory(beeperAccessory.UUID, true, {});
}
}
});
// wait the entry delay time and trigger the security system
this.entryTriggerDelayTimerHandle = setTimeout(() => {
this.log.debug(
`Set [${securitySystemAccessory?.displayName}] (${securitySystemAccessory?.context.device.serialNumber}) as '${securitySystemAccessory?.context.device.type}' characteristic: 4 (triggered!)`
);
this.controlSecuritySystem(4);
}, this.entryTriggerDelay);
} else {
// accessory is just sensing change
// restrict it to contact or motion sensor accessories that have the audible notification setting configured
if (['contact', 'motion'].includes(accessory.type) && accessory.audibleBeep) {
this.accessoriesRuntimeCache.forEach((beeperAccessory) => {
if (beeperAccessory.type === 'beeper') {
this.actuateAccessory(beeperAccessory.UUID, true, null);
}
});
}
}
}
}
/**
* Actuate a zone on a panel based on the switch's state.
*
* @param zoneUUID string HAP UUID for the switch zone accessory.
* @param value boolean The value of the state as represented in HomeKit (may be adjusted by config trigger settings).
* @param inboundSwitchSettings object | null | undefined Settings object that can override the default accessory settings.
*/
actuateAccessory(zoneUUID: string, value: boolean, inboundSwitchSettings: Record<string, unknown> | null) {
// retrieve the matching accessory
const existingAccessory = this.accessories.find((accessory) => accessory.UUID === zoneUUID);
// set the representative state in HomeKit
this.konnectedPlatformAccessories[zoneUUID].service.updateCharacteristic(this.Characteristic.On, value);
if (existingAccessory) {
// loop through the plugin configuration to get the correct panel for the zone
this.config.panels.forEach((panelObject) => {
if (panelObject.ipAddress === existingAccessory.context.device.panel.ip) {
// build endpoint
let panelEndpoint =
'http://' +
existingAccessory.context.device.panel.ip +
':' +
existingAccessory.context.device.panel.port +
'/';
// loop through the plugin configuration to get the zone switch settings
panelObject.zones.forEach((zoneObject) => {
if (zoneObject.zoneNumber === existingAccessory.context.device.serialNumber.split('-')[1]) {
const runtimeCacheAccessory: RuntimeCacheInterface = this.accessoriesRuntimeCache.find((runtimeCacheAccessory) => runtimeCacheAccessory.UUID === zoneUUID) as never;
let actuatorState = 0;
if (runtimeCacheAccessory.trigger === 'low' && value === false) {
actuatorState = 1; // set to normally high (1), waiting to be triggered low (0)
} else if (runtimeCacheAccessory.trigger === 'low' && value === true) {
actuatorState = 0; // set to triggered low (0), waiting to be normally high (1)
} else if ((runtimeCacheAccessory.trigger === 'high' || runtimeCacheAccessory.trigger === undefined) && value === false) {
actuatorState = 0; // set to normally low (0), waiting to be triggered high (1)
} else if ((runtimeCacheAccessory.trigger === 'high' || runtimeCacheAccessory.trigger === undefined) && value === true) {
actuatorState = 1; // set to triggered high (1), waiting to be normally low (0)
}
const actuatorPayload: Record<string, unknown> = {
// explicitly convert boolean to integer for the panel payload
state: actuatorState,
};
// Pro zone vs V1-V2 pin payload property assignment
if ('model' in existingAccessory.context.device.panel) {
// this is a Pro panel
panelEndpoint += 'zone';
if (ZONES[zoneObject.zoneNumber].includes('switch')) {
actuatorPayload.zone = zoneObject.zoneNumber;
} else {
this.log.warn(
`Invalid Zone: Cannot actuate the zone '${zoneObject.zoneNumber}' for Konnected Pro Alarm Panels. Try zones 1-8, 'alarm1', 'out1', or 'alarm2_out2'.`
);
}
} else {
// this is a V1-V2 panel
panelEndpoint += 'device';
// convert zone to a pin
if (zoneObject.zoneNumber < 6 || zoneObject.zoneNumber === 'out') {
actuatorPayload!.pin = ZONES_TO_PINS[zoneObject.zoneNumber];
} else {
this.log.warn(
`Invalid Zone: Cannot actuate the zone '${zoneObject.zoneNumber}' for Konnected V1-V2 Alarm Panels. Try zones 1-5 or 'out'.`
);
}
}
// calculate the duration for a momentary switch to complete its triggered task (eg. sequence of pulses)
// this calculation occurs when there are switch settings and the switch is turning 'on' (true)
// otherwise we simply need send a default payload of 'off'
let actuatorDuration;
const switchSettings = inboundSwitchSettings ? inboundSwitchSettings : zoneObject.switchSettings;
if (switchSettings && value === true) {
if (switchSettings.pulseDuration) {
actuatorPayload.momentary = actuatorDuration = switchSettings.pulseDuration;
}
if (switchSettings.pulseRepeat && switchSettings.pulsePause) {
actuatorPayload.times = switchSettings.pulseRepeat;
actuatorPayload.pause = switchSettings.pulsePause;
if (switchSettings.pulseRepeat > 0) {
actuatorDuration =
actuatorDuration * switchSettings.pulseRepeat +
switchSettings.pulsePause * (switchSettings.pulseRepeat - 1);
}
}
}
this.log.debug(
`Actuating [${existingAccessory.displayName}] (${existingAccessory.context.device.serialNumber}) as '${existingAccessory.context.device.type}' with payload:\n` +
JSON.stringify(actuatorPayload, null, 2)
);
// send payload to panel to actuate, and if momentary, also change the switch state back after duration
const actuatePanelZone = async (url: string) => {
try {
const response = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(actuatorPayload),
});
if (
response.status === 200 &&
['beeper', 'siren', 'strobe', 'switch'].includes(existingAccessory.context.device.type)
) {
// if momentary switch, reset the state after calculated duration
if (actuatorDuration > 0 && switchSettings.pulseRepeat !== Number(-1)) {
setTimeout(() => {
// update Homebridge/HomeKit displayed state
this.konnectedPlatformAccessories[zoneUUID].service.updateCharacteristic(
this.Characteristic.On,
false
);
// update the state cache
this.accessoriesRuntimeCache.forEach((runtimeCacheAccessory) => {
if (runtimeCacheAccessory.UUID === zoneUUID) {
runtimeCacheAccessory.state = existingAccessory.context.device.state = false;
this.log.debug(
`Set [${runtimeCacheAccessory.displayName}] (${runtimeCacheAccessory.serialNumber}) as '${runtimeCacheAccessory.type}' characteristic value: false`
);
}
});
}, actuatorDuration);
}
}
} catch (error: unknown) {
if (error instanceof Error) {
this.log.error(error['message']);
}
}
};
actuatePanelZone(panelEndpoint);
}
});
}
});
}
}
/**
* Arm/Disarm/Trigger the security system accessory.
*
* @param value number The value to change the state of the Security System accessory to.
*/
controlSecuritySystem(value: number) {
// pulse settings
const duration = 100; // change this to make the pulse longer or shorter, everything else will calculate
const pause = 1000 - duration;
const minDefault = 10000; // anything less than 10 seconds is just going to have beeps once a second
clearTimeout(this.exitTriggerDelayTimerHandle1);
clearTimeout(this.exitTriggerDelayTimerHandle2);
clearTimeout(this.exitTriggerDelayTimerHandle3);
delete this.exitTriggerDelayTimerHandle1;
delete this.exitTriggerDelayTimerHandle2;
delete this.exitTriggerDelayTimerHandle3;
// if the security system is set to one of 0: home/stay, 1: away, 2: night
if (value < 3) {
// set security system TARGET based on value (then later make actual state by updating CURRENT state)
this.konnectedPlatformAccessories[this.securitySystemUUID].service.updateCharacteristic(
this.Characteristic.SecuritySystemTargetState,
value
);
this.accessoriesRuntimeCache.forEach((runtimeCacheAccessory) => {
if ('beeper' === runtimeCacheAccessory.type) {
// clears any current beeping
this.actuateAccessory(runtimeCacheAccessory.UUID, false, null);
}
});
if (
(typeof this.config.advanced?.exitDelaySettings?.audibleBeeperModes !== 'undefined' &&
this.config.advanced?.exitDelaySettings?.audibleBeeperModes.includes(String(value))) ||
(typeof this.config.advanced?.exitDelaySettings?.audibleBeeperModes === 'undefined' && value === 1)
) {
// if the user has configured which mode should have audible beeper countdowns
// or if not, but the security mode is being set to away, then we do some sort of default countdown
this.accessoriesRuntimeCache.forEach((runtimeCacheAccessory) => {
if ('beeper' === runtimeCacheAccessory.type) {
if (this.exitTriggerDelay > minDefault) {
// three stages of short beeper pulses (per second): 1ps > 2ps > 4ps
this.actuateAccessory(runtimeCacheAccessory.UUID, true, {
pulseDuration: duration,
pulsePause: pause,
pulseRepeat: Math.floor(this.exitTriggerDelay / 1000 / 3),
});
// we need to clear this if the mode changes before they complete
// make as a variable that we can clear set timeout
this.exitTriggerDelayTimerHandle2 = setTimeout(() => {
this.actuateAccessory(runtimeCacheAccessory.UUID, true, {
pulseDuration: duration,
pulsePause: pause / 2,
pulseRepeat: Math.floor((this.exitTriggerDelay / 1000 / 3) * 2) - 1,
});
}, this.exitTriggerDelay / 3);
// we need to clear this if the mode changes before they complete
// make as a variable that we can clear set timeout
this.exitTriggerDelayTimerHandle3 = setTimeout(() => {
this.actuateAccessory(runtimeCacheAccessory.UUID, true, {
pulseDuration: duration,
pulsePause: pause / 4,
pulseRepeat: Math.floor((this.exitTriggerDelay / 1000 / 3) * 4) - 2,
});
}, (this.exitTriggerDelay / 3) * 2);
} else if (this.exitTriggerDelay <= minDefault && this.exitTriggerDelay > 1000) {
// one short pulse per second
this.actuateAccessory(runtimeCacheAccessory.UUID, true, {
pulseDuration: duration,
pulsePause: pause,
pulseRepeat: this.exitTriggerDelay / 1000,
});
}
}
});
// wait the exit delay time and then arm security system based on value
this.exitTriggerDelayTimerHandle1 = setTimeout(() => {
this.konnectedPlatformAccessories[this.securitySystemUUID].service.updateCharacteristic(
this.Characteristic.SecuritySystemCurrentState,
value
);
}, this.exitTriggerDelay);
} else {
// immediately arm system
this.konnectedPlatformAccessories[this.securitySystemUUID].service.updateCharacteristic(
this.Characteristic.SecuritySystemCurrentState,
value
);
}
} else {
// 3: disarmed, 4: alarm triggered
this.konnectedPlatformAccessories[this.securitySystemUUID].service.updateCharacteristic(
this.Characteristic.SecuritySystemCurrentState,
value
);
}
// store in platform accessories cache
this.accessories.find((accessory) => {
if (accessory.UUID === this.securitySystemUUID) {
accessory.context.device.state = value;
}
});
// if the security system is turned off, turn beepers, sirens and strobes off
if (value === 3) {
clearTimeout(this.entryTriggerDelayTimerHandle);
delete this.entryTriggerDelayTimerHandle;
this.accessoriesRuntimeCache.forEach((runtimeCacheAccessory) => {
if (['beeper', 'siren', 'strobe'].includes(runtimeCacheAccessory.type)) {
this.actuateAccessory(runtimeCacheAccessory.UUID, false, null);
}
});
}
// if the security system is triggered
if (value === 4) {
this.accessoriesRuntimeCache.forEach((runtimeCacheAccessory) => {
// turns off the beeper
if ('beeper' === runtimeCacheAccessory.type) {
this.actuateAccessory(runtimeCacheAccessory.UUID, false, null);
}
// turns on the siren and strobe lights
if (['siren', 'strobe'].includes(runtimeCacheAccessory.type)) {
this.actuateAccessory(runtimeCacheAccessory.UUID, true, null);
}
});
/** for future
* @link https://www.npmjs.com/package/@noonlight/noonlight-sdk
*/
}
}
}
Example #13
Source File: platform.ts From homebridge-plugin-template with Apache License 2.0 | 4 votes |
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class ExampleHomebridgePlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
constructor(
public readonly log: Logger,
public readonly config: PlatformConfig,
public readonly api: API,
) {
this.log.debug('Finished initializing platform:', this.config.name);
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', () => {
log.debug('Executed didFinishLaunching callback');
// run the method to discover / register your devices as accessories
this.discoverDevices();
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices() {
// EXAMPLE ONLY
// A real plugin you would discover accessories from the local network, cloud services
// or a user-defined array in the platform config.
const exampleDevices = [
{
exampleUniqueId: 'ABCD',
exampleDisplayName: 'Bedroom',
},
{
exampleUniqueId: 'EFGH',
exampleDisplayName: 'Kitchen',
},
];
// loop over the discovered devices and register each one if it has not already been registered
for (const device of exampleDevices) {
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
const uuid = this.api.hap.uuid.generate(device.exampleUniqueId);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
// existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
new ExamplePlatformAccessory(this, existingAccessory);
// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.:
// remove platform accessories when no longer present
// this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
// this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
} else {
// the accessory does not yet exist, so we need to create it
this.log.info('Adding new accessory:', device.exampleDisplayName);
// create a new accessory
const accessory = new this.api.platformAccessory(device.exampleDisplayName, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
new ExamplePlatformAccessory(this, accessory);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
}
}
}
Example #14
Source File: platform.ts From homebridge-vieramatic with Apache License 2.0 | 4 votes |
class VieramaticPlatform implements DynamicPlatformPlugin {
readonly Service: typeof Service
readonly Characteristic: typeof Characteristic
readonly accessories: PlatformAccessory[] = []
readonly storage: Storage
constructor(
readonly log: Logger,
private readonly config: PlatformConfig,
private readonly api: API
) {
this.storage = new Storage(api)
this.Characteristic = this.api.hap.Characteristic
this.Service = this.api.hap.Service
this.log.debug('Finished initializing platform:', this.config.platform)
this.api.on('didFinishLaunching', async () => {
log.debug('Executed didFinishLaunching callback')
await this.discoverDevices()
})
}
configureAccessory = (accessory: PlatformAccessory): void => {
this.log.info('Loading accessory from cache:', accessory.displayName)
this.accessories.push(accessory)
}
discoverDevices = async (): Promise<void> => {
this.accessories.map((cachedAccessory) =>
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [cachedAccessory])
)
const devices = (this.config.tvs ?? []) as UserConfig[]
const sanityCheck = dupeChecker(devices)
if (Abnormal(sanityCheck)) {
this.log.error('Aborted loading as a fatal error was found.')
this.log.error(
'Attempting to setup more than a single TV with same IP address: ',
sanityCheck.error.message
)
this.log.error('please fix your config and then restart homebridge again!')
return
}
for (const device of devices) {
const outcome = await this.#deviceSetup(device)
if (Abnormal(outcome)) {
this.log.error(outcome.error.message)
continue
}
this.api.publishExternalAccessories(PLUGIN_NAME, [outcome.value.accessory])
this.log.info('successfully loaded', outcome.value.accessory.displayName)
}
}
#deviceSetupPreFlight = (device: UserConfig): Outcome<void> => {
const { ipAddress, mac } = device
const invalid = (type: string): Error =>
Error(`IGNORED '${ipAddress}': it has an invalid ${type} address.\n\n${prettyPrint(device)}`)
if (!isValidIPv4(ipAddress)) return { error: invalid('ip') }
if (mac && !isValidMACAddress(mac)) return { error: invalid('MAC') }
return {}
}
#knownWorking = (ip: string): VieraSpecs => {
if (isEmpty(this.storage.accessories)) return {}
for (const [_, v] of Object.entries(this.storage.accessories))
if (v.data.ipAddress === ip) return v.data.specs
return {}
}
#deviceSetup = async (device: UserConfig): Promise<Outcome<VieramaticPlatformAccessory>> => {
this.log.info("handling '%s' from config.json", device.ipAddress)
const [ip, outcome] = [device.ipAddress, this.#deviceSetupPreFlight(device)]
if (Abnormal(outcome)) return outcome
const [reachable, cached] = [await VieraTV.livenessProbe(ip), this.#knownWorking(ip)]
if (!reachable && isEmpty(cached)) {
const error = Error(
`IGNORING '${ip}' as it is not reachable.\n` +
"(As we can't rely on cached data since it seems that it was never ever seen and " +
'fully setup before)\n\n' +
'Please make sure that your TV is powered ON and connected to the network.'
)
return { error }
}
const hasAuth = device.appId && device.encKey
const auth = hasAuth ? ({ appId: device.appId, key: device.encKey } as VieraAuth) : undefined
const conn = await VieraTV.connect(ip, this.log, { auth, cached })
if (Abnormal(conn)) return conn
const tv = conn.value
if (device.friendlyName !== undefined) tv.specs.friendlyName = device.friendlyName
tv.specs.friendlyName = tv.specs.friendlyName.trim()
try {
const accessory = new this.api.platformAccessory(
tv.specs.friendlyName,
tv.specs.serialNumber,
this.api.hap.Categories.TELEVISION
)
accessory.context.device = tv
const accessories = this.storage.accessories
const firstTime = isEmpty(accessories) || !accessories[tv.specs.serialNumber]
if (firstTime) this.log.info(`Initializing '${tv.specs.friendlyName}' first time ever.`)
if (!device.disabledAppSupport && Abnormal(tv.apps)) {
const err = `Unable to fetch Apps list from the TV: ${tv.apps.error.message}.`
const ft = `Unable to finish initial setup of ${tv.specs.friendlyName}. ${err}. This TV must be powered ON and NOT in stand-by.`
if (firstTime) return { error: Error(ft) }
this.log.debug(err)
}
return { value: new VieramaticPlatformAccessory(this, accessory, device) }
} catch (error) {
this.log.error('device:', prettyPrint(device))
this.log.error('specs:', prettyPrint(tv.specs))
return { error: error as Error }
}
}
}
Example #15
Source File: platform.ts From homebridge-iRobot with Apache License 2.0 | 4 votes |
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class iRobotPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
constructor(
public readonly log: Logger,
public readonly config: PlatformConfig,
public readonly api: API,
) {
this.log.debug('Finished initializing platform:', this.config.name);
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', () => {
log.debug('Executed didFinishLaunching callback');
// run the method to discover / register your devices as accessories
this.discoverDevices();
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices() {
if(this.config.email === undefined || this.config.password === undefined){
this.log.warn('No email or password provided. Exiting setup');
return;
}
// EXAMPLE ONLY
// A real plugin you would discover accessories from the local network, cloud services
// or a user-defined array in the platform config.
// loop over the discovered devices and register each one if it has not already been registered
for (const device of getRoombas(this.config.email, this.config.password, this.log, this.config)) {
if(this.config.disableMultiRoom){
device.multiRoom = false;
}
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
const uuid = this.api.hap.uuid.generate(device.blid);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
// existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
new iRobotPlatformAccessory(this, existingAccessory, device);
// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.:
// remove platform accessories when no longer present
// this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
// this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
} else {
if (device.ip === 'undefined') {
return;
}
// the accessory does not yet exist, so we need to create it
this.log.info('Adding new accessory:', device.name);
// create a new accessory
const accessory = new this.api.platformAccessory(device.name, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
new iRobotPlatformAccessory(this, accessory, device);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
}
}
}
Example #16
Source File: platform.ts From homebridge-plugin-eufy-security with Apache License 2.0 | 4 votes |
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class EufySecurityHomebridgePlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap
.Characteristic;
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
public httpService: HttpService;
private config: EufyPlatformConfig;
constructor(
public readonly log: Logger,
config: PlatformConfig,
public readonly api: API,
) {
this.config = config as EufyPlatformConfig;
// this.log.debug('Config', this.config);
this.log.debug('Finished initializing platform:', this.config.platform);
this.httpService = new HttpService(this.config.username, this.config.password);
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', async () => {
if (this.config.enablePush) {
this.log.info('push client enabled');
await this.setupPushClient();
} else {
this.log.info('push client disabled');
}
log.debug('Executed didFinishLaunching callback');
// run the method to discover / register your devices as accessories
try {
this.discoverDevices();
} catch(error) {
this.log.error('error while discovering devices');
this.log.error(error);
}
});
}
async setupPushClient() {
const storagePath = this.api.user.storagePath();
const credentialsPath = `${storagePath}/eufy-security-credentials.json`;
let credentials: PushCredentials;
if (fs.existsSync(credentialsPath)) {
this.log.info('credentials found. reusing them...');
credentials = JSON.parse(fs.readFileSync(credentialsPath).toString());
} else {
// Register push credentials
this.log.info('no credentials found. register new...');
const pushService = new PushRegisterService();
credentials = await pushService.createPushCredentials();
fs.writeFileSync(credentialsPath, JSON.stringify(credentials));
this.log.info('wait a short time (5sec)...');
await new Promise((r) => setTimeout(r, 5000));
}
// Start push client
const pushClient = await PushClient.init({
androidId: credentials.checkinResponse.androidId,
securityToken: credentials.checkinResponse.securityToken,
});
const fcmToken = credentials.gcmResponse.token;
await new Promise((resolve) => {
const tHandle = setTimeout(() => {
this.log.error('registering a push token timed out');
resolve(true);
}, 20000);
this.httpService
.registerPushToken(fcmToken)
.catch((err) => {
clearTimeout(tHandle);
this.log.error('failed to register push token', err);
resolve(true);
})
.then(() => {
clearTimeout(tHandle);
this.log.debug('registered at eufy with:', fcmToken);
resolve(true);
});
});
setInterval(async () => {
try {
await this.httpService.pushTokenCheck();
} catch (err) {
this.log.warn('failed to confirm push token');
}
}, 30 * 1000);
pushClient.connect((msg) => {
this.log.debug('push message:', msg);
const matchingUuid = this.api.hap.uuid.generate(msg.payload?.device_sn);
const knownAccessory = this.accessories.find(
(accessory) => accessory.UUID === matchingUuid,
);
const event_type = msg.payload?.payload?.event_type;
if (knownAccessory) {
if (event_type === MessageTypes.MOTION_DETECTION || event_type === MessageTypes.FACE_DETECTION) {
// TODO: Implement motion sensor
} else if (event_type === MessageTypes.PRESS_DOORBELL) {
knownAccessory
.getService(this.api.hap.Service.Doorbell)!
.updateCharacteristic(
this.api.hap.Characteristic.ProgrammableSwitchEvent,
this.api.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS,
);
}
}
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
async discoverDevices() {
const hubs = await this.httpService.listHubs();
for (const hub of hubs) {
this.log.debug(
`found hub "${hub.station_name}" (${hub.station_sn}) `,
);
if (this.config.ignoreHubSns?.includes(hub.station_sn)) {
this.log.debug('ignoring hub ' + hub.station_sn);
}
}
const devices = await this.httpService.listDevices();
for (const device of devices) {
const ignoredHub = this.config.ignoreHubSns?.includes(device.station_sn);
const ignoredDevice = this.config.ignoreDeviceSns?.includes(device.device_sn);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { params, member, station_conn, ...strippedDevice } = device;
this.log.debug(`found device "${device.device_name}" (${device.device_sn})
ID: ${device.device_id}
Model: ${device.device_model}
Serial Number: ${device.device_sn}
Type: ${device.device_type}
Channel: ${device.device_channel}
Hub: ${device.station_conn.station_name} (${device.station_sn})
`);
this.log.debug(
`device dump: ${{
...strippedDevice,
params: params.map((param) => [
param.param_id,
param.param_type,
param.param_value,
]),
}}`,
);
if (ignoredHub) {
this.log.debug(`device is part of ignored hub "${device.station_sn}"`);
}
if (ignoredDevice) {
this.log.debug(`device is ignored "${device.device_sn}"`);
}
if (ignoredHub || ignoredDevice) {
continue;
}
const uuid = this.api.hap.uuid.generate(device.device_sn);
const existingAccessory = this.accessories.find(
(accessory) => accessory.UUID === uuid,
);
// doorbell
if (
[
DeviceType.BATTERY_DOORBELL,
DeviceType.BATTERY_DOORBELL_2,
DeviceType.DOORBELL,
].includes(device.device_type)
) {
if (existingAccessory) {
// the accessory already exists
this.log.info(
'Restoring existing accessory from cache:',
existingAccessory.displayName,
);
new DoorbellPlatformAccessory(this, existingAccessory, device);
} else {
// the accessory does not yet exist, so we need to create it
this.log.info('Adding new accessory:', device.device_name);
// create a new accessory
const accessory = new this.api.platformAccessory(
device.device_name,
uuid,
);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
new DoorbellPlatformAccessory(this, accessory, device);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
}
}
}
// @todo
// this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
// this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
}
}
Example #17
Source File: index.ts From homebridge-electrolux-wellbeing with Apache License 2.0 | 4 votes |
class ElectroluxWellbeingPlatform implements DynamicPlatformPlugin {
private client?: AxiosInstance;
private readonly log: Logging;
private readonly api: API;
private readonly config: PlatformConfig;
private readonly accessories: PlatformAccessory[] = [];
constructor(log: Logging, config: PlatformConfig, api: API) {
this.log = log;
this.api = api;
this.config = config;
api.on(APIEvent.DID_FINISH_LAUNCHING, async () => {
if (this.needsConfiguration()) {
this.log('Please configure this plugin first.');
return;
}
//this.removeAccessories();
try {
this.client = await createClient({
username: this.config.username,
password: this.config.password,
});
} catch (err) {
this.log.debug('Error while creating client', err);
return;
}
const appliances = await this.getAllAppliances();
const applianceData = await Promise.all(
appliances.map((appliance) => this.fetchApplianceData(appliance.pncId)),
);
this.log.debug('Fetched: ', applianceData);
appliances.map(({ applianceName, modelName, pncId }, i) => {
this.addAccessory({
pncId,
name: applianceName,
modelName,
firmwareVersion: applianceData[i]?.firmwareVersion,
});
});
this.updateValues(applianceData);
setInterval(
() => this.checkAppliances(),
this.getPollTime(this.config.pollTime),
);
});
}
needsConfiguration(): boolean {
return !this.config.username || !this.config.password;
}
getPollTime(pollTime): number {
if (!pollTime || pollTime < 5) {
this.log.info('Set poll time is below 5s, forcing 5s');
return 5 * 1000;
}
this.log.debug(`Refreshing every ${pollTime}s`);
return pollTime * 1000;
}
async checkAppliances() {
const data = await this.fetchAppliancesData();
this.log.debug('Fetched: ', data);
this.updateValues(data);
}
async fetchAppliancesData() {
return await Promise.all(
this.accessories.map((accessory) =>
this.fetchApplianceData(accessory.context.pncId),
),
);
}
async fetchApplianceData(pncId: string): Promise<Appliance | undefined> {
try {
const response: { data: WellbeingApi.ApplianceData } =
await this.client!.get(`/Appliances/${pncId}`);
const reported = response.data.twin.properties.reported;
return {
pncId,
name: response.data.applianceData.applianceName,
modelName: response.data.applianceData.modelName,
firmwareVersion: reported.FrmVer_NIU,
workMode: reported.Workmode,
filterRFID: reported.FilterRFID,
filterLife: reported.FilterLife,
fanSpeed: reported.Fanspeed,
UILight: reported.UILight,
safetyLock: reported.SafetyLock,
ionizer: reported.Ionizer,
sleep: reported.Sleep,
scheduler: reported.Scheduler,
filterType: reported.FilterType,
version: reported['$version'],
pm1: reported.PM1,
pm25: reported.PM2_5,
pm10: reported.PM10,
tvoc: reported.TVOC,
co2: reported.CO2,
temp: reported.Temp,
humidity: reported.Humidity,
envLightLevel: reported.EnvLightLvl,
rssi: reported.RSSI,
};
} catch (err) {
this.log.info('Could not fetch appliances data: ' + err);
}
}
async getAllAppliances() {
try {
const response: { data: WellbeingApi.Appliance[] } =
await this.client!.get('/Domains/Appliances');
return response.data;
} catch (err) {
this.log.info('Could not fetch appliances: ' + err);
return [];
}
}
async sendCommand(
pncId: string,
command: string,
value: CharacteristicValue,
) {
this.log.debug('sending command', {
[command]: value,
});
try {
const response = await this.client!.put(`/Appliances/${pncId}/Commands`, {
[command]: value,
});
this.log.debug('command responded', response.data);
} catch (err) {
this.log.info('Could run command', err);
}
}
updateValues(data) {
this.accessories.map((accessory) => {
const { pncId } = accessory.context;
const state = this.getApplianceState(pncId, data);
// Guard against missing data due to API request failure.
if (!state) {
return;
}
// Keep firmware revision up-to-date in case the device is updated.
accessory
.getService(Service.AccessoryInformation)!
.setCharacteristic(
Characteristic.FirmwareRevision,
state.firmwareVersion,
);
accessory
.getService(Service.TemperatureSensor)!
.updateCharacteristic(Characteristic.CurrentTemperature, state.temp);
accessory
.getService(Service.HumiditySensor)!
.updateCharacteristic(
Characteristic.CurrentRelativeHumidity,
state.humidity,
);
accessory
.getService(Service.CarbonDioxideSensor)!
.updateCharacteristic(Characteristic.CarbonDioxideLevel, state.co2);
if (state.envLightLevel) {
// Env Light Level needs to be tested with lux meter
accessory
.getService(Service.LightSensor)!
.updateCharacteristic(
Characteristic.CurrentAmbientLightLevel,
state.envLightLevel,
);
}
accessory
.getService(Service.AirQualitySensor)!
.updateCharacteristic(
Characteristic.AirQuality,
this.getAirQualityLevel(state.pm25),
)
.updateCharacteristic(Characteristic.PM2_5Density, state.pm25)
.updateCharacteristic(Characteristic.PM10Density, state.pm10)
.updateCharacteristic(
Characteristic.VOCDensity,
this.convertTVOCToDensity(state.tvoc),
);
accessory
.getService(Service.AirPurifier)!
.updateCharacteristic(Characteristic.FilterLifeLevel, state.filterLife)
.updateCharacteristic(
Characteristic.FilterChangeIndication,
state.filterLife < 10
? Characteristic.FilterChangeIndication.CHANGE_FILTER
: Characteristic.FilterChangeIndication.FILTER_OK,
)
.updateCharacteristic(
Characteristic.Active,
state.workMode !== WorkModes.Off,
)
.updateCharacteristic(
Characteristic.CurrentAirPurifierState,
this.getAirPurifierState(state.workMode),
)
.updateCharacteristic(
Characteristic.TargetAirPurifierState,
this.getAirPurifierStateTarget(state.workMode),
)
.updateCharacteristic(
Characteristic.RotationSpeed,
state.fanSpeed * FAN_SPEED_MULTIPLIER,
)
.updateCharacteristic(
Characteristic.LockPhysicalControls,
state.safetyLock,
)
.updateCharacteristic(Characteristic.SwingMode, state.ionizer);
});
}
getApplianceState(pncId: string, data): Appliance {
return _.find(data, { pncId });
}
configureAccessory(accessory: PlatformAccessory): void {
this.log('Configuring accessory %s', accessory.displayName);
const { pncId } = accessory.context;
accessory.on(PlatformAccessoryEvent.IDENTIFY, () => {
this.log('%s identified!', accessory.displayName);
});
accessory
.getService(Service.AirPurifier)!
.getCharacteristic(Characteristic.Active)
.on(
CharacteristicEventTypes.SET,
(value: CharacteristicValue, callback: CharacteristicSetCallback) => {
const workMode = value === 1 ? WorkModes.Auto : WorkModes.Off;
if (
accessory
.getService(Service.AirPurifier)!
.getCharacteristic(Characteristic.Active).value !== value
) {
this.sendCommand(pncId, 'WorkMode', workMode);
this.log.info(
'%s AirPurifier Active was set to: ' + workMode,
accessory.displayName,
);
}
callback();
},
);
accessory
.getService(Service.AirPurifier)!
.getCharacteristic(Characteristic.TargetAirPurifierState)
.on(
CharacteristicEventTypes.SET,
(value: CharacteristicValue, callback: CharacteristicSetCallback) => {
const workMode =
value === Characteristic.TargetAirPurifierState.MANUAL
? WorkModes.Manual
: WorkModes.Auto;
this.sendCommand(pncId, 'WorkMode', workMode);
this.log.info(
'%s AirPurifier Work Mode was set to: ' + workMode,
accessory.displayName,
);
callback();
},
);
accessory
.getService(Service.AirPurifier)!
.getCharacteristic(Characteristic.RotationSpeed)
.on(
CharacteristicEventTypes.SET,
(value: CharacteristicValue, callback: CharacteristicSetCallback) => {
const fanSpeed = Math.floor(
parseInt(value.toString()) / FAN_SPEED_MULTIPLIER,
);
this.sendCommand(pncId, 'FanSpeed', fanSpeed);
this.log.info(
'%s AirPurifier Fan Speed set to: ' + fanSpeed,
accessory.displayName,
);
callback();
},
);
accessory
.getService(Service.AirPurifier)!
.getCharacteristic(Characteristic.LockPhysicalControls)
.on(
CharacteristicEventTypes.SET,
(value: CharacteristicValue, callback: CharacteristicSetCallback) => {
if (
accessory
.getService(Service.AirPurifier)!
.getCharacteristic(Characteristic.LockPhysicalControls).value !==
value
) {
this.sendCommand(pncId, 'SafetyLock', value);
this.log.info(
'%s AirPurifier Saftey Lock set to: ' + value,
accessory.displayName,
);
}
callback();
},
);
accessory
.getService(Service.AirPurifier)!
.getCharacteristic(Characteristic.SwingMode)
.on(
CharacteristicEventTypes.SET,
(value: CharacteristicValue, callback: CharacteristicSetCallback) => {
if (
accessory
.getService(Service.AirPurifier)!
.getCharacteristic(Characteristic.SwingMode).value !== value
) {
this.sendCommand(pncId, 'Ionizer', value);
this.log.info(
'%s AirPurifier Ionizer set to: ' + value,
accessory.displayName,
);
}
callback();
},
);
this.accessories.push(accessory);
}
addAccessory({ name, modelName, pncId, firmwareVersion }) {
const uuid = hap.uuid.generate(pncId);
if (!this.isAccessoryRegistered(name, uuid)) {
this.log.info('Adding new accessory with name %s', name);
const accessory = new Accessory(name, uuid);
accessory.context.pncId = pncId;
accessory.addService(Service.AirPurifier);
accessory.addService(Service.AirQualitySensor);
accessory.addService(Service.TemperatureSensor);
accessory.addService(Service.CarbonDioxideSensor);
accessory.addService(Service.HumiditySensor);
accessory.addService(Service.LightSensor);
accessory
.getService(Service.AccessoryInformation)!
.setCharacteristic(Characteristic.Manufacturer, 'Electrolux')
.setCharacteristic(Characteristic.Model, modelName)
.setCharacteristic(Characteristic.SerialNumber, pncId)
.setCharacteristic(Characteristic.FirmwareRevision, firmwareVersion);
this.configureAccessory(accessory);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
} else {
this.log.info(
'Accessory name %s already added, loading from cache ',
name,
);
}
}
removeAccessories() {
this.log.info('Removing all accessories');
this.api.unregisterPlatformAccessories(
PLUGIN_NAME,
PLATFORM_NAME,
this.accessories,
);
this.accessories.splice(0, this.accessories.length);
}
isAccessoryRegistered(name: string, uuid: string) {
return !!_.find(this.accessories, { UUID: uuid });
}
getAirQualityLevel(pm25: number): number {
switch (true) {
case pm25 < 6:
return Characteristic.AirQuality.EXCELLENT;
case pm25 < 12:
return Characteristic.AirQuality.GOOD;
case pm25 < 36:
return Characteristic.AirQuality.FAIR;
case pm25 < 50:
return Characteristic.AirQuality.INFERIOR;
case pm25 >= 50:
return Characteristic.AirQuality.POOR;
}
return Characteristic.AirQuality.UNKNOWN;
}
getAirPurifierState(workMode: WorkModes): number {
if (workMode !== WorkModes.Off) {
return Characteristic.CurrentAirPurifierState.PURIFYING_AIR;
}
return Characteristic.CurrentAirPurifierState.INACTIVE;
}
getAirPurifierStateTarget(workMode: WorkModes): number {
if (workMode === WorkModes.Auto) {
return Characteristic.TargetAirPurifierState.AUTO;
}
return Characteristic.TargetAirPurifierState.MANUAL;
}
// Best effort attempt to convert Wellbeing TVOC ppb reading to μg/m3, but we lack insight into their algorithms
// or TVOC densities. We assume 1 ppb = 3.243 μg/m3 (see benzene @ 20C [1]) as this produces results (μg/m3) that fit
// quite well within the defined ranges in [2].
//
// Wellbeing defines 1500 ppb as possibly having an effect on health when exposed to these levels for a month, [2]
// lists 400-500 μg/m3 as _marginal_ which sounds like a close approximation. Here's an example where 1500 ppb falls
// within the _marginal_ range.
//
// 1500 * 3.243 / 10 = 486.45
//
// Note: It's uncertain why we have to divide the result by 10 for the values to make sense, perhaps this is a
// Wellbeing quirk, but at least the values look good.
//
// The maximum value shown by Wellbeing is 4000 ppb and the maximum value accepted by HomeKit is 1000 μg/m3, our
// assumed molecular density may put the value outside of the HomeKit range, but not by much, which seems acceptable:
//
// 4000 * 3.243 / 10 = 1297.2
//
// [1] https://uk-air.defra.gov.uk/assets/documents/reports/cat06/0502160851_Conversion_Factors_Between_ppb_and.pdf
// [2] https://myhealthyhome.info/assets/pdfs/TB531rev2TVOCInterpretation.pdf
convertTVOCToDensity(tvocppb: number): number {
const ugm3 = (tvocppb * 3.243) / 10;
return Math.min(ugm3, 1000);
}
}
Example #18
Source File: platform.ts From homebridge-plugin-govee with Apache License 2.0 | 4 votes |
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class GoveeHomebridgePlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap
.Characteristic;
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
private platformStatus?: APIEvent;
private readonly discoveryCache = new Map();
constructor(
public readonly log: Logger,
public readonly config: PlatformConfig,
public readonly api: API
) {
this.log.info("Finished initializing platform:", this.config.name);
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on("didFinishLaunching", () => {
log.debug("Executed didFinishLaunching callback");
// run the method to discover / register your devices as accessories
this.platformStatus = APIEvent.DID_FINISH_LAUNCHING;
this.discoverDevices();
});
this.api.on("shutdown", () => {
this.platformStatus = APIEvent.SHUTDOWN;
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.info("Loading accessory from cache:", accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices() {
this.log.debug("Start discovery");
if (this.config.debug) {
GoveeDebug(true);
}
GoveeStartDiscovery(this.goveeDiscoveredReading.bind(this));
GoveeRegisterScanStart(this.goveeScanStarted.bind(this));
GoveeRegisterScanStop(this.goveeScanStopped.bind(this));
// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.:
// this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
private goveeDiscoveredReading(reading: GoveeReading) {
this.log.debug("Govee reading", reading);
let deviceUniqueId = reading.uuid;
if (reading.model) {
deviceUniqueId = reading.model;
}
if (!deviceUniqueId) {
this.log.error(
"device missing unique identifier. Govee reading: ",
reading
);
return;
}
//Now check if the device is in the ignore list, if it is, skip working on it, if ignore list is empty do nothing!
if (this.config.IgnoreDeviceNames) {
this.log.debug("Ignore List filled, will check list!");
const displayName = `${this.sanitize(reading.model)}`;
const IgnoredDeviceNames = this.config.IgnoreDeviceNames;
if (IgnoredDeviceNames.includes(displayName)) {
//Device is in the Ignore-List so skip working on it
this.log.debug("Device in Ignore List ", displayName, " skipping the device!");
return;
}
}
else {
this.log.debug("Ignore List is empty, nothing to check!");
}
// discovered devices and register each one if it has not already been registered
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
const uuid = this.api.hap.uuid.generate(deviceUniqueId);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(
(accessory) => accessory.UUID === uuid
);
if (this.discoveryCache.has(uuid)) {
const cachedInstance = this.discoveryCache.get(
uuid
) as GoveePlatformAccessory;
cachedInstance.updateReading(reading);
return;
}
if (existingAccessory) {
// the accessory already exists
this.log.info(
"Restoring existing accessory from cache:",
existingAccessory.displayName
);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
existingAccessory.context.batteryThreshold = this.config.batteryThreshold;
existingAccessory.context.humidityOffset = this.config.humidityOffset;
this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
const existingInstance = new GoveePlatformAccessory(
this,
existingAccessory,
reading
);
this.discoveryCache.set(uuid, existingInstance);
} else {
const displayName = `${this.sanitize(reading.model)}`;
// the accessory does not yet exist, so we need to create it
this.log.info("Adding new accessory:", displayName);
// create a new accessory
const accessory = new this.api.platformAccessory(displayName, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
const contextDevice: DeviceContext = {
address: this.sanitize(reading.address),
model: this.sanitize(reading.model),
uuid: reading.uuid,
};
accessory.context.device = contextDevice;
accessory.context.batteryThreshold = this.config.batteryThreshold;
accessory.context.humidityOffset = this.config.humidityOffset;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
const newInstance = new GoveePlatformAccessory(this, accessory, reading);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
this.discoveryCache.set(uuid, newInstance);
}
}
private goveeScanStarted() {
this.log.info("Govee Scan Started");
}
private goveeScanStopped() {
this.log.info("Govee Scan Stopped");
if (!this.platformStatus || this.platformStatus === APIEvent.SHUTDOWN) {
return;
}
const WAIT_INTERVAL = 5000;
// wait, and restart discovery if platform status doesn't change
setTimeout(() => {
if (!this.platformStatus || this.platformStatus === APIEvent.SHUTDOWN) {
return;
}
this.log.warn("Govee discovery stopped while Homebridge is running.");
this.log.info("Restart Discovery");
GoveeStartDiscovery(this.goveeDiscoveredReading.bind(this));
}, WAIT_INTERVAL);
}
private sanitize(s: string): string {
return s.trim().replace("_", "");
}
}
Example #19
Source File: platform.ts From homebridge-tapo-p100 with Apache License 2.0 | 4 votes |
/**
* TapoPlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export default class TapoPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
public readonly FakeGatoHistoryService;
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
public readonly config: TapoConfig;
public customCharacteristics: {
[key: string]: WithUUID<new () => Characteristic>;
};
constructor(
public readonly log: Logger,
config: PlatformConfig,
public readonly api: API,
) {
this.log.debug('config.json: %j', config);
this.config = parseConfig(config);
this.log.debug('config: %j', this.config);
this.log.debug('Finished initializing platform:', this.config.name);
this.customCharacteristics = Characteristics(api.hap.Characteristic);
this.FakeGatoHistoryService = fakegato(this.api);
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', () => {
log.debug('Executed didFinishLaunching callback');
// run the method to discover / register your devices as accessories
this.discoverDevices();
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices() {
// EXAMPLE ONLY
// A real plugin you would discover accessories from the local network, cloud services
// or a user-defined array in the platform config.
const devices = this.config.discoveryOptions.devices;
if(devices){
// loop over the discovered devices and register each one if it has not already been registered
for (const device of devices) {
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
const uuid = this.api.hap.uuid.generate(device.host + (device.name ? device.name : ''));
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
if (device) {
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
// existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
if(device.type && device.type.toLowerCase() === 'colorlight'){
new L530Accessory(this.log, this, existingAccessory, device.timeout ? device.timeout : 2, device.updateInterval);
} else if(device.type && device.type.toLowerCase() === 'light'){
new L510EAccessory(this.log, this, existingAccessory, device.timeout ? device.timeout : 2, device.updateInterval);
} else if(device.type && device.type.toLowerCase() === 'powerplug'){
new P110Accessory(this.log, this, existingAccessory, device.timeout ? device.timeout : 2, device.updateInterval);
} else{
new P100Accessory(this.log, this, existingAccessory, device.timeout ? device.timeout : 2, device.updateInterval);
}
// update accessory cache with any changes to the accessory details and information
this.api.updatePlatformAccessories([existingAccessory]);
} else if (!device) {
// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.:
// remove platform accessories when no longer present
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
}
} else {
// the accessory does not yet exist, so we need to create it
this.log.info('Adding new accessory:', device.host);
let accessory:PlatformAccessory;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
if(device.type && device.type.toLowerCase() === 'colorlight'){
// create a new accessory
accessory = new this.api.platformAccessory(device.name ? device.name : device.host, uuid, Categories.LIGHTBULB);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
new L530Accessory(this.log, this, accessory, device.timeout ? device.timeout : 2, device.updateInterval);
} else if(device.type && device.type.toLowerCase() === 'light'){
// create a new accessory
accessory = new this.api.platformAccessory(device.name ? device.name : device.host, uuid, Categories.LIGHTBULB);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
new L510EAccessory(this.log, this, accessory, device.timeout ? device.timeout : 2, device.updateInterval);
} else if(device.type && device.type.toLowerCase() === 'powerplug'){
// create a new accessory
accessory = new this.api.platformAccessory(device.name ? device.name : device.host, uuid, Categories.OUTLET);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
new P110Accessory(this.log, this, accessory, device.timeout ? device.timeout : 2, device.updateInterval);
} else{
// create a new accessory
accessory = new this.api.platformAccessory(device.name ? device.name : device.host, uuid, Categories.OUTLET);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
new P100Accessory(this.log, this, accessory, device.timeout ? device.timeout : 2, device.updateInterval);
}
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
}
}
}
}
Example #20
Source File: platform.ts From HomebridgeMagicHome-DynamicPlatform with Apache License 2.0 | 4 votes |
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class HomebridgeMagichomeDynamicPlatform implements DynamicPlatformPlugin {
public readonly Service = this.api.hap.Service;
public readonly Characteristic = this.api.hap.Characteristic;
// this is used to track restored cached accessories
public readonly accessories: MagicHomeAccessory[] = [];
public count = 1;
private periodicDiscovery: NodeJS.Timeout | null = null;
private logs: Logs;
constructor(
public readonly hbLogger: Logger,
public readonly config: PlatformConfig,
public readonly api: API,
) {
if (this.config.advancedOptions.logLevel) {
this.logs = new Logs(hbLogger, this.config.advancedOptions.logLevel);
} else {
this.logs = new Logs(hbLogger);
}
//this.logs = getLogger();
this.logs.warn('Finished initializing homebridge-magichome-dynamic-platform %o', loadJson<any>(join(__dirname, '../package.json'), {}).version);
this.logs.info('If this plugin brings you joy, consider visiting GitHub and giving it a ⭐.');
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on(APIEvent.DID_FINISH_LAUNCHING, () => {
this.logs.debug('Executed didFinishLaunching callback');
this.count = 1;
// run the method to discover / register your devices as accessories
this.discoverDevices(true);
// Periodic scan for devices
const shouldRediscover = this.config.advancedOptions?.periodicDiscovery ?? false;
if (shouldRediscover) {
this.periodicDiscovery = setInterval(() => this.discoverDevices(false), 30000);
}
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: MagicHomeAccessory) {
this.logs.debug('%o - Loading accessory from cache...', this.count++, accessory.context.device.displayName);
// set cached accessory as not recently seen
// if found later to be a match with a discovered device, will change to true
accessory.context.device.restartsSinceSeen++;
accessory.context.pendingRegistration = true;
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* Accessories are added by one of three Methods:
* Method One: New devices that were seen after scanning the network and are registered for the first time
* Method Two: Cached devices that were seen after scanning the network and are added while checking for ip discrepancies
* Method Three: Cached devices that were not seen after scanning the network but are still added with a warning to the user
*/
async discoverDevices(dgb: boolean | null) {
const { isValidDeviceModel } = HomebridgeMagichomeDynamicPlatform;
const pendingUpdate = new Set();
const recentlyRegisteredDevices = new Set();
let registeredDevices = 0, newDevices = 0, unseenDevices = 0, scans = 0;
let discover = new Discover(this.logs, this.config);
let devicesDiscovered: IDeviceDiscoveredProps[] = await discover.scan(2000);
while (devicesDiscovered.length === 0 && scans < 5) {
this.logs.debug('( Scan: %o ) Discovered zero devices... rescanning...', scans + 1);
devicesDiscovered = await discover.scan(2000);
scans++;
}
discover = null;
if (devicesDiscovered.length === 0) {
this.logs.debug('\nDiscovered zero devices!\n');
} else {
this.logs.debug('\nDiscovered %o devices.\n', devicesDiscovered.length);
}
// loop over the discovered devices and register each one if it has not already been registered
for (const deviceDiscovered of devicesDiscovered) {
let existingAccessory: MagicHomeAccessory = null;
try {
// generate a unique id for the accessory this should be generated from
const generatedUUID = this.api.hap.uuid.generate(deviceDiscovered.uniqueId);
// check that the device has not already been registered by checking the
// cached devices we stored in the `configureAccessory` method above
existingAccessory = this.accessories.find(accessory => accessory.UUID === generatedUUID);
if (!existingAccessory) {
if (!this.createNewAccessory(deviceDiscovered, generatedUUID)) {
continue;
}
recentlyRegisteredDevices.add(deviceDiscovered.uniqueId);
registeredDevices++;
newDevices++;
} else {
// This deviceDiscovered already exist in cache!
// Check if cached device complies to the device model,
if (!isValidDeviceModel(existingAccessory.context.device, null)) {
//`Device "${uniqueId}" is online, but has outdated data model. Attempting to update it.
this.logs.debug(`The known device "${deviceDiscovered.uniqueId}" seen during discovery has outdated data model (pre v1.8.6). Rebuilding device. `, deviceDiscovered);
// ****** RECONSTRUCT DEVICE - patch existingAccessory with updated data *****
const initialState = await this.getInitialState(deviceDiscovered.ipAddress, 10000);
const deviceQueryData: IDeviceQueriedProps = await this.determineController(deviceDiscovered);
if (!initialState || !deviceQueryData) {
this.logs.error('Warning! Device type could not be determined for device: %o, this is usually due to an unresponsive device.\n Please restart homebridge. If the problem persists, ensure the device works in the "Magichome Pro" app.\n file an issue on github with an uploaded log\n',
deviceDiscovered.uniqueId);
continue;
}
const oldName = existingAccessory.context.displayName ||
existingAccessory.context.device?.lightParameters?.convenientName ||
deviceQueryData.lightParameters.convenientName;
const rootProps = { UUID: generatedUUID, cachedIPAddress: deviceDiscovered.ipAddress, restartsSinceSeen: 0, displayName: oldName, lastKnownState: initialState };
const deviceData: IDeviceProps = Object.assign(rootProps, deviceDiscovered, deviceQueryData);
existingAccessory.context.device = deviceData;
// ****** RECONSTRUCT DEVICE *****
if (isValidDeviceModel(existingAccessory.context.device, this.logs)) {
this.logs.debug(`[discovered+cached] Device "${deviceDiscovered.uniqueId}" successfully repaired!`);
} else {
this.logs.error(`[discovered+cached] Device "${deviceDiscovered.uniqueId}" was not repaired successfully. Ensure it can be controlled in the MagicHome app then restart homebridge to try again while it is online.`);
continue;
}
}
if (!this.registerExistingAccessory(deviceDiscovered, existingAccessory)) {
continue;
}
// add to list of registered devices, so we can show a summary in the end
recentlyRegisteredDevices.add(deviceDiscovered.uniqueId);
registeredDevices++;
}
} catch (error) {
this.logs.debug('[discovered+cached] platform.ts discoverDevices() accessory creation has thrown the following error: %o', error);
this.logs.debug('[discovered+cached] The existingAccessory object is: ', existingAccessory);
this.logs.debug('[discovered+cached] The deviceDiscovered object is: ', deviceDiscovered);
}
}
//=================================================
// End Cached Devices //
//***************** Device Pruning Start *****************//
//if config settings are enabled, devices that are no longer seen
//will be pruned, removing them from the cache. Usefull for removing
//unplugged or unresponsive accessories
for (const accessory of this.accessories) {
try {
if (!isValidDeviceModel(accessory.context.device, null)) {
// only offline, cached devices, old data model, should trigger here.
const { uniqueId } = accessory.context.device;
this.logs.debug(`Device "${uniqueId}" was not seen during discovery. Ensure it can be controlled in the MagicHome app. Rescan in 30 seconds...`);
pendingUpdate.add(uniqueId);
continue;
}
if (accessory.context.device?.displayName && accessory.context.device.displayName.toString().toLowerCase().includes('delete')) {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.logs.warn('Successfully pruned accessory: ', accessory.context.device.displayName,
'due to being marked for deletion\n');
continue;
//if the config parameters for pruning are set to true, prune any devices that haven't been seen
//for more restarts than the accepted ammount
} else if (this.config.pruning.pruneMissingCachedAccessories || this.config.pruning.pruneAllAccessoriesNextRestart) {
if (accessory.context.device.restartsSinceSeen >= this.config.pruning.restartsBeforeMissingAccessoriesPruned || this.config.pruning.pruneAllAccessoriesNextRestart) {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.logs.warn('Successfully pruned accessory:', accessory.context.device.displayName,
'which had not being seen for (', accessory.context.device.restartsSinceSeen, ') restart(s).\n');
continue;
}
}
//simple warning to notify user that their accessory hasn't been seen in n restarts
if (accessory.context.device.restartsSinceSeen > 0) {
//logic for removing blacklisted devices
if (!this.isAllowed(accessory.context.device.uniqueId)) {
this.logs.warn('Warning! Accessory: %o will be pruned as its Unique ID: %o is blacklisted or is not whitelisted.\n',
accessory.context.device.displayName, accessory.context.device.uniqueId);
try {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
} catch (err) {
this.logs.debug('Accessory: %o count not be pruned. Likely it had never been registered.\n',
accessory.context.device.displayName, accessory.context.device.uniqueId);
}
continue;
}
this.logs.debug(`Warning! Continuing to register cached accessory "${accessory.context.device.uniqueId}" despite not being seen for ${accessory.context.device.restartsSinceSeen} restarts.`);
// create the accessory handler
let lightAccessory: HomebridgeMagichomeDynamicPlatformAccessory = null;
try {
lightAccessory = new accessoryType[accessory.context.device.lightParameters.controllerLogicType](this, accessory, this.config);
} catch (error) {
this.logs.error('[1] The controllerLogicType does not exist in accessoryType list. Did you migrate this? controllerLogicType=', accessory.context.device?.lightParameters?.controllerLogicType);
this.logs.error('device object: ', accessory.context.device);
continue;
}
// udpate the accessory to platform
this.api.updatePlatformAccessories([accessory]);
registeredDevices++;
unseenDevices++;
}
} catch (error) {
this.logs.error('platform.ts discoverDevices() accessory pruning has thrown the following error: %o', error);
this.logs.error('The context object is: ', accessory.context);
}
}
this.logs.debug('\nRegistered %o Magichome device(s). \nNew devices: %o \nCached devices that were seen this restart: %o'
+ '\nCached devices that were not seen this restart: %o\n',
registeredDevices,
newDevices,
registeredDevices - newDevices - unseenDevices,
unseenDevices);
// Discovery summary:
if (recentlyRegisteredDevices.size > 0) {
const found = recentlyRegisteredDevices.size;
const pending = Array.from(pendingUpdate).length;
const pendingStr = pending > 0 ? ` Pending update: ${pending} devices` : '';
this.logs.debug(`Discovery summary: Found ${found} devices.${pendingStr}`);
}
this.count = 1; // reset the device logging counter
}//discoveredDevices
isAllowed(uniqueId): boolean {
const blacklistedUniqueIDs = this.config.deviceManagement.blacklistedUniqueIDs;
let isAllowed = true;
try {
if (blacklistedUniqueIDs !== undefined
&& this.config.deviceManagement.blacklistOrWhitelist !== undefined) {
if (((blacklistedUniqueIDs).includes(uniqueId)
&& (this.config.deviceManagement.blacklistOrWhitelist).includes('blacklist'))
|| (!(blacklistedUniqueIDs).includes(uniqueId))
&& (this.config.deviceManagement.blacklistOrWhitelist).includes('whitelist')) {
isAllowed = false;
}
}
} catch (error) {
this.logs.debug(error);
}
return isAllowed;
}
async getInitialState(ipAddress, _timeout = 500) {
const transport = new Transport(ipAddress, this.config);
try {
let scans = 0, data;
while (data == null && scans < 5) {
data = await transport.getState(_timeout);
scans++;
}
return data;
} catch (error) {
this.logs.debug(error);
}
}
async determineController(discoveredDevice): Promise<IDeviceQueriedProps | null> {
const { ipAddress } = discoveredDevice || {};
if (typeof ipAddress !== 'string') {
this.logs.error('Cannot determine controller because invalid IP address. Device:', discoveredDevice);
return null;
}
const initialState = await this.getInitialState(ipAddress, 10000);
if (initialState == undefined) {
this.logs.debug('Cannot determine controller. Device unreacheable.', discoveredDevice);
return null;
}
let lightParameters: ILightParameters;
const controllerHardwareVersion = initialState.controllerHardwareVersion;
const controllerFirmwareVersion = initialState.controllerFirmwareVersion;
this.logs.debug('Attempting to assign controller to new device: UniqueId: %o \nIpAddress %o \nModel: %o\nHardware Version: %o \nDevice Type: %o\n',
discoveredDevice.uniqueId, discoveredDevice.ipAddress, discoveredDevice.modelNumber, initialState.controllerHardwareVersion.toString(16), initialState.controllerFirmwareVersion.toString(16));
//set the lightVersion so that we can give the device a useful name and later know how which protocol to use
if (lightTypesMap.has(controllerHardwareVersion)) {
this.logs.debug('Device %o: Hardware Version: %o with Firmware Version: %o matches known device type records',
discoveredDevice.uniqueId,
controllerHardwareVersion.toString(16),
controllerFirmwareVersion.toString(16));
lightParameters = lightTypesMap.get(controllerHardwareVersion);
} else {
this.logs.warn('Unknown device version number: %o... unable to create accessory.', controllerHardwareVersion.toString(16));
this.logs.warn('Please create an issue at https://github.com/Zacknetic/HomebridgeMagicHome-DynamicPlatform/issues and upload your homebridge.log');
return null;
}
this.logs.debug('Controller Logic Type assigned to %o', lightParameters.controllerLogicType);
return {
lightParameters,
controllerHardwareVersion: controllerHardwareVersion,
controllerFirmwareVersion: controllerFirmwareVersion,
};
}
/**
* Accessory Generation Method One: UUID has not been seen before. Register new accessory.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
* @param deviceDiscovered
* @param generatedUUID
*/
async createNewAccessory(deviceDiscovered: IDeviceDiscoveredProps, generatedUUID): Promise<boolean> {
const unsupportedModels: string[] = ['000-0000']; //AK001-ZJ210 is suported...
const deviceQueryData: IDeviceQueriedProps = await this.determineController(deviceDiscovered);
if (deviceQueryData == null) {
if (unsupportedModels.includes(deviceDiscovered.modelNumber)) {
this.logs.warn('Warning! Discovered device did not respond to query. Device is in the unsupported device list.\nFile an issue on github requesting support. Details:', deviceDiscovered);
} else {
this.logs.warn('Warning! Discovered device did not respond to query. This is usually due to an unresponsive device.\nPlease restart homebridge. If the problem persists, ensure the device works in the "Magichome Pro" app.\nFile an issue on github with an uploaded log.', deviceDiscovered);
}
return false;
}
//check if device is on blacklist or is not on whitelist
if (!this.isAllowed(deviceDiscovered.uniqueId)) {
this.logs.warn('Warning! New device with Unique ID: %o is blacklisted or is not whitelisted.\n',
deviceDiscovered.uniqueId);
return false;
}
// if user has oped, use unique name such as "Bulb AABBCCDD"
if (this.config.advancedOptions && this.config.advancedOptions.namesWithMacAddress) {
const uniqueIdName = getUniqueIdName(deviceDiscovered.uniqueId, deviceQueryData.lightParameters.controllerLogicType);
deviceQueryData.lightParameters.convenientName = uniqueIdName;
}
const accessory = new this.api.platformAccessory(deviceQueryData.lightParameters.convenientName, generatedUUID) as MagicHomeAccessory;
// set its restart prune counter to 0 as it has been seen this session
const deviceData: IDeviceProps = Object.assign({ UUID: generatedUUID, cachedIPAddress: deviceDiscovered.ipAddress, restartsSinceSeen: 0, displayName: deviceQueryData.lightParameters.convenientName }, deviceDiscovered, deviceQueryData);
accessory.context.device = deviceData;
this.printDeviceInfo('Registering new accessory...!', accessory);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
// create the accessory handler
let lightAccessory: HomebridgeMagichomeDynamicPlatformAccessory = null;
try {
lightAccessory = new accessoryType[accessory.context.device.lightParameters.controllerLogicType](this, accessory, this.config);
} catch (error) {
this.logs.error('[2] The controllerLogicType does not exist in accessoryType list. controllerLogicType=', accessory.context.device?.lightParameters?.controllerLogicType);
this.logs.error('device object: ', accessory.context.device);
return false;
}
this.accessories.push(accessory);
return true;
}
/**
* Accessory Generation Method Two: UUID has been seen before. Load from cache.
* Test if seen accessory "is allowed" and that the IP address is identical
* @param deviceDiscovered
* @param existingAccessory
*/
registerExistingAccessory(deviceDiscovered, existingAccessory: MagicHomeAccessory): boolean {
if (!this.isAllowed(existingAccessory.context.device.uniqueId)) {
this.logs.warn('Warning! Accessory: %o will be pruned as its Unique ID: %o is blacklisted or is not whitelisted.\n',
existingAccessory.context.device.displayName, existingAccessory.context.device.uniqueId);
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
return false;
}
// set its restart prune counter to 0 as it has been seen this session
existingAccessory.context.device.restartsSinceSeen = 0;
// test if the existing cached accessory ip address matches the discovered
// accessory ip address if not, replace it
const ipHasNotChanged = existingAccessory.context.device.cachedIPAddress === deviceDiscovered.ipAddress;
const { pendingRegistration } = existingAccessory.context;
const registrationComplete = !pendingRegistration;
if (registrationComplete && ipHasNotChanged) {
this.logs.debug(`Device ${existingAccessory.context.device.uniqueId} already registered. Registration update not required`);
return false;
}
if (!ipHasNotChanged) {
this.logs.warn('Ip address discrepancy found for accessory: %o\n Expected ip address: %o\n Discovered ip address: %o',
existingAccessory.context.device.displayName, existingAccessory.context.device.cachedIPAddress, deviceDiscovered.ipAddress);
// overwrite the ip address of the existing accessory to the newly disovered ip address
existingAccessory.context.device.cachedIPAddress = deviceDiscovered.ipAddress;
this.logs.warn('Ip address successfully reassigned to: %o\n ', existingAccessory.context.device.cachedIPAddress);
}
this.printDeviceInfo('Registering existing accessory: ', existingAccessory);
// create the accessory handler
let lightAccessory: HomebridgeMagichomeDynamicPlatformAccessory = null;
try {
if (!existingAccessory.context?.device?.lightParameters?.controllerLogicType || accessoryType[existingAccessory.context?.device?.lightParameters?.controllerLogicType] === undefined) {
this.logs.error('[registerExistingAccessory] The controllerLogicType does not exist in accessoryType list. controllerLogicType=', existingAccessory.context.device?.lightParameters?.controllerLogicType);
this.logs.error('[registerExistingAccessory] device object: ', existingAccessory.context.device);
return false;
}
lightAccessory = new accessoryType[existingAccessory.context.device.lightParameters.controllerLogicType](this, existingAccessory, this.config);
} catch (error) {
this.logs.error('[registerExistingAccessory] The controllerLogicType does not exist in accessoryType list. controllerLogicType=', existingAccessory.context.device?.lightParameters?.controllerLogicType);
this.logs.error('[registerExistingAccessory] device object: ', existingAccessory.context.device);
this.logs.error(error);
return false;
}
// udpate the accessory to your platform
this.api.updatePlatformAccessories([existingAccessory]);
existingAccessory.context.pendingRegistration = false;
return true;
}
printDeviceInfo(message: string, accessory: MagicHomeAccessory) {
const device = accessory.context.device;
this.logs.info(`${this.count++} - ${message}[ ${device.displayName} ]`);
this.logs.debug(`
Controller Logic Type: ${device.lightParameters?.controllerLogicType}
Model: ${device.modelNumber}
Unique ID: ${device.uniqueId}
IP-Address: ${device.ipAddress}
Hardware Version: ${device.controllerHardwareVersion?.toString(16)}
Firmware Version: ${device.controllerHardwareVersion?.toString(16)}\n`);
}
async send(transport, command: number[], useChecksum = true, _timeout = 200) {
const buffer = Buffer.from(command);
const output = await transport.send(buffer, useChecksum, _timeout);
this.logs.debug('Recived the following response', output);
} //send
static isValidDeviceModel(_device: IDeviceProps, logger): boolean {
const device = cloneDeep(_device);
try {
const { lightParameters } = device || {};
const rootProps = ['UUID', 'cachedIPAddress', 'restartsSinceSeen', 'displayName', 'ipAddress', 'uniqueId', 'modelNumber', 'lightParameters', 'controllerHardwareVersion', 'controllerFirmwareVersion'];
const lightProps = ['controllerLogicType', 'convenientName', 'simultaneousCCT', 'hasColor', 'hasBrightness'];
const missingRootProps = rootProps.filter(k => device[k] === undefined || device[k] == null);
const missingLightProps = lightProps.filter(k => lightParameters[k] === undefined || lightParameters[k] == null);
const missingProps = [...missingRootProps, ...missingLightProps];
// special case: props that can be null: 'lastKnownState'
if (device.lastKnownState === undefined) {
missingProps.push('lastKnownState');
}
if (!Object.values(ControllerTypes).includes(lightParameters.controllerLogicType)) {
if (logger) {
logger.error(`[isValidDeviceModel] The ContollerLogicType "${lightParameters.controllerLogicType}" is unknown.`);
}
return false;
}
if (missingProps.length > 0) {
if (logger) {
logger.error('[isValidDeviceModel] unable to validate device model. Missing properties: ', missingProps);
logger.debug('\nThree things are certain:\nDeath, taxes and lost data.\nGuess which has occurred.');
}
return false;
}
return true;
} catch (err) {
return false;
}
}
}
Example #21
Source File: platform.ts From homebridge-tasmota with Apache License 2.0 | 4 votes |
export class tasmotaPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
private readonly defunctAccessories: PlatformAccessory[] = [];
public readonly services: tasmotaGarageService[] | tasmotaSwitchService[] | tasmotaLightService[] | tasmotaSensorService[] |
tasmotaBinarySensorService[] | tasmotaFanService[] = [];
private discoveryTopicMap: DiscoveryTopicMap[] = [];
private CustomCharacteristic;
// Auto removal of non responding devices
private cleanup: any;
private timeouts = {};
private timeoutCounter = 1;
private debug: any;
public FakeGatoHistoryService;
public teleperiod = 300;
constructor(
public readonly log: Logger,
public readonly config: any,
public readonly api: API,
) {
this.log.debug('Finished initializing platform:', this.config.name);
this.cleanup = this.config['cleanup'] || 24; // Default removal of defunct devices after 24 hours
this.debug = this.config['debug'] || false;
this.teleperiod = this.config['teleperiod'] || 300;
if (this.debug) {
let namespaces = debugEnable.disable();
// this.log("DEBUG-1", namespaces);
if (namespaces) {
namespaces = namespaces + ',Tasmota*';
} else {
namespaces = 'Tasmota*';
}
// this.log("DEBUG-2", namespaces);
debugEnable.enable(namespaces);
}
if (this.config.override) {
interface Injection { key: string, value: any }
interface Injections { topic: string, injection: Injection[] }
const injections: Injections[] = [];
Object.keys(this.config.override).forEach((topic) => {
const inject: Injection[] = [];
Object.entries(this.config.override[topic]).forEach(
([key, value]) => {
// debug("topic: %s, key: %s, value: %s", topic, key, value);
const injection: Injection = { key: key, value: value };
inject.push(injection);
},
);
injections.push({ topic: topic, injection: inject });
});
debug('This is your override reformated to injections.');
debug('"injections": %s\n', JSON.stringify(injections, null, 2));
}
/* eslint-disable */
this.CustomCharacteristic = require('./lib/CustomCharacteristics')(this.Service, this.Characteristic);
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', () => {
log.debug('Executed didFinishLaunching callback');
// run the method to discover / register your devices as accessories
debug('%d accessories for cleanup', this.defunctAccessories.length);
if (this.defunctAccessories.length > 0) {
this.cleanupDefunctAccessories();
}
this.discoverDevices();
if (this.config.history) {
this.FakeGatoHistoryService = fakegato(this.api);
// Only addEntries that match the expected profile of the function.
this.FakeGatoHistoryService.prototype.appendData = function(entry) {
entry.time = Math.round(new Date().valueOf() / 1000);
switch (this.accessoryType) {
default:
// debug('unhandled this.accessoryType', this.accessoryType);
this.addEntry(entry);
}
};
}
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
debug('Load: "%s" %d services', accessory.displayName, accessory.services.length);
if (accessory.services.length > 1) {
// debug('context', accessory.context);
accessory.context.timeout = this.autoCleanup(accessory);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
} else {
this.log.warn('Warning: Removing incomplete accessory definition from cache:', accessory.displayName);
this.defunctAccessories.push(accessory);
}
}
/* Check the topic against the configuration's filterList.
*/
isTopicAllowed(topic: string, filter: string, filterAllow: Array<string>, filterDeny: Array<string>): boolean {
// debug('isTopicFiltered', topic)
let defaultAllow = true;
let allowThis = false;
if (filter) {
defaultAllow = false;
if (topic.match(filter)) {
debug('isTopicFiltered matched filter', filter);
allowThis = true;
}
}
if (filterAllow) {
defaultAllow = false;
for (const filter of filterAllow) {
if (topic.match(filter)) {
debug('isTopicFiltered matched filterAllow entry', filter);
allowThis = true;
}
}
}
if (filterDeny) {
for (const filter of filterDeny) {
if (topic.match(filter)) {
debug('isTopicFiltered matched filterDeny entry', filter);
return false;
}
}
}
return allowThis || defaultAllow;
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices() {
debug('discoverDevices');
// EXAMPLE ONLY
// A real plugin you would discover accessories from the local network, cloud services
// or a user-defined array in the platform config.
const mqttHost = new Mqtt(this.config);
// debug('MqttHost', mqttHost);
mqttHost.on('Remove', (topic) => {
// debug('remove-0', topic);
if (this.discoveryTopicMap[topic]) {
const existingAccessory = this.accessories.find(accessory => accessory.UUID === this.discoveryTopicMap[topic].uuid);
if (existingAccessory) {
// debug('Remove', this.discoveryTopicMap[topic]);
switch (this.discoveryTopicMap[topic].type) {
case 'Service':
this.serviceCleanup(this.discoveryTopicMap[topic].uniq_id, existingAccessory);
break;
case 'Accessory':
this.accessoryCleanup(existingAccessory);
break;
}
delete this.discoveryTopicMap[topic];
} else {
debug('missing accessory', topic, this.discoveryTopicMap[topic]);
}
} else {
// debug('Remove failed', topic);
}
});
mqttHost.on('Discovered', (topic, config) => {
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
// debug('topic', topic);
// debug('filter', this.config.filter);
// debug('filterList', this.config.filterList);
if (this.isTopicAllowed(topic, this.config.filter, this.config.filterAllow, this.config.filterDeny)) {
let message = normalizeMessage(config);
// debug('normalizeMessage ->', message);
if (message.dev && message.dev.ids[0]) {
const identifier = message.dev.ids[0]; // Unique per accessory
const uniq_id = message.uniq_id; // Unique per service
message = this.discoveryOveride(uniq_id, message);
debug('Discovered ->', topic, config.name, message);
const uuid = this.api.hap.uuid.generate(identifier);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
this.log.info('Found existing accessory:', message.name);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
// existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
existingAccessory.context.mqttHost = mqttHost;
existingAccessory.context.device[uniq_id] = message;
existingAccessory.context.identifier = identifier;
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
if (this.services[uniq_id]) {
this.log.warn('Restoring existing service from cache:', message.name);
this.services[uniq_id].refresh();
switch (message.tasmotaType) {
case 'sensor':
if (!message.dev_cla) { // This is the device status topic
this.discoveryTopicMap[topic] = { topic: topic, type: 'Accessory', uniq_id: uniq_id, uuid: uuid };
} else {
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
}
// debug('discoveryTopicMap', this.discoveryTopicMap[topic]);
break;
default:
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
}
} else {
this.log.info('Creating service:', message.name, message.tasmotaType);
switch (message.tasmotaType) {
case 'sensor':
this.services[uniq_id] = new tasmotaSensorService(this, existingAccessory, uniq_id);
if (!message.dev_cla) { // This is the device status topic
this.discoveryTopicMap[topic] = { topic: topic, type: 'Accessory', uniq_id: uniq_id, uuid: uuid };
} else {
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
}
break;
case 'light':
this.services[uniq_id] = new tasmotaLightService(this, existingAccessory, uniq_id);
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
break;
case 'fan':
case 'fanFixed':
this.services[uniq_id] = new tasmotaFanService(this, existingAccessory, uniq_id);
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
break;
case 'switch':
this.services[uniq_id] = new tasmotaSwitchService(this, existingAccessory, uniq_id);
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
break;
case 'garageDoor':
this.services[uniq_id] = new tasmotaGarageService(this, existingAccessory, uniq_id);
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
break;
case 'binary_sensor':
this.services[uniq_id] = new tasmotaBinarySensorService(this, existingAccessory, uniq_id);
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
break;
default:
this.log.warn('Warning: Unhandled Tasmota device type', message.tasmotaType);
}
}
debug('discoveryDevices - this.api.updatePlatformAccessories - %d', existingAccessory.services.length);
this.api.updatePlatformAccessories([existingAccessory]);
} else {
// the accessory does not yet exist, so we need to create it
this.log.info('Adding new accessory:', message.name);
// create a new accessory
const accessory = new this.api.platformAccessory(message.name, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = {};
accessory.context.device[uniq_id] = message;
accessory.context.mqttHost = mqttHost;
accessory.context.identifier = identifier;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
switch (message.tasmotaType) {
case 'switch':
this.services[uniq_id] = new tasmotaSwitchService(this, accessory, uniq_id);
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
break;
case 'garageDoor':
this.services[uniq_id] = new tasmotaGarageService(this, accessory, uniq_id);
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
break;
case 'light':
this.services[uniq_id] = new tasmotaLightService(this, accessory, uniq_id);
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
break;
case 'fan':
this.services[uniq_id] = new tasmotaFanService(this, accessory, uniq_id);
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
break;
case 'sensor':
this.services[uniq_id] = new tasmotaSensorService(this, accessory, uniq_id);
if (!message.dev_cla) { // This is the device status topic
this.discoveryTopicMap[topic] = { topic: topic, type: 'Accessory', uniq_id: uniq_id, uuid: uuid };
} else {
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
}
break;
case 'binary_sensor':
this.services[uniq_id] = new tasmotaBinarySensorService(this, accessory, uniq_id);
this.discoveryTopicMap[topic] = { topic: topic, type: 'Service', uniq_id: uniq_id, uuid: uuid };
break;
default:
this.log.warn('Warning: Unhandled Tasmota device type', message.tasmotaType);
}
debug('discovery devices - this.api.registerPlatformAccessories - %d', accessory.services.length);
if (accessory.services.length > 1) {
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
this.accessories.push(accessory);
} else {
this.log.warn('Warning: incomplete HASS Discovery message and device definition', topic, config.name);
}
}
if (this.services[uniq_id] && this.services[uniq_id].service && this.services[uniq_id].service.getCharacteristic(this.Characteristic.ConfiguredName).listenerCount('set') < 1) {
(this.services[uniq_id].service.getCharacteristic(this.Characteristic.ConfiguredName) ||
this.services[uniq_id].service.addCharacteristic(this.Characteristic.ConfiguredName))
.on('set', setConfiguredName.bind(this.services[uniq_id]));
}
} else {
this.log.warn('Warning: Malformed HASS Discovery message', topic, config.name);
}
} else {
debug('filtered', topic);
}
});
}
discoveryOveride(uniq_id: string, message: any) {
/* eslint-disable */
if (this.config.override) { // pre version 0.1.0 override configuration
// debug('override', this.config.override);
var overrides = [];
for (const [key, value] of Object.entries(this.config.override)) {
// debug(`${key}: ${value}`);
overrides[key] = value;
}
if (overrides[uniq_id]) {
// debug('Merging', this.config.override[uniq_id]);
let merged = { ...message, ...this.config.override[uniq_id] };
// debug('Merged', merged);
return normalizeMessage(merged);
}
} else if (this.config.injections) {
// debug('injections', this.config.injections);
this.config.injections.forEach(overide => {
if (overide.topic === uniq_id) {
overide.injection.forEach(inject => {
message[inject.key] = inject.value;
});
}
});
}
return normalizeMessage(message);
}
serviceCleanup(uniq_id: string, existingAccessory: PlatformAccessory) {
// debug('service array', this.services);
debug('serviceCleanup', uniq_id);
if (this.services[uniq_id]) {
if (this.services[uniq_id].service) {
this.log.info('Removing Service', this.services[uniq_id].service.displayName);
if (this.services[uniq_id].statusSubscribe) {
// debug("Cleaned up listeners", mqttHost);
// debug(this.services[uniq_id].statusSubscribe.event);
if (this.services[uniq_id].statusSubscribe.event) {
existingAccessory.context.mqttHost.removeAllListeners(this.services[uniq_id].statusSubscribe.event);
} else {
this.log.error('statusSubscribe.event missing', this.services[uniq_id].service.displayName);
}
if (this.services[uniq_id].availabilitySubscribe) {
existingAccessory.context.mqttHost.removeAllListeners(this.services[uniq_id].availabilitySubscribe.event);
} else {
this.log.error('availabilitySubscribe missing', this.services[uniq_id].service.displayName);
}
// debug("Cleaned up listeners", existingAccessory.context.mqttHost);
}
existingAccessory.removeService(this.services[uniq_id].service);
delete this.services[uniq_id];
debug('serviceCleanup - this.api.updatePlatformAccessories');
this.api.updatePlatformAccessories([existingAccessory]);
} else {
debug('serviceCleanup - object');
delete this.services[uniq_id];
}
} else {
debug('No service', uniq_id);
}
}
autoCleanup(accessory: PlatformAccessory) {
let timeoutID;
// debug("autoCleanup", accessory.displayName, accessory.context.timeout, this.timeouts);
// debug('autoCleanup \"%s\" topic %s', accessory.displayName, findVal(accessory.context.device, "stat_t"));
if (findVal(accessory.context.device, "stat_t")) {
if (accessory.context.timeout) {
timeoutID = accessory.context.timeout;
clearTimeout(this.timeouts[timeoutID]);
delete this.timeouts[timeoutID];
}
timeoutID = this.timeoutCounter++;
this.timeouts[timeoutID] = setTimeout(this.accessoryCleanup.bind(this), this.cleanup * 60 * 60 * 1000, accessory);
return (timeoutID);
} else {
// debug('autoCleanup unavailable \"%s\" topic %s', accessory.displayName);
return null;
}
}
accessoryCleanup(existingAccessory: PlatformAccessory) {
this.log.info('Removing Accessory', existingAccessory.displayName);
// debug('Services', this.services);
// debug('Accessory', this.discoveryTopicMap[topic]);
// debug('FILTER', this.services.filter(x => x.UUID === this.discoveryTopicMap[topic].uuid));
const services = this.services;
const output: string[] = [];
const uuid = existingAccessory.UUID;
Object.keys(services).some((k: any) => {
// debug(k);
// debug(services[k].accessory.UUID);
if (uuid === services[k].accessory.UUID) {
output.push(k);
// this.serviceCleanup(k);
}
});
output.forEach(element => {
this.serviceCleanup(element, existingAccessory);
});
// Remove accessory
this.accessories.splice(this.accessories.findIndex(accessory => accessory.UUID === existingAccessory.UUID), 1);
// debug('this.timeouts - before', this.timeouts);
clearTimeout(this.timeouts[existingAccessory.context.timeout]);
this.timeouts[existingAccessory.context.timeout] = null;
debug('unregister - this.api.unregisterPlatformAccessories');
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
// debug('this.timeouts - after', this.timeouts);
}
// Remove defunct accessories discovered during startup
cleanupDefunctAccessories() {
this.defunctAccessories.forEach(accessory => {
debug('Removing', accessory.displayName);
this.accessoryCleanup(accessory);
});
}
}
Example #22
Source File: index.ts From homebridge-philips-air with BSD 2-Clause "Simplified" License | 4 votes |
class PhilipsAirPlatform implements DynamicPlatformPlugin {
private readonly log: Logging;
private readonly api: API;
private readonly config: PhilipsAirPlatformConfig;
private readonly timeout: number;
private readonly cachedAccessories: Array<PlatformAccessory> = [];
private readonly purifiers: Map<string, Purifier> = new Map();
private readonly commandQueue: Array<Command> = [];
private queueRunning = false;
enqueuePromise = promisify(this.enqueueCommand);
constructor(log: Logging, config: PlatformConfig, api: API) {
this.log = log;
this.config = config as unknown as PhilipsAirPlatformConfig;
this.api = api;
this.timeout = (this.config.timeout_seconds || 5) * 1000;
api.on(APIEvent.DID_FINISH_LAUNCHING, this.didFinishLaunching.bind(this));
}
configureAccessory(accessory: PlatformAccessory): void {
this.cachedAccessories.push(accessory);
}
didFinishLaunching(): void {
const ips: Array<string> = [];
this.config.devices.forEach((device: DeviceConfig) => {
this.addAccessory(device);
const uuid = hap.uuid.generate(device.ip);
ips.push(uuid);
});
const badAccessories: Array<PlatformAccessory> = [];
this.cachedAccessories.forEach(cachedAcc => {
if (!ips.includes(cachedAcc.UUID)) {
badAccessories.push(cachedAcc);
}
});
this.removeAccessories(badAccessories);
this.purifiers.forEach((purifier) => {
this.enqueueCommand(CommandType.Polling, purifier);
this.enqueueCommand(CommandType.GetFirmware, purifier);
this.enqueueCommand(CommandType.GetStatus, purifier);
this.enqueueCommand(CommandType.GetFilters, purifier);
});
}
async storeKey(purifier: Purifier): Promise<void> {
if (purifier.client && purifier.client instanceof HttpClient) {
purifier.accessory.context.key = (purifier.client as HttpClient).key;
}
}
async setData(purifier: Purifier, values: any, // eslint-disable-line @typescript-eslint/no-explicit-any
callback?: (error?: Error | null | undefined) => void): Promise<void> {
try {
await purifier.client?.setValues(values);
await this.storeKey(purifier);
if (callback) {
callback();
}
} catch (err) {
if (callback) {
callback(err);
}
}
}
async updatePolling(purifier: Purifier): Promise<void> {
try {
// Polling interval
let polling = purifier.config.polling || 60;
if (polling < 60) {
polling = 60;
}
setInterval(function() {
exec('python3 ' + pathTopyaircontrol + ' --ipaddr ' + purifier.config.ip + ' --protocol ' + purifier.config.protocol + ' --status', (error, stdout, stderr) => {
if (error || stderr) {
console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[36m[Philips Air] \x1b[31m[' + purifier.config.name + '] Unable to get data for polling: Error: spawnSync python3 ETIMEDOUT.\x1b[0m');
console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[33mIf your have "Error: spawnSync python3 ETIMEDOUT" your need unplug the accessory from outlet for 10 seconds and plug again.\x1b[0m');
}
if (error || stderr || error && stderr) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
stdout = {om: localStorage.getItem('om'), pwr: localStorage.getItem('pwr'), cl: false, aqil: localStorage.getItem('aqil'), uil: localStorage.getItem('uil'), dt: 0, dtrs: 0, mode: localStorage.getItem('mode'), func: localStorage.getItem('func'), rhset: localStorage.getItem('rhset'), 'rh': localStorage.getItem('rh'), 'temp': localStorage.getItem('temp'), pm25: localStorage.getItem('pm25'), iaql: localStorage.getItem('iaql'), aqit: 4, ddp: '1', rddp: localStorage.getItem('rddp'), err: 0, wl: localStorage.getItem('wl'), fltt1: localStorage.getItem('fltt1'), fltt2: localStorage.getItem('fltt2'), fltsts0: localStorage.getItem('fltsts0'), fltsts1: localStorage.getItem('fltsts1'), fltsts2: localStorage.getItem('fltsts2'), wicksts: localStorage.getItem('wicksts')};
stdout = JSON.stringify(stdout);
}
const obj = JSON.parse(stdout);
if (!error || !stderr || !error && !stderr) {
localStorage.setItem('pwr', obj.pwr);
localStorage.setItem('om', obj.om);
localStorage.setItem('aqil', obj.aqil);
localStorage.setItem('uil', obj.uil);
localStorage.setItem('mode', obj.mode);
localStorage.setItem('func', obj.func);
localStorage.setItem('rhset', obj.rhset);
localStorage.setItem('iaql', obj.iaql);
localStorage.setItem('pm25', obj.pm25);
localStorage.setItem('rh', obj.rh);
localStorage.setItem('temp', obj.temp);
localStorage.setItem('rddp', obj.rddp);
localStorage.setItem('wl', obj.wl);
localStorage.setItem('fltt1', obj.fltt1);
localStorage.setItem('fltt2', obj.fltt2);
localStorage.setItem('fltsts0', obj.fltsts0);
localStorage.setItem('fltsts1', obj.fltsts1);
localStorage.setItem('fltsts2', obj.fltsts2);
localStorage.setItem('wicksts', obj.wicksts);
} else {
localStorage.setItem('pwr', 1);
localStorage.setItem('om', '0');
localStorage.setItem('aqil', '0');
localStorage.setItem('uil', 0);
localStorage.setItem('mode', 'A');
localStorage.setItem('func', 'PH');
localStorage.setItem('rhset', 50);
localStorage.setItem('iaql', 1);
localStorage.setItem('pm25', 1);
localStorage.setItem('rh', 45);
localStorage.setItem('temp', 25);
localStorage.setItem('rddp', 1);
localStorage.setItem('wl', 100);
localStorage.setItem('fltt1', 'A3');
localStorage.setItem('fltt2', 'C7');
localStorage.setItem('fltsts0', 287);
localStorage.setItem('fltsts1', 2553);
localStorage.setItem('fltsts2', 2553);
localStorage.setItem('wicksts', 4005);
}
const purifierService = purifier.accessory.getService(hap.Service.AirPurifier);
if (purifierService) {
const state = parseInt(obj.pwr) * 2;
purifierService
.updateCharacteristic(hap.Characteristic.Active, obj.pwr)
.updateCharacteristic(hap.Characteristic.CurrentAirPurifierState, state)
.updateCharacteristic(hap.Characteristic.LockPhysicalControls, obj.cl);
}
const qualityService = purifier.accessory.getService(hap.Service.AirQualitySensor);
if (qualityService) {
const iaql = Math.ceil(obj.iaql / 3);
qualityService
.updateCharacteristic(hap.Characteristic.AirQuality, iaql)
.updateCharacteristic(hap.Characteristic.PM2_5Density, obj.pm25);
}
if (purifier.config.temperature_sensor) {
const temperature_sensor = purifier.accessory.getService('Temperature');
if (temperature_sensor) {
temperature_sensor.updateCharacteristic(hap.Characteristic.CurrentTemperature, obj.temp);
}
}
if (purifier.config.humidity_sensor) {
const humidity_sensor = purifier.accessory.getService('Humidity');
if (humidity_sensor) {
humidity_sensor.updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, obj.rh);
}
}
if (purifier.config.light_control) {
const lightsService = purifier.accessory.getService('Lights');
if (obj.pwr == '1') {
if (lightsService) {
lightsService
.updateCharacteristic(hap.Characteristic.On, obj.aqil > 0)
.updateCharacteristic(hap.Characteristic.Brightness, obj.aqil);
}
}
}
if (purifier.config.humidifier) {
let water_level = 100;
let speed_humidity = 0;
if (obj.func == 'PH' && obj.wl == 0) {
water_level = 0;
}
if (obj.pwr == '1') {
if (obj.func == 'PH' && water_level == 100) {
if (obj.rhset == 40) {
speed_humidity = 25;
} else if (obj.rhset == 50) {
speed_humidity = 50;
} else if (obj.rhset == 60) {
speed_humidity = 75;
} else if (obj.rhset == 70) {
speed_humidity = 100;
}
}
}
const Humidifier = purifier.accessory.getService('Humidifier');
if (Humidifier) {
Humidifier
.updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, obj.rh)
.updateCharacteristic(hap.Characteristic.WaterLevel, water_level)
.updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1)
.updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity);
if (water_level == 0) {
if (obj.func != 'P') {
exec('airctrl --ipaddr ' + purifier.config.ip + ' --protocol ' + purifier.config.protocol + ' --func P', (error, stdout, stderr) => {
if (error || stderr) {
console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[36m[Philips Air] \x1b[31m[' + purifier.config.name + '] Unable to get data for polling: Error: spawnSync python3 ETIMEDOUT.\x1b[0m');
console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[33mIf your have "Error: spawnSync python3 ETIMEDOUT" your need unplug the accessory from outlet for 10 seconds and plug again.\x1b[0m');
}
});
}
Humidifier
.updateCharacteristic(hap.Characteristic.Active, 0)
.updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 0)
.updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, 0);
}
}
}
if (purifier.config.logger) {
if (purifier.config.temperature_sensor) {
if (!error || !stderr || !error && !stderr) {
const logger_temp = fs.createWriteStream(pathToSensorFiles + 'temp.txt', {
flags: 'w'
});
logger_temp.write(obj.temp.toString());
logger_temp.end();
}
}
if (purifier.config.humidity_sensor) {
if (!error || !stderr || !error && !stderr) {
const logger_hum = fs.createWriteStream(pathToSensorFiles + 'hum.txt', {
flags: 'w'
});
logger_hum.write(obj.rh.toString());
logger_hum.end();
}
}
}
});
}, polling * 1000);
} catch (err) {
this.log.error('[' + purifier.config.name + '] Unable to load polling info');
}
}
async updateFirmware(purifier: Purifier): Promise<void> {
try {
purifier.lastfirmware = Date.now();
const firmware: PurifierFirmware = await purifier.client?.getFirmware();
await this.storeKey(purifier);
const accInfo = purifier.accessory.getService(hap.Service.AccessoryInformation);
if (accInfo) {
const name = firmware.modelid;
accInfo
.updateCharacteristic(hap.Characteristic.Manufacturer, 'Philips')
.updateCharacteristic(hap.Characteristic.SerialNumber, purifier.config.ip)
.updateCharacteristic(hap.Characteristic.Model, name)
.updateCharacteristic(hap.Characteristic.FirmwareRevision, firmware.version);
}
} catch (err) {
this.log.error('[' + purifier.config.name + '] Unable to load firmware info: ' + err);
}
}
async updateFilters(purifier: Purifier): Promise<void> {
try {
const filters: PurifierFilters = await purifier.client?.getFilters();
purifier.lastfilters = Date.now();
await this.storeKey(purifier);
const preFilter = purifier.accessory.getService('Pre-filter');
if (preFilter) {
const fltsts0change = filters.fltsts0 == 0;
const fltsts0life = filters.fltsts0 / 360 * 100;
preFilter
.updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltsts0change)
.updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltsts0life);
}
const carbonFilter = purifier.accessory.getService('Active carbon filter');
if (carbonFilter) {
const fltsts2change = filters.fltsts2 == 0;
const fltsts2life = filters.fltsts2 / 4800 * 100;
carbonFilter
.updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltsts2change)
.updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltsts2life);
}
const hepaFilter = purifier.accessory.getService('HEPA filter');
if (hepaFilter) {
const fltsts1change = filters.fltsts1 == 0;
const fltsts1life = filters.fltsts1 / 4800 * 100;
hepaFilter
.updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltsts1change)
.updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltsts1life);
}
if (purifier.config.humidifier) {
const wickFilter = purifier.accessory.getService('Wick filter');
if (wickFilter) {
const fltwickchange = filters.wicksts == 0;
const fltwicklife = Math.round(filters.wicksts / 4800 * 100);
wickFilter
.updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltwickchange)
.updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltwicklife);
}
}
} catch (err) {
this.log.error('[' + purifier.config.name + '] Unable to load filter info: ' + err);
}
}
async updateStatus(purifier: Purifier): Promise<void> {
try {
const status: PurifierStatus = await purifier.client?.getStatus();
purifier.laststatus = Date.now();
await this.storeKey(purifier);
const purifierService = purifier.accessory.getService(hap.Service.AirPurifier);
if (purifierService) {
const state = parseInt(status.pwr) * 2;
purifierService
.updateCharacteristic(hap.Characteristic.Active, status.pwr)
.updateCharacteristic(hap.Characteristic.CurrentAirPurifierState, state)
.updateCharacteristic(hap.Characteristic.LockPhysicalControls, status.cl);
}
const qualityService = purifier.accessory.getService(hap.Service.AirQualitySensor);
if (qualityService) {
const iaql = Math.ceil(status.iaql / 3);
qualityService
.updateCharacteristic(hap.Characteristic.AirQuality, iaql)
.updateCharacteristic(hap.Characteristic.PM2_5Density, status.pm25);
}
if (purifier.config.temperature_sensor) {
const temperature_sensor = purifier.accessory.getService('Temperature');
if (temperature_sensor) {
temperature_sensor.updateCharacteristic(hap.Characteristic.CurrentTemperature, status.temp);
}
}
if (purifier.config.humidity_sensor) {
const humidity_sensor = purifier.accessory.getService('Humidity');
if (humidity_sensor) {
humidity_sensor.updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, status.rh);
}
}
if (purifier.config.light_control) {
const lightsService = purifier.accessory.getService('Lights');
if (status.pwr == '1') {
if (lightsService) {
lightsService
.updateCharacteristic(hap.Characteristic.On, status.aqil > 0)
.updateCharacteristic(hap.Characteristic.Brightness, status.aqil);
}
}
}
if (purifier.config.humidifier) {
const Humidifier = purifier.accessory.getService('Humidifier');
if (Humidifier) {
let speed_humidity = 0;
let state_ph = 0;
let water_level = 100;
if (status.func == 'PH' && status.wl == 0) {
water_level = 0;
}
if (status.pwr == '1') {
if (status.func == 'PH' && water_level == 100) {
state_ph = 1;
if (status.rhset == 40) {
speed_humidity = 25;
} else if (status.rhset == 50) {
speed_humidity = 50;
} else if (status.rhset == 60) {
speed_humidity = 75;
} else if (status.rhset == 70) {
speed_humidity = 100;
}
}
}
Humidifier
.updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, status.rh)
.updateCharacteristic(hap.Characteristic.WaterLevel, water_level)
.updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1);
if (state_ph && status.rhset >= 40) {
Humidifier
.updateCharacteristic(hap.Characteristic.Active, state_ph)
.updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, state_ph * 2)
.updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity);
}
if (water_level == 0) {
if (status.func != 'P') {
exec('airctrl --ipaddr ' + purifier.config.ip + ' --protocol ' + purifier.config.protocol + ' --func P', (err, stdout, stderr) => {
if (err) {
return;
}
if (stderr) {
console.error('Unable to switch off purifier ' + stderr + '. If your have "sync timeout" error your need unplug the accessory from the outlet for 10 seconds.');
}
});
}
}
}
}
} catch (err) {
this.log.error('[' + purifier.config.name + '] Unable to load status info: ' + err);
}
}
async setPower(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> {
const purifier = this.purifiers.get(accessory.displayName);
if (purifier) {
const values = {
pwr: (state as boolean).toString()
};
try {
const status: PurifierStatus = await purifier.client?.getStatus();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
purifier.laststatus = Date.now();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.storeKey(purifier);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
let water_level = 100;
if (status.func == 'PH' && status.wl == 0) {
water_level = 0;
}
const purifierService = accessory.getService(hap.Service.AirPurifier);
if (purifierService) {
purifierService.updateCharacteristic(hap.Characteristic.CurrentAirPurifierState, state as number * 2);
}
if (purifier.config.humidifier) {
if (water_level == 0) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
values['func'] = 'P';
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.enqueuePromise(CommandType.SetData, purifier, values);
if (purifier.config.light_control) {
const lightsService = accessory.getService('Lights');
if (lightsService) {
if (state) {
lightsService
.updateCharacteristic(hap.Characteristic.On, status.aqil > 0)
.updateCharacteristic(hap.Characteristic.Brightness, status.aqil);
} else {
lightsService
.updateCharacteristic(hap.Characteristic.On, 0)
.updateCharacteristic(hap.Characteristic.Brightness, 0);
}
}
}
if (purifier.config.humidifier) {
const Humidifier = accessory.getService('Humidifier');
let state_ph = 0;
if (status.func == 'PH' && water_level == 100) {
state_ph = 1;
}
if (Humidifier) {
Humidifier.updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1);
if (state) {
Humidifier
.updateCharacteristic(hap.Characteristic.Active, state_ph)
.updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, state_ph * 2);
} else {
Humidifier
.updateCharacteristic(hap.Characteristic.Active, 0)
.updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 0)
.updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, 0);
}
}
}
} catch (err) {
this.log.error('[' + purifier.config.name + '] Error setting power: ' + err);
}
}
}
async setBrightness(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> {
const purifier = this.purifiers.get(accessory.displayName);
if (purifier) {
const values = {
aqil: state,
uil: state ? '1' : '0'
};
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.enqueuePromise(CommandType.SetData, purifier, values);
} catch (err) {
this.log.error('[' + purifier.config.name + '] Error setting brightness: ' + err);
}
}
}
async setMode(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> {
const purifier = this.purifiers.get(accessory.displayName);
if (purifier) {
const values = {
mode: state ? 'P' : 'M'
};
if (purifier.config.allergic_func) {
values.mode = state ? 'P' : 'A';
} else {
values.mode = state ? 'P' : 'M';
}
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.enqueuePromise(CommandType.SetData, purifier, values);
if (state != 0) {
const purifierService = accessory.getService(hap.Service.AirPurifier);
if (purifierService) {
purifierService
.updateCharacteristic(hap.Characteristic.RotationSpeed, 0)
.updateCharacteristic(hap.Characteristic.TargetAirPurifierState, state);
}
}
} catch (err) {
this.log.error('[' + purifier.config.name + '] Error setting mode: ' + err);
}
}
}
async setLock(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> {
const purifier = this.purifiers.get(accessory.displayName);
if (purifier) {
const values = {
cl: state == 1
};
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.enqueuePromise(CommandType.SetData, purifier, values);
} catch (err) {
this.log.error('[' + purifier.config.name + '] Error setting lock: ' + err);
}
}
}
async setHumidity(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> {
const purifier = this.purifiers.get(accessory.displayName);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const status: PurifierStatus = await purifier.client?.getStatus();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
purifier.laststatus = Date.now();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.storeKey(purifier);
const Humidifier = accessory.getService('Humidifier');
if (purifier) {
const values = {
func: state ? 'PH' : 'P'
};
let water_level = 100;
if (status.func == 'PH' && status.wl == 0) {
water_level = 0;
}
let speed_humidity = 0;
let state_ph = 0;
if (status.func == 'PH' && water_level == 100) {
state_ph = 1;
if (status.rhset == 40) {
speed_humidity = 25;
} else if (status.rhset == 50) {
speed_humidity = 50;
} else if (status.rhset == 60) {
speed_humidity = 75;
} else if (status.rhset == 70) {
speed_humidity = 100;
}
}
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.enqueuePromise(CommandType.SetData, purifier, values);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Humidifier.updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1);
if (state) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Humidifier
.updateCharacteristic(hap.Characteristic.Active, 1)
.updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, state_ph * 2)
.updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Humidifier
.updateCharacteristic(hap.Characteristic.Active, 0)
.updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 0)
.updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, 0);
}
} catch (err) {
this.log.error('[' + purifier.config.name + '] Error setting func: ' + err);
}
}
}
async setHumidityTarget(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> {
const purifier = this.purifiers.get(accessory.displayName);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const status: PurifierStatus = await purifier.client?.getStatus();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
purifier.laststatus = Date.now();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.storeKey(purifier);
const Humidifier = accessory.getService('Humidifier');
if (purifier) {
const speed = state;
const values = {
func: state ? 'PH' : 'P',
rhset: 40
};
let speed_humidity = 0;
if (speed > 0 && speed <= 25) {
values.rhset = 40;
speed_humidity = 25;
} else if (speed > 25 && speed <= 50) {
values.rhset = 50;
speed_humidity = 50;
} else if (speed > 50 && speed <= 75) {
values.rhset = 60;
speed_humidity = 75;
} else if (speed > 75 && speed <= 100) {
values.rhset = 70;
speed_humidity = 100;
}
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.enqueuePromise(CommandType.SetData, purifier, values);
if (Humidifier) {
let water_level = 100;
if (status.func == 'PH' && status.wl == 0) {
water_level = 0;
}
Humidifier.updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1);
if (speed_humidity > 0) {
Humidifier
.updateCharacteristic(hap.Characteristic.Active, 1)
.updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 2)
.updateCharacteristic(hap.Characteristic.WaterLevel, water_level)
.updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity);
} else {
Humidifier
.updateCharacteristic(hap.Characteristic.Active, 0);
}
}
} catch (err) {
this.log.error('[' + purifier.config.name + '] Error setting humidifier: ' + err);
}
}
}
async setFan(accessory: PlatformAccessory, state: CharacteristicValue): Promise<void> {
const purifier = this.purifiers.get(accessory.displayName);
if (purifier) {
let divisor = 25;
let offset = 0;
if (purifier.config.sleep_speed) {
divisor = 20;
offset = 1;
}
const speed = Math.ceil(state as number / divisor);
if (speed > 0) {
const values = {
mode: 'M',
om: ''
};
if (offset == 1 && speed == 1) {
values.om = 's';
} else if (speed < 4 + offset) {
values.om = (speed - offset).toString();
} else {
values.om = 't';
}
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.enqueuePromise(CommandType.SetData, purifier, values);
const service = accessory.getService(hap.Service.AirPurifier);
if (service) {
service.updateCharacteristic(hap.Characteristic.TargetAirPurifierState, 0);
}
if (purifier.timeout) {
clearTimeout(purifier.timeout);
}
purifier.timeout = setTimeout(() => {
if (service) {
service.updateCharacteristic(hap.Characteristic.RotationSpeed, speed * divisor);
}
purifier.timeout = undefined;
}, 1000);
} catch (err) {
this.log.error('[' + purifier.config.name + '] Error setting fan: ' + err);
}
}
}
}
addAccessory(config: DeviceConfig): void {
this.log('[' + config.name + '] Initializing accessory...');
const uuid = hap.uuid.generate(config.ip);
let accessory = this.cachedAccessories.find(cachedAcc => {
return cachedAcc.UUID == uuid;
});
if (!accessory) {
accessory = new Accessory(config.name, uuid);
accessory.addService(hap.Service.AirPurifier, config.name);
accessory.addService(hap.Service.AirQualitySensor, 'Air quality', 'Air quality');
if (config.light_control) {
accessory.addService(hap.Service.Lightbulb, 'Lights', 'Lights')
.addCharacteristic(hap.Characteristic.Brightness);
}
accessory.addService(hap.Service.FilterMaintenance, 'Pre-filter', 'Pre-filter');
accessory.addService(hap.Service.FilterMaintenance, 'Active carbon filter', 'Active carbon filter');
accessory.addService(hap.Service.FilterMaintenance, 'HEPA filter', 'HEPA filter');
if (config.temperature_sensor) {
accessory.addService(hap.Service.TemperatureSensor, 'Temperature', 'Temperature');
}
if (config.humidity_sensor) {
accessory.addService(hap.Service.HumiditySensor, 'Humidity', 'Humidity');
}
if (config.humidifier) {
accessory.addService(hap.Service.HumidifierDehumidifier, 'Humidifier', 'Humidifier');
accessory.addService(hap.Service.FilterMaintenance, 'Wick filter', 'Wick filter');
}
this.api.registerPlatformAccessories('homebridge-philips-air', 'philipsAir', [accessory]);
} else {
let lightsService = accessory.getService('Lights');
if (config.light_control) {
if (lightsService == undefined) {
lightsService = accessory.addService(hap.Service.Lightbulb, 'Lights', 'Lights');
lightsService.addCharacteristic(hap.Characteristic.Brightness);
}
} else if (lightsService != undefined) {
accessory.removeService(lightsService);
}
const temperature_sensor = accessory.getService('Temperature');
if (config.temperature_sensor) {
if (temperature_sensor == undefined) {
accessory.addService(hap.Service.TemperatureSensor, 'Temperature', 'Temperature');
}
} else if (temperature_sensor != undefined) {
accessory.removeService(temperature_sensor);
}
const humidity_sensor = accessory.getService('Humidity');
if (config.humidity_sensor) {
if (humidity_sensor == undefined) {
accessory.addService(hap.Service.HumiditySensor, 'Humidity', 'Humidity');
}
} else if (humidity_sensor != undefined) {
accessory.removeService(humidity_sensor);
}
const Humidifier = accessory.getService('Humidifier');
if (config.humidifier) {
if (Humidifier == undefined) {
accessory.addService(hap.Service.HumidifierDehumidifier, 'Humidifier', 'Humidifier');
}
} else if (Humidifier != undefined) {
accessory.removeService(Humidifier);
}
}
this.setService(accessory, config);
let client: AirClient;
switch (config.protocol) {
case 'coap':
client = new CoapClient(config.ip, this.timeout);
break;
case 'plain_coap':
client = new PlainCoapClient(config.ip, this.timeout);
break;
case 'http_legacy':
client = new HttpClientLegacy(config.ip, this.timeout);
break;
case 'http':
default:
if (accessory.context.key) {
client = new HttpClient(config.ip, this.timeout, accessory.context.key);
} else {
client = new HttpClient(config.ip, this.timeout);
}
}
this.purifiers.set(accessory.displayName, {
accessory: accessory,
client: client,
config: config
});
}
removeAccessories(accessories: Array<PlatformAccessory>): void {
accessories.forEach(accessory => {
this.log('[' + accessory.displayName + '] Removed from Homebridge.');
this.api.unregisterPlatformAccessories('homebridge-philips-air', 'philipsAir', [accessory]);
});
}
setService(accessory: PlatformAccessory, config: DeviceConfig): void {
accessory.on(PlatformAccessoryEvent.IDENTIFY, () => {
this.log('[' + accessory.displayName + '] Identify requested.');
});
const purifierService = accessory.getService(hap.Service.AirPurifier);
let min_step_purifier_speed = 25;
if (config.sleep_speed) {
min_step_purifier_speed = 20;
}
if (purifierService) {
purifierService
.getCharacteristic(hap.Characteristic.Active)
.on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => {
try {
await this.setPower(accessory, state);
callback();
} catch (err) {
callback(err);
}
});
purifierService
.getCharacteristic(hap.Characteristic.TargetAirPurifierState)
.on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => {
try {
await this.setMode(accessory, state);
callback();
} catch (err) {
callback(err);
}
});
purifierService
.getCharacteristic(hap.Characteristic.LockPhysicalControls)
.on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => {
try {
await this.setLock(accessory, state);
callback();
} catch (err) {
callback(err);
}
});
purifierService
.getCharacteristic(hap.Characteristic.RotationSpeed)
.on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => {
try {
await this.setFan(accessory, state);
callback();
} catch (err) {
callback(err);
}
}).setProps({
minValue: 0,
maxValue: 100,
minStep: min_step_purifier_speed
});
}
if (config.light_control) {
const lightService = accessory.getService('Lights');
if (lightService) {
lightService
.getCharacteristic(hap.Characteristic.Brightness)
.on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => {
try {
await this.setBrightness(accessory, state);
callback();
} catch (err) {
callback(err);
}
}).setProps({
minValue: 0,
maxValue: 100,
minStep: 25
});
}
}
if (config.humidifier) {
const Humidifier = accessory.getService('Humidifier');
if (Humidifier) {
Humidifier
.getCharacteristic(hap.Characteristic.Active)
.on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => {
try {
await this.setHumidity(accessory, state);
callback();
} catch (err) {
callback(err);
}
});
Humidifier
.getCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState)
.on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => {
try {
await this.setHumidityTarget(accessory, state);
await this.setHumidity(accessory, state);
callback();
} catch (err) {
callback(err);
}
}).setProps({
validValues: [
hap.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE,
hap.Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING
]
});
Humidifier
.getCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState)
.on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => {
try {
await this.setHumidityTarget(accessory, state);
await this.setHumidity(accessory, state);
callback();
} catch (err) {
callback(err);
}
}).setProps({
validValues: [
hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER
]
});
Humidifier
.getCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold)
.on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => {
try {
await this.setHumidityTarget(accessory, state);
callback();
} catch (err) {
callback(err);
}
}).setProps({
minValue: 0,
maxValue: 100,
minStep: 25
});
}
}
}
enqueueAccessory(commandType: CommandType, accessory: PlatformAccessory): void {
const purifier = this.purifiers.get(accessory.displayName);
if (purifier) {
this.enqueueCommand(commandType, purifier);
}
}
enqueueCommand(commandType: CommandType, purifier: Purifier, data?: any, // eslint-disable-line @typescript-eslint/no-explicit-any
callback?: (error?: Error | null | undefined) => void): void {
if (commandType != CommandType.SetData) {
const exists = this.commandQueue.find((command) => {
return command.purifier.config.ip == purifier.config.ip && command.type == commandType;
});
if (exists) {
return; // Don't enqueue commands we already have in the queue
}
}
this.commandQueue.push({
purifier: purifier,
type: commandType,
callback: callback,
data: data
});
if (!this.queueRunning) {
this.queueRunning = true;
this.nextCommand();
}
}
nextCommand(): void {
const todoItem = this.commandQueue.shift();
if (!todoItem) {
return;
}
let command;
switch (todoItem.type) {
case CommandType.Polling:
command = this.updatePolling(todoItem.purifier);
break;
case CommandType.GetFirmware:
command = this.updateFirmware(todoItem.purifier);
break;
case CommandType.GetFilters:
command = this.updateFilters(todoItem.purifier);
break;
case CommandType.GetStatus:
command = this.updateStatus(todoItem.purifier);
break;
case CommandType.SetData:
command = this.setData(todoItem.purifier, todoItem.data, todoItem.callback);
}
command.then(() => {
if (this.commandQueue.length > 0) {
this.nextCommand();
} else {
this.queueRunning = false;
}
});
}
}
Example #23
Source File: index.ts From homebridge-nest-cam with GNU General Public License v3.0 | 4 votes |
class NestCamPlatform implements DynamicPlatformPlugin {
private readonly log: Logging;
private readonly api: API;
private config: NestConfig;
private options: Options;
private readonly nestObjects: Array<NestObject> = [];
private structures: Array<string> = [];
private cameras: Array<string> = [];
constructor(log: Logging, config: PlatformConfig, api: API) {
this.log = log;
this.api = api;
this.config = config as NestConfig;
this.options = new Options();
// Need a config or plugin will not start
if (!config) {
return;
}
this.initDefaultOptions();
api.on(APIEvent.DID_FINISH_LAUNCHING, this.didFinishLaunching.bind(this));
api.on(APIEvent.SHUTDOWN, this.isShuttingDown.bind(this));
}
private initDefaultOptions(): void {
// Setup boolean options
Object.keys(this.options).forEach((opt) => {
const key = opt as keyof Options;
if (this.config.options) {
const configVal = this.config.options[key];
if (typeof configVal === 'undefined') {
this.options[key] = true;
this.log.debug(`Defaulting ${key} to true`);
} else {
this.options[key] = configVal;
this.log.debug(`Using ${key} from config: ${configVal}`);
}
}
});
const structures = this.config.options?.structures;
if (typeof structures !== 'undefined') {
this.log.debug(`Using structures from config: ${structures}`);
this.structures = structures;
} else {
this.log.debug('Defaulting structures to []');
}
const cameras = this.config.options?.cameras;
if (typeof cameras !== 'undefined') {
this.log.debug(`Using cameras from config: ${cameras}`);
this.cameras = cameras;
} else {
this.log.debug('Defaulting cameras to []');
}
}
configureAccessory(accessory: PlatformAccessory<Record<string, CameraInfo>>): void {
this.log.info(`Configuring accessory ${accessory.displayName}`);
accessory.on(PlatformAccessoryEvent.IDENTIFY, () => {
this.log.info(`${accessory.displayName} identified!`);
});
const cameraInfo = accessory.context.cameraInfo;
const camera = new NestCam(this.config, cameraInfo, this.log);
const nestAccessory = new NestAccessory(accessory, camera, this.config, this.log, hap);
nestAccessory.configureController();
// Microphone configuration
if (camera.info.capabilities.includes('audio.microphone')) {
nestAccessory.createService(hap.Service.Microphone);
nestAccessory.createService(hap.Service.Speaker);
this.log.debug(`Creating microphone for ${accessory.displayName}.`);
}
// Doorbell configuration
if (camera.info.capabilities.includes('indoor_chime') && this.options.doorbellAlerts) {
nestAccessory.createService(hap.Service.Doorbell, 'Doorbell');
this.log.debug(`Creating doorbell sensor for ${accessory.displayName}.`);
camera.startAlertChecks();
} else {
nestAccessory.removeService(hap.Service.Doorbell, 'Doorbell');
}
// Add doorbell switch
if (
camera.info.capabilities.includes('indoor_chime') &&
this.options.doorbellAlerts &&
this.options.doorbellSwitch
) {
const service = nestAccessory.createService(hap.Service.StatelessProgrammableSwitch, 'DoorbellSwitch');
this.log.debug(`Creating doorbell switch for ${accessory.displayName}.`);
service.getCharacteristic(hap.Characteristic.ProgrammableSwitchEvent).setProps({
maxValue: hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS,
});
} else {
nestAccessory.removeService(hap.Service.StatelessProgrammableSwitch, 'DoorbellSwitch');
}
// Streaming switch configuration
if (camera.info.capabilities.includes('streaming.start-stop') && this.options.streamingSwitch) {
nestAccessory.createSwitchService('Streaming', hap.Service.Switch, 'streaming.enabled', async (value) => {
await nestAccessory.toggleActive(value as boolean);
});
} else {
nestAccessory.removeService(hap.Service.Switch, 'Streaming');
}
// Chime switch configuration
if (camera.info.capabilities.includes('indoor_chime') && this.options.chimeSwitch) {
nestAccessory.createSwitchService('Chime', hap.Service.Switch, 'doorbell.indoor_chime.enabled', async (value) => {
await nestAccessory.toggleChime(value as boolean);
});
} else {
nestAccessory.removeService(hap.Service.Switch, 'Chime');
}
// Announcements switch configuration
if (camera.info.capabilities.includes('indoor_chime') && this.options.announcementsSwitch) {
nestAccessory.createSwitchService(
'Announcements',
hap.Service.Switch,
'doorbell.chime_assist.enabled',
async (value) => {
await nestAccessory.toggleAnnouncements(value as boolean);
},
);
} else {
nestAccessory.removeService(hap.Service.Switch, 'Announcements');
}
// Audio switch configuration
if (camera.info.capabilities.includes('audio.microphone') && this.options.audioSwitch) {
nestAccessory.createSwitchService('Audio', hap.Service.Switch, 'audio.enabled', async (value) => {
await nestAccessory.toggleAudio(value as boolean);
});
} else {
nestAccessory.removeService(hap.Service.Switch, 'Audio');
}
//Update Firmware Revision
const accessoryInformation = accessory.getService(hap.Service.AccessoryInformation);
if (accessoryInformation) {
accessoryInformation.setCharacteristic(hap.Characteristic.FirmwareRevision, cameraInfo.combined_software_version);
}
this.nestObjects.push({ accessory: accessory, camera: camera });
}
private async setupMotionServices(): Promise<void> {
this.nestObjects.forEach(async (obj) => {
const camera = obj.camera;
const accessory = obj.accessory;
if (accessory) {
const nestAccessory = new NestAccessory(accessory, camera, this.config, this.log, hap);
if (this.options.motionDetection) {
// Motion configuration
const services = nestAccessory.getServicesByType(hap.Service.MotionSensor);
const alertTypes = await camera.getAlertTypes();
// Remove invalid services
const invalidServices = services.filter((x) => !alertTypes.includes(x.displayName));
for (const service of invalidServices) {
accessory.removeService(service);
}
alertTypes.forEach((type) => {
if (camera.info.capabilities.includes('detectors.on_camera')) {
nestAccessory.createService(hap.Service.MotionSensor, type);
this.log.debug(`Creating motion sensor for ${accessory.displayName} ${type}.`);
camera.startAlertChecks();
}
});
} else {
nestAccessory.removeAllServicesByType(hap.Service.MotionSensor);
}
}
});
}
private cleanupAccessories(): void {
// Remove cached cameras filtered by structure
if (this.structures.length > 0) {
const oldObjects = this.nestObjects.filter(
(obj: NestObject) => !this.structures.includes(obj.camera.info.nest_structure_id.replace('structure.', '')),
);
oldObjects.forEach((obj) => {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [obj.accessory]);
const index = this.nestObjects.indexOf(obj);
if (index > -1) {
obj.camera.stopAlertChecks();
this.nestObjects.splice(index, 1);
}
});
}
// Remove cached cameras filtered by uuid
if (this.cameras.length > 0) {
const oldObjects = this.nestObjects.filter((obj: NestObject) => !this.cameras.includes(obj.camera.info.uuid));
oldObjects.forEach((obj) => {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [obj.accessory]);
const index = this.nestObjects.indexOf(obj);
if (index > -1) {
obj.camera.stopAlertChecks();
this.nestObjects.splice(index, 1);
}
});
}
}
/**
* Filter cameras from Nest account
*/
private filterCameras(cameras: Array<CameraInfo>): Array<CameraInfo> {
if (this.structures.length > 0) {
this.log.debug('Filtering cameras by structures config');
cameras = cameras.filter((info: CameraInfo) =>
this.structures.includes(info.nest_structure_id.replace('structure.', '')),
);
}
if (this.cameras.length > 0) {
this.log.debug('Filtering cameras by cameras config');
cameras = cameras.filter((info: CameraInfo) => this.cameras.includes(info.uuid));
}
return cameras;
}
/**
* Add fetched cameras from nest to Homebridge
*/
private async addCameras(cameras: Array<CameraInfo>): Promise<void> {
const filteredCameras = await this.filterCameras(cameras);
filteredCameras.forEach(async (cameraInfo: CameraInfo) => {
const uuid = hap.uuid.generate(cameraInfo.uuid);
// Parenthesis in the name breaks HomeKit for some reason
const displayName = cameraInfo.name.replace('(', '').replace(')', '');
const accessory = new Accessory(displayName, uuid);
accessory.context.cameraInfo = cameraInfo;
const model = cameraInfo.type < ModelTypes.length ? ModelTypes[cameraInfo.type] : 'Nest Camera';
const accessoryInformation = accessory.getService(hap.Service.AccessoryInformation);
if (accessoryInformation) {
accessoryInformation.setCharacteristic(hap.Characteristic.Manufacturer, 'Nest');
accessoryInformation.setCharacteristic(hap.Characteristic.Model, model);
accessoryInformation.setCharacteristic(hap.Characteristic.SerialNumber, cameraInfo.serial_number);
accessoryInformation.setCharacteristic(
hap.Characteristic.FirmwareRevision,
cameraInfo.combined_software_version,
);
}
// Only add new cameras that are not cached
const obj = this.nestObjects.find((x: NestObject) => x.accessory.UUID === uuid);
if (obj) {
await obj.camera.updateData(cameraInfo);
} else {
this.log.debug(`New camera found: ${cameraInfo.name}`);
this.configureAccessory(accessory); // abusing the configureAccessory here
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
});
}
async getAccessToken(): Promise<string> {
const { refreshToken, googleAuth, nest_token } = this.config;
if (refreshToken) {
return await auth(refreshToken, this.config.options?.fieldTest, this.log);
} else if (googleAuth) {
const { apiKey, issueToken, cookies } = googleAuth;
if (!issueToken || !cookies) {
this.log.error('You must provide issueToken and cookies in config.json. Please see README.md for instructions');
return '';
}
return await old_auth(issueToken, cookies, apiKey, this.log);
} else if (nest_token) {
return await nest_auth(nest_token, this.log);
} else {
this.log.error(
'You must provide a refreshToken or googleAuth object in config.json. Please see README.md for instructions',
);
return '';
}
}
async didFinishLaunching(): Promise<void> {
const self = this;
const accessToken = await this.getAccessToken();
if (accessToken) {
this.config.access_token = accessToken;
// Nest needs to be reauthenticated about every hour
setInterval(async () => {
self.log.debug('Reauthenticating with config credentials');
this.config.access_token = await this.getAccessToken();
}, 3480000); // 58 minutes
const cameras = await getCameras(this.config, this.log);
await this.addCameras(cameras);
await this.setupMotionServices();
this.cleanupAccessories();
const session = new NestSession(this.config, this.log);
const cameraObjects = this.nestObjects.map((x) => x.camera);
await session.subscribe(cameraObjects);
} else {
this.log.error('Unable to retrieve access token.');
}
}
isShuttingDown(): void {
const accessoryObjects = this.nestObjects.map((x) => x.accessory);
this.api.updatePlatformAccessories(accessoryObjects);
}
}
Example #24
Source File: index.ts From homebridge-fordpass with GNU General Public License v3.0 | 4 votes |
class FordPassPlatform implements DynamicPlatformPlugin {
private readonly log: Logging;
private readonly api: API;
private readonly accessories: Array<PlatformAccessory> = [];
private readonly vehicles: Array<Vehicle> = [];
private config: FordpassConfig;
private pendingLockUpdate = false;
constructor(log: Logging, config: PlatformConfig, api: API) {
this.log = log;
this.api = api;
this.config = config as FordpassConfig;
// Need a config or plugin will not start
if (!config) {
return;
}
if (!config.username || !config.password) {
this.log.error('Please add a userame and password to your config.json');
return;
}
api.on(APIEvent.DID_FINISH_LAUNCHING, this.didFinishLaunching.bind(this));
}
configureAccessory(accessory: PlatformAccessory): void {
const self = this;
this.log.info(`Configuring accessory ${accessory.displayName}`);
accessory.on(PlatformAccessoryEvent.IDENTIFY, () => {
this.log.info(`${accessory.displayName} identified!`);
});
const vehicle = new Vehicle(accessory.context.name, accessory.context.vin, this.config, this.log);
const fordAccessory = new FordpassAccessory(accessory);
// Create Lock service
const defaultState = hap.Characteristic.LockTargetState.UNSECURED;
const lockService = fordAccessory.createService(hap.Service.LockMechanism);
const switchService = fordAccessory.createService(hap.Service.Switch);
const batteryService = fordAccessory.createService(
hap.Service.Battery,
this.config.options?.batteryName || 'Fuel Level',
);
lockService.setCharacteristic(hap.Characteristic.LockCurrentState, defaultState);
lockService
.setCharacteristic(hap.Characteristic.LockTargetState, defaultState)
.getCharacteristic(hap.Characteristic.LockTargetState)
.on(CharacteristicEventTypes.SET, async (value: CharacteristicValue, callback: CharacteristicSetCallback) => {
this.log.debug(`${value ? 'Locking' : 'Unlocking'} ${accessory.displayName}`);
let commandId = '';
let command = Command.LOCK;
if (value === hap.Characteristic.LockTargetState.UNSECURED) {
command = Command.UNLOCK;
}
commandId = await vehicle.issueCommand(command);
let tries = 30;
this.pendingLockUpdate = true;
const self = this;
const interval = setInterval(async () => {
if (tries > 0) {
const status = await vehicle.commandStatus(command, commandId);
if (status?.status === 200) {
lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, value);
self.pendingLockUpdate = false;
clearInterval(interval);
}
tries--;
} else {
self.pendingLockUpdate = false;
clearInterval(interval);
}
}, 1000);
callback(undefined, value);
})
.on(CharacteristicEventTypes.GET, async (callback: CharacteristicGetCallback) => {
// Return cached value immediately then update properly
let lockNumber = hap.Characteristic.LockTargetState.UNSECURED;
const lockStatus = vehicle?.info?.lockStatus.value;
if (lockStatus === 'LOCKED') {
lockNumber = hap.Characteristic.LockTargetState.SECURED;
}
callback(undefined, lockNumber);
const status = await vehicle.status();
if (status) {
let lockNumber = hap.Characteristic.LockTargetState.UNSECURED;
const lockStatus = status.lockStatus.value;
if (lockStatus === 'LOCKED') {
lockNumber = hap.Characteristic.LockTargetState.SECURED;
}
lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, lockNumber);
lockService.updateCharacteristic(hap.Characteristic.LockTargetState, lockNumber);
} else {
self.log.error(`Cannot get information for ${accessory.displayName} lock`);
}
});
switchService
.setCharacteristic(hap.Characteristic.On, false)
.getCharacteristic(hap.Characteristic.On)
.on(CharacteristicEventTypes.SET, async (value: CharacteristicValue, callback: CharacteristicSetCallback) => {
this.log.debug(`${value ? 'Starting' : 'Stopping'} ${accessory.displayName}`);
if (value as boolean) {
await vehicle.issueCommand(Command.START);
} else {
await vehicle.issueCommand(Command.STOP);
}
callback(undefined, value);
})
.on(CharacteristicEventTypes.GET, async (callback: CharacteristicGetCallback) => {
// Return cached value immediately then update properly
const engineStatus = vehicle?.info?.remoteStartStatus.value || 0;
callback(undefined, engineStatus);
const status = await vehicle.status();
if (status) {
let started = false;
const engineStatus = status.remoteStartStatus.value || 0;
if (engineStatus > 0) {
started = true;
}
switchService.updateCharacteristic(hap.Characteristic.On, started);
} else {
self.log.error(`Cannot get information for ${accessory.displayName} engine`);
}
});
batteryService
.setCharacteristic(hap.Characteristic.BatteryLevel, 100)
.getCharacteristic(hap.Characteristic.BatteryLevel)
.on(CharacteristicEventTypes.GET, async (callback: CharacteristicGetCallback) => {
// Return cached value immediately then update properly
const fuel = vehicle?.info?.fuel?.fuelLevel;
const battery = vehicle?.info?.batteryFillLevel?.value;
let level = fuel || battery || 100;
if (level > 100) {
level = 100;
}
if (level < 0) {
level = 0;
}
callback(undefined, level);
const status = await vehicle.status();
if (status) {
const fuel = status.fuel?.fuelLevel;
const battery = status.batteryFillLevel?.value;
const chargingStatus = vehicle?.info?.chargingStatus?.value;
let level = fuel || battery || 100;
if (level > 100) {
level = 100;
}
if (level < 0) {
level = 0;
}
batteryService.updateCharacteristic(hap.Characteristic.BatteryLevel, level);
if (battery) {
if (chargingStatus === 'ChargingAC') {
batteryService.updateCharacteristic(
hap.Characteristic.ChargingState,
hap.Characteristic.ChargingState.CHARGING,
);
} else {
batteryService.updateCharacteristic(
hap.Characteristic.ChargingState,
hap.Characteristic.ChargingState.NOT_CHARGING,
);
}
} else {
batteryService.updateCharacteristic(
hap.Characteristic.ChargingState,
hap.Characteristic.ChargingState.NOT_CHARGEABLE,
);
}
if (level < 10) {
batteryService.updateCharacteristic(
hap.Characteristic.StatusLowBattery,
hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW,
);
} else {
batteryService.updateCharacteristic(
hap.Characteristic.StatusLowBattery,
hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL,
);
}
} else {
self.log.error(`Cannot get information for ${accessory.displayName} engine`);
}
});
this.vehicles.push(vehicle);
this.accessories.push(accessory);
}
async didFinishLaunching(): Promise<void> {
const self = this;
const ford = new Connection(this.config, this.log);
const authInfo = await ford.auth();
if (authInfo) {
setInterval(async () => {
self.log.debug('Reauthenticating with refresh token');
await ford.refreshAuth();
}, authInfo.expires_in * 1000 - 10000);
await this.addVehicles(ford);
await this.updateVehicles();
await this.refreshVehicles();
// Vehicle info needs to be updated every minute
setInterval(async () => {
await self.updateVehicles();
}, 60 * 1000);
}
}
async addVehicles(connection: Connection): Promise<void> {
const vehicles = await connection.getVehicles();
vehicles?.forEach(async (vehicle: VehicleConfig) => {
vehicle.vin = vehicle.vin.toUpperCase();
const name = vehicle.nickName || vehicle.vehicleType;
const uuid = hap.uuid.generate(vehicle.vin);
const accessory = new Accessory(name, uuid);
accessory.context.name = name;
accessory.context.vin = vehicle.vin;
const accessoryInformation = accessory.getService(hap.Service.AccessoryInformation);
if (accessoryInformation) {
accessoryInformation.setCharacteristic(hap.Characteristic.Manufacturer, 'Ford');
accessoryInformation.setCharacteristic(hap.Characteristic.Model, name);
accessoryInformation.setCharacteristic(hap.Characteristic.SerialNumber, vehicle.vin);
}
// Only add new cameras that are not cached
if (!this.accessories.find((x: PlatformAccessory) => x.UUID === uuid)) {
this.log.debug(`New vehicle found: ${name}`);
this.configureAccessory(accessory); // abusing the configureAccessory here
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
});
// Remove vehicles that were removed from config
this.accessories.forEach((accessory: PlatformAccessory<Record<string, string>>) => {
if (!vehicles?.find((x: VehicleConfig) => x.vin === accessory.context.vin)) {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
const index = this.accessories.indexOf(accessory);
if (index > -1) {
this.accessories.splice(index, 1);
this.vehicles.slice(index, 1);
}
}
});
}
async updateVehicles(): Promise<void> {
this.vehicles.forEach(async (vehicle: Vehicle) => {
const status = await vehicle.status();
this.log.debug(`Updating info for ${vehicle.name}`);
const lockStatus = status?.lockStatus.value;
let lockNumber = hap.Characteristic.LockCurrentState.UNSECURED;
if (lockStatus === 'LOCKED') {
lockNumber = hap.Characteristic.LockCurrentState.SECURED;
}
const engineStatus = status?.remoteStartStatus.value || 0;
let started = false;
if (engineStatus > 0) {
started = true;
}
const uuid = hap.uuid.generate(vehicle.vin);
const accessory = this.accessories.find((x: PlatformAccessory) => x.UUID === uuid);
if (!this.pendingLockUpdate) {
const lockService = accessory?.getService(hap.Service.LockMechanism);
lockService && lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, lockNumber);
lockService && lockService.updateCharacteristic(hap.Characteristic.LockTargetState, lockNumber);
}
const switchService = accessory?.getService(hap.Service.Switch);
switchService && switchService.updateCharacteristic(hap.Characteristic.On, started);
});
}
async refreshVehicles(): Promise<void> {
this.vehicles.forEach(async (vehicle: Vehicle) => {
if (vehicle.autoRefresh && vehicle.refreshRate && vehicle.refreshRate > 0) {
this.log.debug(`Configuring ${vehicle.name} to refresh every ${vehicle.refreshRate} minutes.`);
setInterval(async () => {
this.log.debug(`Refreshing info for ${vehicle.name}`);
await vehicle.issueCommand(Command.REFRESH);
}, 60000 * vehicle.refreshRate);
}
});
}
}