From 7a17877a24ca69fd598ba653564c63503e7e9f11 Mon Sep 17 00:00:00 2001 From: Long Zhao Date: Mon, 12 Aug 2024 10:24:50 +1000 Subject: [PATCH] feat: retry until reach the expected state --- src/CGDGarageDoor.ts | 139 +++++++++++++++++++++++------------------- src/parseDoorState.ts | 36 +++++++++++ src/retry.ts | 10 ++- 3 files changed, 120 insertions(+), 65 deletions(-) create mode 100644 src/parseDoorState.ts diff --git a/src/CGDGarageDoor.ts b/src/CGDGarageDoor.ts index 3bdd711..415a4f2 100644 --- a/src/CGDGarageDoor.ts +++ b/src/CGDGarageDoor.ts @@ -1,5 +1,6 @@ import { Logging } from 'homebridge'; import http from 'http'; +import parseDoorState, { DoorState } from './parseDoorState'; import retry from './retry'; const httpAgent = new http.Agent({ keepAlive: true }); @@ -21,15 +22,6 @@ interface Config { deviceLocalKey: string; } -enum DoorState { - Closed, - Opened, - Stopped, - Closing, - Opening, - Error, -} - type StatusUpdateListener = () => void; export class CGDGarageDoor { @@ -46,7 +38,14 @@ export class CGDGarageDoor { this.poolStatus(); } - private run = async ({ cmd, value, softValue = value }) => { + private run = async ({ + cmd, value, + softValue = value, + until = async () => { + this.log.debug('Running without until...'); + return true; + }, + }) => { this.log.debug(`Setting ${cmd} to ${softValue}`); let oldStatus: Status; @@ -54,7 +53,7 @@ export class CGDGarageDoor { oldStatus = { ...this.status }; this.status[cmd] = softValue; - if (!this.isStatusEqual(oldStatus, this.status)) { + if (!this.isStatusEqual(oldStatus)) { this.log.debug(`Updating ${cmd} to ${softValue}`); this.statusUpdateListener?.(); } @@ -81,18 +80,22 @@ export class CGDGarageDoor { } return data; - }, { + }, until, { retries: 3, onRetry: (error, retries) => { this.log.warn(`Failed to run command [${retries} retries]: ${cmd}=${value}`); - this.log.warn(JSON.stringify(error)); + if (error instanceof Error) { + this.log.warn(`Error: ${error.message}`); + } }, onRecover: (retries) => { this.log.info(`Recovered to run command [${retries} retries]: ${cmd}=${value}`); }, onFail: (error) => { this.log.error(`Failed to run command: ${cmd}=${value}`); - this.log.error(JSON.stringify(error)); + if (error instanceof Error) { + this.log.error(`Error: ${error.message}`); + } if (oldStatus) { this.log.debug(`Reverting ${cmd}`); @@ -106,25 +109,29 @@ export class CGDGarageDoor { private withIsUpdating = async (fn: () => Promise): Promise => { this.isUpdating = true; this.log.debug('Updating is in progress...'); + await fn(); - setTimeout(() => { - this.isUpdating = false; - this.log.debug('Updating is finished'); - }, 2000); + this.isUpdating = false; + this.log.debug('Updating is finished'); }; - private refreshStatus = async () => { - this.log.debug(`Refreshing status... ${this.isUpdating}`); - if (this.isUpdating) { - this.log.debug('Skip'); + private until = (fn: (status: Status) => boolean) => async (): Promise => { + await new Promise((resolve) => setTimeout(resolve, 2000)); - return; - } + const status = await this.getStatus(); + this.log.debug(`Checking status... ${JSON.stringify(status)}`); + + return !!status && fn(status); + }; + private getStatus = async (): Promise => { this.log.debug('Getting status...'); - const data = await this.run({ cmd: 'status', value: 'json', softValue: (new Date()).toLocaleString() }); + const data = await this.run({ + cmd: 'status', value: 'json', + softValue: (new Date()).toLocaleString(), + }); if (!data) { this.log.error('Can not get status!'); @@ -132,17 +139,29 @@ export class CGDGarageDoor { return; } - if (this.status && this.isStatusEqual(this.status, data as Status)) { + return data as Status; + }; + + private refreshStatus = async () => { + this.log.debug(`Refreshing status... ${this.isUpdating}`); + if (this.isUpdating) { + this.log.debug('Skip'); + + return; + } + + const status = await this.getStatus(); + if (!status || this.isStatusEqual(status)) { return; } - this.status = data as Status; + this.status = status; this.statusUpdateListener?.(); }; - private isStatusEqual = (a: Status, b: Status) => { + private isStatusEqual = (data: Status) => { const values = ['lamp', 'door', 'vacation']; - return values.every((value) => a[value] === b[value]); + return values.every((value) => this.status?.[value] === data[value]); }; private poolStatus = async () => { @@ -150,32 +169,6 @@ export class CGDGarageDoor { setTimeout(this.poolStatus, 5000); }; - private getDoorState = (): DoorState => { - if (this.status?.door.startsWith('Closed')) { - return DoorState.Closed; - } - - if (this.status?.door.startsWith('Opened')) { - return DoorState.Opened; - } - - if (this.status?.door.startsWith('Closing')) { - return DoorState.Closing; - } - - if (this.status?.door.startsWith('Opening')) { - return DoorState.Opening; - } - - if (this.status?.door.startsWith('Stop')) { - return DoorState.Stopped; - } - - this.log.error(`[getDoorState] Unknown door status: ${this.status?.door}`); - - return DoorState.Error; - }; - public onStatusUpdate = (listener: StatusUpdateListener) => { this.statusUpdateListener = listener; }; @@ -192,7 +185,7 @@ export class CGDGarageDoor { }); public getCurrentDoorState = (): number => { - const doorState = this.getDoorState(); + const doorState = parseDoorState(this.status?.door); // export declare class CurrentDoorState extends Characteristic { // static readonly UUID: string; @@ -219,7 +212,7 @@ export class CGDGarageDoor { }; public getTargetDoorState = (): number => { - const doorState = this.getDoorState(); + const doorState = parseDoorState(this.status?.door); // export declare class TargetDoorState extends Characteristic { // static readonly UUID: string; @@ -246,13 +239,21 @@ export class CGDGarageDoor { this.withIsUpdating(async () => { if (value === 0) { this.log.debug('Opening door...'); - await this.run({ cmd: 'door', value: 'open', softValue: 'Opening' }); + await this.run({ + cmd: 'door', value: 'open', + softValue: 'Opening', + until: this.until((status) => [DoorState.Opened, DoorState.Opening, DoorState.Stopped].includes(parseDoorState(status.door))), + }); this.log.debug('Opened door!'); } if (value === 1) { this.log.debug('Closing door...'); - await this.run({ cmd: 'door', value: 'close', softValue: 'Closing' }); + await this.run({ + cmd: 'door', value: 'close', + softValue: 'Closing', + until: this.until((status) => [DoorState.Closed, DoorState.Closing, DoorState.Stopped].includes(parseDoorState(status.door))), + }); this.log.debug('Closed door!'); } }); @@ -276,13 +277,19 @@ export class CGDGarageDoor { this.withIsUpdating(async () => { if (value) { this.log.debug('Turning on lightbulb...'); - await this.run({ cmd: 'lamp', value: 'on' }); + await this.run({ + cmd: 'lamp', value: 'on', + until: this.until((status) => status.lamp === 'on'), + }); this.log.debug('Turned on lightbulb!'); } if (!value) { this.log.debug('Turning off lightbulb...'); - await this.run({ cmd: 'lamp', value: 'off' }); + await this.run({ + cmd: 'lamp', value: 'off', + until: this.until((status) => status.lamp === 'off'), + }); this.log.debug('Turned off lightbulb!'); } }); @@ -306,13 +313,19 @@ export class CGDGarageDoor { this.withIsUpdating(async () => { if (value) { this.log.debug('Turning on vacation...'); - await this.run({ cmd: 'vacation', value: 'on' }); + await this.run({ + cmd: 'vacation', value: 'on', + until: this.until((status) => status.vacation === 'on'), + }); this.log.debug('Turned on vacation!'); } if (!value) { this.log.debug('Turning off vacation...'); - await this.run({ cmd: 'vacation', value: 'off' }); + await this.run({ + cmd: 'vacation', value: 'off', + until: this.until((status) => status.vacation === 'off'), + }); this.log.debug('Turned off vacation!'); } }); diff --git a/src/parseDoorState.ts b/src/parseDoorState.ts new file mode 100644 index 0000000..8b02a8e --- /dev/null +++ b/src/parseDoorState.ts @@ -0,0 +1,36 @@ +export enum DoorState { + Closed, + Opened, + Stopped, + Closing, + Opening, + Error, +} + +export default function parseDoorState(door?: string): DoorState { + if (!door) { + return DoorState.Error; + } + + if (door.startsWith('Closed')) { + return DoorState.Closed; + } + + if (door.startsWith('Opened')) { + return DoorState.Opened; + } + + if (door.startsWith('Closing')) { + return DoorState.Closing; + } + + if (door.startsWith('Opening')) { + return DoorState.Opening; + } + + if (door.startsWith('Stop')) { + return DoorState.Stopped; + } + + return DoorState.Error; +} diff --git a/src/retry.ts b/src/retry.ts index dbecbdc..8dec61a 100644 --- a/src/retry.ts +++ b/src/retry.ts @@ -6,12 +6,18 @@ interface Config { onFail: (error: unknown) => void; } -const retry = async (fn: () => Promise, config: Config) => { +const retry = async (fn: () => Promise, until: () => Promise, config: Config) => { const { retries, onRetry, onRecover, onFail, isRetry } = config; try { const data = await fn(); + const result = await until(); + + if (!result) { + throw new Error('Failed to reach the expected state'); + } + if (isRetry) { onRecover(retries); } @@ -24,7 +30,7 @@ const retry = async (fn: () => Promise, config: Config) => { onRetry(error, retries); - return retry(fn, { + return retry(fn, until, { ...config, isRetry: true, retries: retries - 1,