homebridge-hatch-baby-rest-volume
Version:
Homebridge plugin for Hatch Baby Rest bluetooth night light volume
189 lines (188 loc) • 9.46 kB
JavaScript
"use strict";
/**
* If you are viewing this code and considering extracting it for another use,
* please let me know with a GitHub issue! I would be happy to provide an API that
* can be used by other projects, but didn't want to go to the work until there was a need.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HatchBabyRest = void 0;
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const util_1 = require("./util");
const rest_commands_1 = require("./rest-commands");
const util_2 = require("util");
const feedback_1 = require("./feedback");
const usedPeripheralIds = [];
class HatchBabyRest {
constructor(name, macAddress, logger) {
this.name = name;
this.macAddress = macAddress;
this.logger = logger;
this.noble = require('@abandonware/noble');
this.peripheralPromise = this.getPeripheralByAddress(this.macAddress);
this.onFeedback = new rxjs_1.BehaviorSubject({
time: 0,
power: false,
volume: 0,
color: { r: 0, g: 0, b: 0, a: 0 },
audioTrack: 0 /* None */,
});
this.onPower = this.onFeedback.pipe(operators_1.map((feedback) => feedback.power), operators_1.distinctUntilChanged(), operators_1.share());
this.onVolume = this.onFeedback.pipe(operators_1.map((feedback) => feedback.volume), operators_1.distinctUntilChanged(), operators_1.share());
this.onColor = this.onFeedback.pipe(operators_1.map((feedback) => feedback.color), operators_1.distinctUntilChanged(feedback_1.colorsMatch), operators_1.share());
this.onAudioTrack = this.onFeedback.pipe(operators_1.map((feedback) => feedback.audioTrack), operators_1.distinctUntilChanged(), operators_1.share());
this.onUsingConnection = new rxjs_1.Subject();
this.getDevice().then((device) => {
device.on('connect', () => {
return this.subscribeToFeedback();
});
});
process.on('SIGINT', () => {
this.disconnect();
process.exit();
});
this.onUsingConnection.pipe(operators_1.debounceTime(5000)).subscribe(() => {
this.disconnect();
});
}
getPeripheralByAddress(address) {
return __awaiter(this, void 0, void 0, function* () {
this.logger.info('Waiting for bluetooth to power on');
yield rxjs_1.fromEvent(this.noble, 'stateChange')
.pipe(operators_1.startWith(this.noble.state), operators_1.filter((state) => state === 'poweredOn'), operators_1.take(1))
.toPromise();
const stripedAddress = util_1.stripMacAddress(address), peripheralPromise = rxjs_1.fromEvent(this.noble, 'discover')
.pipe(operators_1.filter((peripheral) => {
return (util_1.stripMacAddress(peripheral.address) === stripedAddress ||
(peripheral.addressType === 'unknown' &&
!usedPeripheralIds.includes(peripheral.id)));
}), operators_1.take(1), operators_1.tap((peripheral) => {
usedPeripheralIds.push(peripheral.id);
this.logger.info(`Found device ${peripheral.advertisement.localName} with address ${peripheral.address || peripheral.addressType}`);
if (peripheral.addressType === 'unknown') {
this.logger.info(`${peripheral.advertisement.localName} has an unknown address. This happens on OSX when you discover a device which you have never connected to. Once connected, OSX will remember the address of this device. If this is not the correct device, please restart homebridge and this device should be skipped over.`);
// Force the device to connect, which will load the address into OSX for the next run
this.getCharacteristic("02260002-5efd-47eb-9c1a-de53f7a2b232" /* CurrentFeedback */, "02260001-5efd-47eb-9c1a-de53f7a2b232" /* Advertising */);
}
}))
.toPromise();
this.logger.info('Scanning for device');
this.noble.startScanning(['180a']);
return peripheralPromise;
});
}
connect() {
return __awaiter(this, void 0, void 0, function* () {
const device = yield this.getDevice();
if (device.state === 'connected') {
return device;
}
this.logger.info(`Connecting to ${device.advertisement.localName}...`);
yield util_2.promisify(device.connect.bind(device))();
this.logger.info(`Connected to ${device.advertisement.localName}`);
return device;
});
}
disconnect() {
if (this.reconnectSubscription) {
this.reconnectSubscription.unsubscribe();
this.reconnectSubscription = undefined;
}
if (this.device) {
this.device.disconnect();
this.logger.info(`Disconnected from ${this.device.advertisement.localName}`);
}
this.discoverServicesPromise = undefined;
}
getServices() {
if (!this.discoverServicesPromise) {
this.discoverServicesPromise = this.connect().then((device) => util_2.promisify(device.discoverAllServicesAndCharacteristics.bind(device))());
}
return this.discoverServicesPromise;
}
getService(serviceUuid) {
return __awaiter(this, void 0, void 0, function* () {
const services = yield this.getServices(), targetUuid = util_1.stripUuid(serviceUuid), service = services.find((s) => util_1.stripUuid(s.uuid) === targetUuid);
if (!service) {
this.disconnect();
throw new Error(`Service ${serviceUuid} not found!`);
}
return service;
});
}
getCharacteristic(characteristicUuid, serviceUuid) {
return __awaiter(this, void 0, void 0, function* () {
this.onUsingConnection.next();
const service = yield this.getService(serviceUuid), targetUuid = util_1.stripUuid(characteristicUuid), characteristic = service.characteristics.find((c) => util_1.stripUuid(c.uuid) === targetUuid);
if (!characteristic) {
this.disconnect();
throw new Error(`Characteristic ${characteristicUuid} not found!`);
}
this.onUsingConnection.next();
return characteristic;
});
}
getDevice() {
return __awaiter(this, void 0, void 0, function* () {
if (this.device) {
return this.device;
}
return (this.device = yield this.peripheralPromise);
});
}
getName() {
return __awaiter(this, void 0, void 0, function* () {
const device = yield this.getDevice();
return device.advertisement.localName;
});
}
setCommand(command, value) {
return __awaiter(this, void 0, void 0, function* () {
yield this.connect();
const writeCharacteristic = yield this.getCharacteristic("02240002-5efd-47eb-9c1a-de53f7a2b232" /* Tx */, "02240001-5efd-47eb-9c1a-de53f7a2b232" /* Rest */), restCommand = rest_commands_1.formatRestCommand(command, value);
yield util_2.promisify(writeCharacteristic.write.bind(writeCharacteristic))(restCommand, false);
this.onUsingConnection.next();
});
}
setAudioTrack(track) {
return this.setCommand("SN" /* SetTrackNumber */, track);
}
setColor(color) {
return this.setCommand("SC" /* SetColor */, color);
}
setVolume(volume) {
if (volume < 0 || volume > 100) {
throw new Error('Volume must be between 0 and 100. Received ' + volume);
}
return this.setCommand("SV" /* SetVolume */, Math.floor((volume / 100) * 255));
}
setPower(on) {
return this.setCommand("SI" /* SetPower */, on ? 1 : 0);
}
subscribeToFeedback() {
return __awaiter(this, void 0, void 0, function* () {
const feedbackCharacteristic = yield this.getCharacteristic("02260002-5efd-47eb-9c1a-de53f7a2b232" /* CurrentFeedback */, "02260001-5efd-47eb-9c1a-de53f7a2b232" /* Advertising */);
feedbackCharacteristic.on('read', (data) => {
this.onFeedback.next(feedback_1.parseFeedbackBuffer(data));
});
feedbackCharacteristic.subscribe((err) => {
if (err) {
this.logger.error('Failed to subscribe to feedback events', err);
}
});
});
}
get currentFeedback() {
return this.onFeedback.getValue();
}
}
exports.HatchBabyRest = HatchBabyRest;