UNPKG

fp2-proxy

Version:

Proxy for Aqara Presence Sensor FP2

234 lines (204 loc) 7.35 kB
// fp2-proxy/lib/Fp2Client.js // Copyright © 2023-2025 Erik Baauw. All rights reserved. // // Proxy for Aqara Presence Sensor FP2. import { EventEmitter } from 'node:events' import { HttpClient, IPDiscovery } from 'hap-controller' import { OptionParser } from 'hb-lib-tools/OptionParser' import sodium from 'libsodium-wrappers' function uuid (id) { return '00000' + id + '-0000-1000-8000-0026BB765291' } const nZones = 30 const uuids = { AccessoryInformation: uuid('03E'), Identify: uuid('014'), Manufacturer: uuid('020'), // value: 'Aqara' Model: uuid('021'), // value 'PS-S02D' Name: uuid('023'), // value: 'Presence-Sensor-FP2-XXXX SerialNumber: uuid('030'), // value: '54EF44XXXXXX' FirmwareRevision: uuid('052'), // value: '1.1.7' HardwareRevision: uuid('053'), // value: '1.0.0' Aqara: '34AB8811-AC7F-4340-BAC3-FD6A85F9943B', // value: 6.1;6.1 ProductData: uuid('220'), // value: xDsGOzOmv1k= ProtocolInformation: uuid('0A2'), Version: uuid('037'), WiFiTransport: uuid('22A'), CurrentTransport: uuid('22B'), // value: true WiFiCapabilities: uuid('22C'), // value: 9 WiFiConfigurationControl: uuid('22D'), // value 0x02020000030400000000 AccessoryRuntimeInformation: uuid('239'), Ping: uuid('23C'), FirmwareUpdate: '9715BF53-AB63-4449-8DC7-2785D617390A', FirmwareUpdateStatus: '7D943F6A-E052-4E96-A176-D17BF00E32CB', // value: -1 FirmwareUpdateUrl: 'A45EFD52-0DB5-4C1A-9727-513FBCD8185F', // write only FirmwareUpdateChecksum: '40F0124A-579D-40E4-865E-0EF6740EA64B', // write only FirmwareVersion: '96BF5F20-2996-4DB6-8D65-0E36314BCB6D', // value: '1.1.7' DeviceModel: '36B7A28B-3200-4783-A3FB-6714F11B1417', // value: 'lumi.motion.agl001' SelectedIotPlatform: 'F5329CB1-A50B-4225-BA9B-331449E7F7A9', // value: 1 Aiot: 'F49132D1-12DF-4119-87D3-A93E8D68531E', // Name: uuid('023'), // value: 'AIOT' AiotCountryDomain: '25D889CB-7135-4A29-B5B4-C1FFD6D2DD5C', // value: 'aiot-coap.aqara.cn' AiotDid: 'C7EECAA7-91D9-40EB-AD0C-FFDDE3143CB9', // value: 'lumi1.54ef44xxxxxx' AiotBindKey: '80FA747E-CB45-45A4-B7BE-AA7D9964859E', // write only AiotBindState: 'C3B8A329-EF0C-4739-B773-E5B7AEA52C71', // value: true LightSensor: uuid('084'), // Name: uuid('023'), CurrentAmbientLightLevel: uuid('06B'), OccupancySensor: uuid('086'), // Name: uuid('023'), OccupancyDetected: uuid('071'), SensorIndex: 'C8622A33-826A-4DD3-9BE9-D496361F29BB' } class Fp2Client extends EventEmitter { constructor (params = {}) { super() this._options = { timeout: 5 } const optionParser = new OptionParser(this._options) optionParser .objectKey('service') .objectKey('pairData') .intKey('timeout', 5, 60) .parse(params) this._discovery = new IPDiscovery() this._client = new HttpClient( this._options.service.id, this._options.service.address, this._options.service.port, this._options.pairData, { usePersistentConnections: true } ) } async connect () { if (this._map != null) { return } if (this._options.pairData == null) { throw new Error(`${this._options.service.id}: not paired`) } this._accessories = await this._client.getAccessories() this._map = {} const services = this._accessories.accessories[0].services for (const service of services) { const characteristics = {} for (const characteristic of service.characteristics) { characteristics[characteristic.type] = characteristic.iid if (characteristic.type === uuids.SensorIndex) { if (service.type === uuids.OccupancySensor) { service.type += '|' + characteristic.value } } } this._map[service.type] = { iid: service.iid, characteristics } } } async disconnect () { delete this._map this._client?.close() } async identify () { if (this._options.pairData == null) { this.emit('identify') // Doesn't work: FP2 doesn't support unauthenticated identify. await this._client.identify() return } await this.connect() const iid = this._iid(uuids.AccessoryInformation, uuids.Identify) await this._put(iid, true) } get service () { return this._options.service } get pairData () { return this._options.pairData } async pair (setupCode) { if (!this._options.service.availableToPair) { throw new Error('already paired') } const pairMethod = await this._discovery.getPairMethod(this._options.service) const data = await this._client.startPairing(pairMethod) await this._client.finishPairing(data, setupCode) this._options.pairData = this._client.getLongTermData() const seed = Buffer.from(sodium.randombytes_buf(32)) const key = sodium.crypto_sign_seed_keypair(seed) const identifier = 'abcdefg' const isAdmin = false await this._client.addPairing(identifier, Buffer.from(key.publicKey), isAdmin) this._client.close() return this._options.pairData } async unpair () { await this.connect() await this._client.removePairing(this._client.pairingProtocol.iOSDevicePairingID) this._client.close() delete this._options.pairData } async accessories () { await this.connect() return this._accessories } _iid (serviceType, characteristicType, subtype) { if (serviceType.length === 3) { serviceType = uuid(serviceType) } if (subtype != null) { serviceType += '|' + subtype } return this._map[serviceType]?.characteristics?.[characteristicType] } async _get (iid) { await this.connect() const list = ['1.' + iid] this.emit('get', list) const chars = await this._client.getCharacteristics(list) return chars.characteristics[0].value } async _put (iid, value) { await this.connect() const body = {} body['1.' + iid] = value this.emit('put', body) await this._client.setCharacteristics(body) } async getId () { await this.connect() const iid = this._iid(uuids.AccessoryInformation, uuids.SerialNumber) return this._get(iid) } async subscribe () { await this.connect() const events = {} this._client .on('event', (event) => { for (const message of event.characteristics) { this.emit('event', events[message.iid](message)) } }) .on('event-disconnect', async (list) => { this.emit('event', { reachable: false }) try { await this._client.subscribeCharacteristics(list) } catch (error) { this.emit(error) } }) const iid = this._iid(uuids.LightSensor, uuids.CurrentAmbientLightLevel) const list = ['1.' + iid] this.emit('event', { lightLevel: await this._get(iid) }) events[iid] = (message) => { return { lightLevel: message.value } } for (let i = 0; i < nZones; i++) { const iid = this._iid(uuids.OccupancySensor, uuids.OccupancyDetected, i) if (iid != null) { list.push('1.' + iid) this.emit('event', { zone: i, presence: (await this._get(iid)).value === 1 }) events[iid] = (message) => { return { zone: i, presence: message.value === 1 } } } } this.emit('subscribe', list) await this._client.subscribeCharacteristics(list) } } export { Fp2Client }