Skip to content

Commit

Permalink
fix(plugin): more robust event and request handling
Browse files Browse the repository at this point in the history
- complete overhaul of event chain
- integrate http requests in base accessory
- resolve long-standing issues with promises and async calls
- better error handling
- rename platform event bus in preparation of accessory event bus
  • Loading branch information
johannrichard committed Nov 26, 2020
1 parent 76a609b commit 982ff2a
Show file tree
Hide file tree
Showing 10 changed files with 537 additions and 398 deletions.
569 changes: 279 additions & 290 deletions src/dingzAccessory.ts

Large diffs are not rendered by default.

99 changes: 97 additions & 2 deletions src/lib/dingzDaBaseAccessory.ts
@@ -1,25 +1,57 @@
import { Logger, PlatformAccessory } from 'homebridge';
import { DingzDaHomebridgePlatform } from '../platform';
import { DeviceInfo } from './commonTypes';
import { DingzEvent } from './dingzEventBus';
import { PlatformEvent } from './platformEventBus';

import axios, { AxiosRequestConfig, AxiosError, AxiosInstance } from 'axios';
import axiosRetry from 'axios-retry';

import { REQUEST_RETRIES, RETRY_TIMEOUT } from '../settings';
import { DeviceNotReachableError } from './errors';
export class DingzDaBaseAccessory {
protected static axiosRetryConfig = {
retries: REQUEST_RETRIES,
retryDelay: axiosRetry.exponentialDelay,
shouldResetTimeout: true,
};

protected static axios = axios;

protected readonly log: Logger;
protected readonly request: AxiosInstance;

protected device: DeviceInfo;
protected baseUrl: string;
protected isReachable = true;

constructor(
protected readonly platform: DingzDaHomebridgePlatform,
protected readonly accessory: PlatformAccessory,
) {
// Set-up axios instances
this.device = this.accessory.context.device;
this.baseUrl = `http://${this.device.address}`;

this.log = platform.log;
this.request = axios.create({
baseURL: this.baseUrl,
timeout: RETRY_TIMEOUT,
headers: { Token: this.device.token ?? '' },
});

/**
* Set-up the protected and static axios instance
*
* This leads to up to 18s delay between retries before failing
* Each request will time-out after 3000 ms
* maxdelay -> 2^7*100 ms -> 12.8s
*/
axiosRetry(this.request, DingzDaBaseAccessory.axiosRetryConfig);
axiosRetry(axios, DingzDaBaseAccessory.axiosRetryConfig);

// Register listener for updated device info (e.g. on restore with new IP)
this.platform.eb.on(
DingzEvent.UPDATE_DEVICE_INFO,
PlatformEvent.UPDATE_DEVICE_INFO,
(deviceInfo: DeviceInfo) => {
if (deviceInfo.mac === this.device.mac) {
this.log.debug(
Expand Down Expand Up @@ -62,4 +94,67 @@ export class DingzDaBaseAccessory {
this.device.accessoryClass,
);
}

/**
* Handler for request errors
* @param e AxiosError: the error returned by this.request()
*/
protected handleRequestErrors = (e: AxiosError) => {
if (e.isAxiosError) {
switch (e.code) {
case 'ECONNABORTED':
this.log.error('Connection aborted --> ' + e.config.url);
this.isReachable = false;
break;
default:
break;
}
this.log.error('HTTP Response Error in --> ' + e.config.url);
this.log.error(e.code ?? 'NOERRCODE');
this.log.error(e.message);
this.log.error(e.stack ?? '<No stack>');
this.log.error(e.response?.data);
this.log.error(e.response?.statusText ?? '');
} else if (e instanceof DeviceNotReachableError) {
this.log.error(
`handleRequestErrors() --> ${this.device.name} (${this.device.address})`,
);
this.isReachable = false;
} else {
this.log.error(e.message + '\n' + e.stack);
throw new Error('Device request failed -> escalating error');
}
};

protected static async _fetch({
url,
method = 'get',
returnBody = false,
token,
body,
}: {
url: string;
method?: string;
returnBody?: boolean;
token?: string;
body?: object | string;
}) {
// Retry up to 3 times, with exponential Backoff
// (https://developers.google.com/analytics/devguides/reporting/core/v3/errors#backoff)
const data = await axios({
url: url,
method: method,
headers: {
Token: token ?? '',
},
data: body,
} as AxiosRequestConfig).then((response) => {
if (returnBody) {
return response.data;
} else {
return response.status;
}
});
return data;
}
}
32 changes: 19 additions & 13 deletions src/lib/dingzEventBus.ts → src/lib/platformEventBus.ts
Expand Up @@ -4,47 +4,53 @@ import { ButtonId } from './dingzTypes';

// Platform elements
// EVENT TYPES
export const enum DingzEvent {
export const enum PlatformEvent {
UPDATE_DEVICE_INFO = 'updateDeviceInfo',
ACTION = 'deviceAction',
REQUEST_STATE_UPDATE = 'requestStateUpdate',
PUSH_STATE_UPDATE = 'pushStateUpdate',
}

export declare interface DingzEventBus {
export declare interface PlatformEventBus {
on(
event: DingzEvent.ACTION,
event: PlatformEvent.ACTION,
listener: (mac: string, action: ButtonAction, battery: number) => void,
): this;
on(
event: DingzEvent.ACTION,
event: PlatformEvent.ACTION,
listener: (mac: string, action: ButtonAction, button: ButtonId) => void,
): this;
on(
event: DingzEvent.UPDATE_DEVICE_INFO,
event: PlatformEvent.UPDATE_DEVICE_INFO,
listener: (deviceInfo: DeviceInfo) => void,
): this;
on(event: DingzEvent.REQUEST_STATE_UPDATE, listener: () => void): this;
on(event: DingzEvent.PUSH_STATE_UPDATE, listener: () => void): this;
on(event: PlatformEvent.REQUEST_STATE_UPDATE, listener: () => void): this;
on(
event: PlatformEvent.PUSH_STATE_UPDATE,
listener: (mac: string) => void,
): this;

emit(
event: DingzEvent.ACTION,
event: PlatformEvent.ACTION,
mac: string,
action: ButtonAction,
battery: number,
): boolean;
emit(
event: DingzEvent.ACTION,
event: PlatformEvent.ACTION,
mac: string,
action: ButtonAction,
button: ButtonId,
): boolean;
emit(event: DingzEvent.UPDATE_DEVICE_INFO, deviceInfo: DeviceInfo): boolean;
emit(event: DingzEvent.REQUEST_STATE_UPDATE): boolean;
emit(event: DingzEvent.PUSH_STATE_UPDATE): boolean;
emit(
event: PlatformEvent.UPDATE_DEVICE_INFO,
deviceInfo: DeviceInfo,
): boolean;
emit(event: PlatformEvent.REQUEST_STATE_UPDATE): boolean;
emit(event: PlatformEvent.PUSH_STATE_UPDATE, mac: string): boolean;
}

export class DingzEventBus extends EventEmitter {
export class PlatformEventBus extends EventEmitter {
constructor() {
super();
this.setMaxListeners(20); // Maximum of 20 services
Expand Down
4 changes: 2 additions & 2 deletions src/myStromButtonAccessory.ts
Expand Up @@ -9,7 +9,7 @@ import { Policy } from 'cockatiel';
import { DingzDaHomebridgePlatform } from './platform';
import { MyStromDeviceInfo } from './lib/myStromTypes';
import { ButtonAction } from './lib/commonTypes';
import { DingzEvent } from './lib/dingzEventBus';
import { PlatformEvent } from './lib/platformEventBus';
import { ButtonState } from './lib/dingzTypes';
import { DingzDaBaseAccessory } from './lib/dingzDaBaseAccessory';

Expand Down Expand Up @@ -120,7 +120,7 @@ export class MyStromButtonAccessory extends DingzDaBaseAccessory {
.getCharacteristic(this.platform.Characteristic.ChargingState)
.on(CharacteristicEventTypes.GET, this.getChargingState.bind(this));

this.platform.eb.on(DingzEvent.ACTION, (mac, action, battery) => {
this.platform.eb.on(PlatformEvent.ACTION, (mac, action, battery) => {
if (mac === this.device.mac) {
this.buttonState = action ?? ButtonAction.SINGLE_PRESS;
this.batteryLevel = battery;
Expand Down
4 changes: 2 additions & 2 deletions src/myStromLightbulbAccessory.ts
Expand Up @@ -12,7 +12,7 @@ import qs from 'qs';

import { DingzDaHomebridgePlatform } from './platform';
import { MyStromDeviceInfo, MyStromLightbulbReport } from './lib/myStromTypes';
import { DingzEvent } from './lib/dingzEventBus';
import { PlatformEvent } from './lib/platformEventBus';
import { DingzDaBaseAccessory } from './lib/dingzDaBaseAccessory';

/**
Expand Down Expand Up @@ -106,7 +106,7 @@ export class MyStromLightbulbAccessory extends DingzDaBaseAccessory {

// Subscribe to the REQUEST_STATE_UPDATE event
this.platform.eb.on(
DingzEvent.REQUEST_STATE_UPDATE,
PlatformEvent.REQUEST_STATE_UPDATE,
this.getDeviceStateUpdate.bind(this),
);
}
Expand Down
6 changes: 3 additions & 3 deletions src/myStromPIRAccessory.ts
Expand Up @@ -11,7 +11,7 @@ import { DingzDaHomebridgePlatform } from './platform';
import { MyStromDeviceInfo, MyStromPIRReport } from './lib/myStromTypes';
import { ButtonAction } from './lib/commonTypes';
import { DeviceNotReachableError } from './lib/errors';
import { DingzEvent } from './lib/dingzEventBus';
import { PlatformEvent } from './lib/platformEventBus';
import { DingzDaBaseAccessory } from './lib/dingzDaBaseAccessory';

// Policy for long running tasks, retry every hour
Expand Down Expand Up @@ -120,13 +120,13 @@ export class MyStromPIRAccessory extends DingzDaBaseAccessory {

// Subscribe to the REQUEST_STATE_UPDATE event
this.platform.eb.on(
DingzEvent.REQUEST_STATE_UPDATE,
PlatformEvent.REQUEST_STATE_UPDATE,
this.getDeviceStateUpdate.bind(this),
);

if (!(this.platform.config.motionPoller ?? true)) {
// Implement *push* event handling
this.platform.eb.on(DingzEvent.ACTION, (mac, action) => {
this.platform.eb.on(PlatformEvent.ACTION, (mac, action) => {
this.log.debug(`Processing DingzEvent.ACTION ${action}`);

if (mac === this.device.mac) {
Expand Down
4 changes: 2 additions & 2 deletions src/myStromSwitchAccessory.ts
Expand Up @@ -9,7 +9,7 @@ import type {

import { DingzDaHomebridgePlatform } from './platform';
import { MyStromDeviceInfo, MyStromSwitchReport } from './lib/myStromTypes';
import { DingzEvent } from './lib/dingzEventBus';
import { PlatformEvent } from './lib/platformEventBus';
import { DingzDaBaseAccessory } from './lib/dingzDaBaseAccessory';

/**
Expand Down Expand Up @@ -109,7 +109,7 @@ export class MyStromSwitchAccessory extends DingzDaBaseAccessory {

// Subscribe to the REQUEST_STATE_UPDATE event
this.platform.eb.on(
DingzEvent.REQUEST_STATE_UPDATE,
PlatformEvent.REQUEST_STATE_UPDATE,
this.getDeviceStateUpdate.bind(this),
);
}
Expand Down

0 comments on commit 982ff2a

Please sign in to comment.