Skip to content

Commit

Permalink
Refactored noble and states
Browse files Browse the repository at this point in the history
  • Loading branch information
tritter committed Nov 25, 2022
1 parent 81797fa commit 25589c7
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 100 deletions.
103 changes: 53 additions & 50 deletions src/controllers/machineController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { PlatformAccessory, Logger } from 'homebridge';
import { CoffeeType, CoffeeTypeUtils } from '../models/cofeeTypes';
import { on, Peripheral, startScanningAsync, stopScanningAsync, Characteristic, removeAllListeners, state } from '@abandonware/noble';
import MachineUDID from '../models/machineUDIDs';
import { MachineStatus } from '../models/machineStatus';
import { ResponseStatus } from '../models/responseStatus';
Expand All @@ -10,7 +9,8 @@ import { CapsuleCount } from '../models/capsuleCount';
import EventEmitter from 'events';
import os from 'os';
import { TemperatureType, TemperatureUtils } from '../models/temperatureType';
import { assertBluetooth } from '../helpers';
import noble from '@abandonware/noble';


export interface IMachineController {
isConnected(): boolean;
Expand All @@ -21,6 +21,7 @@ export interface IMachineController {
cancel(): Promise<ResponseStatus | undefined>;
reconnect(): Promise<void>;
disconnect(): Promise<void>;
startWatching(): void;
}

export interface IMachineControllerEvents {
Expand All @@ -34,25 +35,30 @@ export class MachineController extends EventEmitter implements IMachineControlle
private static RECONNECT_TIMEOUT_MS = 1000 * 10; //10 second
private static UNREACHABLE_TIMEOUT = 1000 * 60 * 5; //5 min
private readonly _config: IDeviceConfig;
private _periphial: Peripheral | undefined;
private _periphial: noble.Peripheral | undefined;
private _lastBrew: CoffeeType | undefined;
private _lastStatus: MachineStatus | undefined;
private _scanning = false;
private _lastContact: Date | undefined;
private _stateChangeListener: EventEmitter | undefined;

constructor(
public readonly log: Logger,
public readonly accessory: PlatformAccessory) {
super();
this._config = accessory.context.device;
this.subscribeBluetoothState();
}

private subscribeBluetoothState() {
on('stateChange', () => {
if (state !== 'poweredOn') {
this.log.error(`Bluetooth unavailable:${state}`);
startWatching() {
if (this._stateChangeListener) {
return;
}
this._stateChangeListener = noble.on('stateChange', () => {
if (noble.state !== 'poweredOn') {
this.log.error(`Bluetooth unavailable:${noble.state}`);
this.disconnect();
} else {
this.reconnect();
}
});
}
Expand Down Expand Up @@ -105,19 +111,23 @@ export class MachineController extends EventEmitter implements IMachineControlle
}

async reconnect(): Promise<void> {
await this.disconnect();
await this.connect();
if (!this._stateChangeListener) {
this.startWatching();
} else {
await this.disconnect();
await this.connect();
}
}

async connect(): Promise<Characteristic[]> {
async connect(): Promise<noble.Characteristic[]> {
const sessionRunning = this.isConnected();
if (sessionRunning) {
return await this.findCharacteristics(this._periphial!);
}
await this.disconnect();
this._periphial = await this.find();
const characteristics = await this.findCharacteristics(this._periphial!);
this._periphial.on('disconnect', async (error: string) => {
this._periphial?.on('disconnect', async (error: string) => {
this.log.debug('Machine did disconnect!');
this._lastContact = new Date();
this._lastBrew = undefined;
Expand Down Expand Up @@ -146,7 +156,7 @@ export class MachineController extends EventEmitter implements IMachineControlle
return (platform === 'linux' || platform === 'freebsd' || platform === 'win32');
}

async subscribe(characteristics: Characteristic[]) {
async subscribe(characteristics: noble.Characteristic[]) {
await this.subscribeStatus(characteristics);
await this.subscribeSliderStatus(characteristics);
await this.subscribeCapsuleCount(characteristics);
Expand All @@ -165,41 +175,39 @@ export class MachineController extends EventEmitter implements IMachineControlle
this._lastBrew = undefined;
}

private find(): Promise<Peripheral> {
removeAllListeners('discover');
private find(): Promise<noble.Peripheral> {
noble.removeAllListeners('discover');
return new Promise((resolve, rejects) => {
assertBluetooth(this.log).then(() => {
this.log.debug('Start scan...');
on('discover', (peripheral: Peripheral) => {
if (peripheral.advertisement.localName === this._config.name) {
removeAllListeners('discover');
stopScanningAsync();
peripheral.connect((error) => {
this._scanning = false;
if (error) {
rejects('Error connecting to periphial');
} else {
this.log.debug(`Connected to ${peripheral.advertisement.localName}`);
resolve(peripheral);
}
});
}
});
this._scanning = true;
startScanningAsync([MachineUDID.services.auth, MachineUDID.services.command], true);
}).catch();
this.log.debug('Start scan...');
noble.on('discover', (peripheral: noble.Peripheral) => {
if (peripheral.advertisement.localName === this._config.name) {
noble.removeAllListeners('discover');
noble.stopScanningAsync();
peripheral.connect((error) => {
this._scanning = false;
if (error) {
rejects('Error connecting to periphial');
} else {
this.log.debug(`Connected to ${peripheral.advertisement.localName}`);
resolve(peripheral);
}
});
}
});
this._scanning = true;
noble.startScanningAsync([MachineUDID.services.auth, MachineUDID.services.command], true);
});
}

private findCharacteristics(peripheral: Peripheral) : Promise<Characteristic[]> {
private findCharacteristics(peripheral: noble.Peripheral) : Promise<noble.Characteristic[]> {
return new Promise((resolve) => {
peripheral.discoverAllServicesAndCharacteristics((error, services, characteristics) => {
resolve(characteristics);
});
});
}

private authenticate(characteristics: Characteristic[]) : Promise<void> {
private authenticate(characteristics: noble.Characteristic[]) : Promise<void> {
return new Promise((resolve, rejects) => {
this.log.debug('Start Authentication');
const data = this.generateKey();
Expand Down Expand Up @@ -236,7 +244,7 @@ export class MachineController extends EventEmitter implements IMachineControlle
return Buffer.from(view);
}

private subscribeStatus(characteristics: Characteristic[]) : Promise<void> {
private subscribeStatus(characteristics: noble.Characteristic[]) : Promise<void> {
return new Promise((resolve, rejects) => {
this.log.debug('Start subscribe status');
const statusCharacteristic = characteristics.find(char => char.uuid === MachineUDID.characteristics.status)!;
Expand All @@ -259,7 +267,7 @@ export class MachineController extends EventEmitter implements IMachineControlle
});
}

private subscribeSliderStatus(characteristics: Characteristic[]) : Promise<void> {
private subscribeSliderStatus(characteristics: noble.Characteristic[]) : Promise<void> {
return new Promise((resolve, rejects) => {
this.log.debug('Start subscribe status');
const statusCharacteristic = characteristics.find(char => char.uuid === MachineUDID.characteristics.slider)!;
Expand All @@ -281,7 +289,7 @@ export class MachineController extends EventEmitter implements IMachineControlle
});
}

private subscribeCapsuleCount(characteristics: Characteristic[]) : Promise<void> {
private subscribeCapsuleCount(characteristics: noble.Characteristic[]) : Promise<void> {
return new Promise((resolve, rejects) => {
this.log.debug('Start subscribe capsule counter');
const capsuleCharacteristic = characteristics.find(char => char.uuid === MachineUDID.characteristics.capsules)!;
Expand All @@ -295,15 +303,10 @@ export class MachineController extends EventEmitter implements IMachineControlle
this.log.debug(`Received capsule count change! ${data.toString('hex')}`);
this.emit('capsule', new CapsuleCount(data));
});
capsuleCharacteristic.notify(true, (error) => {
if (error) {
rejects(`Error enabling notify ${error}`);
}
});
});
}

private subscribeResponse(characteristics: Characteristic[]) : Promise<void> {
private subscribeResponse(characteristics: noble.Characteristic[]) : Promise<void> {
return new Promise((resolve, rejects) => {
this.log.debug('Start subscribe response');
const responseCharacteristic = characteristics.find(char => char.uuid === MachineUDID.characteristics.response)!;
Expand All @@ -321,7 +324,7 @@ export class MachineController extends EventEmitter implements IMachineControlle
});
}

private async updateStates(characteristics: Characteristic[]) {
private async updateStates(characteristics: noble.Characteristic[]) {
this.log.debug('Request reading states');
const statusCharacteristic = characteristics.find(char => char.uuid === MachineUDID.characteristics.status)!;
const capsulesCharacteristic = characteristics.find(char => char.uuid === MachineUDID.characteristics.capsules)!;
Expand All @@ -331,21 +334,21 @@ export class MachineController extends EventEmitter implements IMachineControlle
sliderCharacteristic.read();
}

private sendBrewCommand(characteristics: Characteristic[],
private sendBrewCommand(characteristics: noble.Characteristic[],
coffeeType: CoffeeType,
temperature: TemperatureType) : Promise<ResponseStatus> {
const command = CoffeeTypeUtils.command(coffeeType, temperature);
this.log.info(`Writing brew ${CoffeeTypeUtils.humanReadable(coffeeType)} - ${TemperatureUtils.toString(temperature)} command`);
return this.sendCommand(characteristics, this.generateBuffer(command));
}

private sendCancelCommand(characteristics: Characteristic[]) : Promise<ResponseStatus> {
private sendCancelCommand(characteristics: noble.Characteristic[]) : Promise<ResponseStatus> {
const command = new Uint8Array([ 0x03, 0x06, 0x01, 0x02]);
this.log.info('Writing cancel command');
return this.sendCommand(characteristics, Buffer.from(command));
}

private async sendCommand(characteristics: Characteristic[], buffer: Buffer) : Promise<ResponseStatus> {
private async sendCommand(characteristics: noble.Characteristic[], buffer: Buffer) : Promise<ResponseStatus> {
return new Promise((resolve, rejects) => {
const receiveChar = characteristics.find(char => char.uuid === MachineUDID.characteristics.response)!;
const sendChar = characteristics.find(char => char.uuid === MachineUDID.characteristics.request)!;
Expand Down
25 changes: 0 additions & 25 deletions src/helpers.ts

This file was deleted.

21 changes: 3 additions & 18 deletions src/platform.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge';
import { on, Peripheral, startScanningAsync, stopScanning } from '@abandonware/noble';
import { PLATFORM_NAME, PLUGIN_NAME } from './settings';
import { ExpertPlatformAccessory } from './platformAccessory';
import MachineUDID from './models/machineUDIDs';
import { IDeviceConfig } from './models/deviceConfig';
import { assertBluetooth } from './helpers';

export class NespressoPlatform implements DynamicPlatformPlugin {

Expand All @@ -23,15 +20,14 @@ export class NespressoPlatform implements DynamicPlatformPlugin {
this.api.on('didFinishLaunching', () => {
log.debug('Start discovery');
try {
this.discoverDevices();
this.addDevices();
} catch(error) {
this.log.error('Error discovering devices:', error);
this.log.error('Error adding devices:', error);
}
});

this.api.on('shutdown', () => {
log.debug('Shutting down discovery');
stopScanning();
});
}

Expand All @@ -40,21 +36,10 @@ export class NespressoPlatform implements DynamicPlatformPlugin {
this.accessories.push(accessory);
}

discoverDevices() {
addDevices() {
const configuredDevices: IDeviceConfig[] = this.config['machines'] ?? new Array<IDeviceConfig>();
const devicesMap = configuredDevices.reduce((a, x) => ({...a, [x.name]: x.token}), {});
this.log.debug(`Configured machines: ${configuredDevices.length}`);
this.log.debug('Start discovery');
on('discover', (peripheral: Peripheral) => {
if (!devicesMap[peripheral.advertisement.localName]) {
this.log.info(`Found new device, please add configuration for: "${peripheral.advertisement.localName}"`);
} else {
this.log.debug(`Found ${peripheral.advertisement.localName}, already configured`);
}
});
assertBluetooth(this.log).then(() => {
startScanningAsync([MachineUDID.services.auth, MachineUDID.services.command], false);
});

for (const configuredDevice of configuredDevices) {
if (!configuredDevice.name) {
Expand Down
8 changes: 1 addition & 7 deletions src/platformAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { CapsuleCount } from './models/capsuleCount';
import { MachineStatus } from './models/machineStatus';
import { MachineController } from './controllers/machineController';
import { TemperatureUtils } from './models/temperatureType';
import { assertBluetooth } from './helpers';

export class ExpertPlatformAccessory {
private static WATCHDOG_INTERVAL_MS = 1000 * 60 * 1; //one minute
Expand All @@ -25,12 +24,7 @@ export class ExpertPlatformAccessory {
this._services = new ServiceController(platform.log, accessory, platform);
this.subscribeServices();
this.subscribeController();
this.connect();
}

async connect() {
await assertBluetooth(this.platform.log);
this._controller.connect();
this._controller.startWatching();
this.startConnectionDog();
}

Expand Down

0 comments on commit 25589c7

Please sign in to comment.