From 0c614667b3657ce5d82414fc5626367c1c2f7e0b Mon Sep 17 00:00:00 2001 From: Haim Kastner Date: Sun, 5 Jul 2020 01:54:30 +0300 Subject: [PATCH] Support Tasmota Air-Conditioning IR Transmitter #148 - Implement cache and IR/RF commands cache manager - Add FetchCommandsAvailable option to the device kind - Update docs - Fix grammar --- README.md | 1 + backend/.vscode/settings.json | 8 + backend/package-lock.json | 5 + backend/package.json | 1 + backend/scripts/buildVersion.js | 4 +- backend/src/business-layer/minionsBl.ts | 6 +- backend/src/business-layer/timingsBl.ts | 2 +- backend/src/controllers/iftttController.ts | 4 +- backend/src/controllers/minionsController.ts | 6 +- backend/src/models/sharedInterfaces.d.ts | 24 +- backend/src/modules/brandModuleBase.ts | 45 +-- .../src/modules/broadlink/broadlinkHandler.ts | 377 ++++-------------- backend/src/modules/ifttt/iftttHandler.ts | 69 ++-- backend/src/modules/miio/miioHandler.ts | 30 +- backend/src/modules/mock/mockHandler.ts | 73 ++-- backend/src/modules/modulesManager.ts | 24 +- backend/src/modules/mqtt/mqttHandler.ts | 69 ++-- backend/src/modules/orvibo/orviboHandler.ts | 21 +- backend/src/modules/tasmota/tasmotaHandler.ts | 134 +++++-- backend/src/modules/tuya/tuyaHandler.ts | 66 +-- .../src/modules/yeelight/yeelightHandler.ts | 18 +- backend/src/routers/routes.ts | 19 +- backend/src/utilities/cacheManager.ts | 307 ++++++++++++++ backend/swagger.yaml | 38 +- .../unit/data-layer/timingsDal.mock.spec.ts | 2 +- .../unit/modules/modulesManager.mock.spec.ts | 40 +- .../src/app/core/sidebar/sidebar.component.ts | 2 +- .../minions/minions.component.html | 4 +- .../minions/minions.component.ts | 10 +- .../timings/timings.component.html | 2 +- .../create-minion-dialog.component.ts | 4 +- .../create-timing-dialog.component.html | 2 +- .../create-timing-dialog.component.ts | 8 +- frontend/src/app/services/minions.service.ts | 6 +- 34 files changed, 810 insertions(+), 621 deletions(-) create mode 100644 backend/.vscode/settings.json create mode 100644 backend/src/utilities/cacheManager.ts diff --git a/README.md b/README.md index 8d9220f1..be3651e2 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ In addination I made a [Linux deployment tutorial](./docs/LINUX.md) for a Linux - Tasmota - Switch (tested with [this](https://www.gearbest.com/robot-vacuum-accessories/pp_009661965579.html?wid=1433363) and [this](https://www.gearbest.com/alarm-systems/pp_009227681096.html?wid=1433363)) + - Air-conditioning (IR Transmitter) (tested with [this](https://www.aliexpress.com/item/33004692351.html) (after [flashing to Tasmota](https://blog.castnet.club/en/blog/flashing-tasmota-on-tuya-ir-bridge)) - [IFTTT](https://ifttt.com/discover) module. [module use documentation](./backend/src/modules/ifttt/README.md).. diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json new file mode 100644 index 00000000..37f5f4c9 --- /dev/null +++ b/backend/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "cSpell.words": [ + "Tasmota", + "Tuya", + "Yeelight", + "broadlink" + ] +} \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 7bc5a299..3ce28df5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1213,6 +1213,11 @@ "through2": "^2.0.0" } }, + "broadlink-ir-converter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/broadlink-ir-converter/-/broadlink-ir-converter-1.0.1.tgz", + "integrity": "sha512-eJXShRfC9ZoWcfy+tMJu3o6/m6XY3DCZR5WZ78o/15/Im1u7y4BhIjjCGoVfPhA/dPq/5Svof4NNaByFcxCWhQ==" + }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", diff --git a/backend/package.json b/backend/package.json index ed140f74..4c4dbea1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -44,6 +44,7 @@ "await-semaphore": "^0.1.3", "bcryptjs": "^2.4.3", "body-parser": "^1.19.0", + "broadlink-ir-converter": "^1.0.1", "chai": "^4.2.0", "chai-http": "^4.3.0", "child-process-promise": "^2.2.1", diff --git a/backend/scripts/buildVersion.js b/backend/scripts/buildVersion.js index bb25652d..feb5bd1c 100644 --- a/backend/scripts/buildVersion.js +++ b/backend/scripts/buildVersion.js @@ -6,14 +6,14 @@ const git = simplegit(); const buildVersionInfo = async() => { const tags = await git.tags(); - const commintHash = await git.revparse(['--short', 'HEAD']); + const commitHash = await git.revparse(['--short', 'HEAD']); const rawTimestamp = await git.show(['-s', '--format=%ct']); const timestamp = +rawTimestamp * 1000; fse.writeFileSync(path.join(__dirname, '../dist', 'versionInfo.json'), JSON.stringify({ version: tags.latest, - commintHash, + commitHash, timestamp, })); } diff --git a/backend/src/business-layer/minionsBl.ts b/backend/src/business-layer/minionsBl.ts index 4915e37d..87036419 100644 --- a/backend/src/business-layer/minionsBl.ts +++ b/backend/src/business-layer/minionsBl.ts @@ -674,7 +674,7 @@ export class MinionsBl { /** * Check if token reqired and not exist. */ - if (deviceKind.isTokenRequierd && !minionToCheck.device.token) { + if (deviceKind.isTokenRequired && !minionToCheck.device.token) { return { responseCode: 2409, message: 'token is requird', @@ -684,7 +684,7 @@ export class MinionsBl { /** * Check if id reqired and not exist. */ - if (deviceKind.isIdRequierd && !minionToCheck.device.deviceId) { + if (deviceKind.isIdRequired && !minionToCheck.device.deviceId) { return { responseCode: 3409, message: 'id is required', @@ -716,7 +716,7 @@ export class MinionsBl { /** * ignore user selection and set corrent minion type based on model. */ - minionToCheck.minionType = deviceKind.suppotedMinionType; + minionToCheck.minionType = deviceKind.supportedMinionType; } /** diff --git a/backend/src/business-layer/timingsBl.ts b/backend/src/business-layer/timingsBl.ts index 38e45753..6d8512bf 100644 --- a/backend/src/business-layer/timingsBl.ts +++ b/backend/src/business-layer/timingsBl.ts @@ -227,7 +227,7 @@ export class TimingsBl { */ private async timeoutTiming(now: Moment, timing: Timing, timingProperties: TimeoutTiming): Promise { const timeoutMoment = moment(timingProperties.startDate); - timeoutMoment.add(timingProperties.durationInMimutes, 'minute'); + timeoutMoment.add(timingProperties.durationInMinutes, 'minute'); /** * If its new trigger timing. diff --git a/backend/src/controllers/iftttController.ts b/backend/src/controllers/iftttController.ts index 2d7c712c..d95daf26 100644 --- a/backend/src/controllers/iftttController.ts +++ b/backend/src/controllers/iftttController.ts @@ -20,7 +20,7 @@ import { IftttActionTriggered, IftttActionTriggeredRequest, IftttIntegrationSettings, - IftttRawActionTriggerd, + IftttRawActionTriggered, } from '../models/sharedInterfaces'; @Tags('Ifttt') @@ -59,7 +59,7 @@ export class IftttController extends Controller { @Response(501, 'Server error') @Security('iftttAuth') @Post('/trigger/minions/raw/') - public async triggeredSomeAction(@Body() iftttRawActionTriggerd: IftttRawActionTriggerd): Promise { + public async triggeredSomeAction(@Body() iftttRawActionTriggerd: IftttRawActionTriggered): Promise { const { apiKey, minionId, setStatus } = iftttRawActionTriggerd; await IftttIntegrationBlSingleton.triggeredMinionAction(minionId, { apiKey, diff --git a/backend/src/controllers/minionsController.ts b/backend/src/controllers/minionsController.ts index ed69a1c8..c8b483dc 100644 --- a/backend/src/controllers/minionsController.ts +++ b/backend/src/controllers/minionsController.ts @@ -25,7 +25,7 @@ import { MinionSetRoomName, MinionStatus, MinionTimeline, - ScaningStatus, + ScanningStatus, SetMinionAutoTurnOff, VersionUpdateStatus, } from '../models/sharedInterfaces'; @@ -140,9 +140,9 @@ export class MinionsController extends Controller { @Security('adminAuth') @Response(501, 'Server error') @Get('rescan') - public async getSescaningMinionsStatus(): Promise { + public async getSescaningMinionsStatus(): Promise { return { - scaningStatus: await MinionsBlSingleton.getScaningStatus(), + scanningStatus: await MinionsBlSingleton.getScaningStatus(), }; } diff --git a/backend/src/models/sharedInterfaces.d.ts b/backend/src/models/sharedInterfaces.d.ts index 92a3a25e..d976aa33 100644 --- a/backend/src/models/sharedInterfaces.d.ts +++ b/backend/src/models/sharedInterfaces.d.ts @@ -139,22 +139,28 @@ export declare interface DeviceKind { /** * Is the device require a token for communication API. */ - isTokenRequierd: boolean; + isTokenRequired: boolean; /** * Is device require id for communication API. */ - isIdRequierd: boolean; + isIdRequired: boolean; /** * Supported minion type for the current device. */ - suppotedMinionType: MinionTypes; + supportedMinionType: MinionTypes; /** * Some of the devices supported recording (for example IR transmitter). */ isRecordingSupported: boolean; + + /** + * Whenever the device and module supported fetching commands data from + * the https://github.com/casanet/rf-commands-repo project + */ + isFetchCommandsAvailable: boolean; } /** @@ -378,7 +384,7 @@ export declare interface TimeoutTiming { /** * Duration to activate timing from the start timeout time in minutes. */ - durationInMimutes: number; + durationInMinutes: number; } /** @@ -707,8 +713,8 @@ export declare interface IftttActionTriggered extends IftttActionTriggeredReques setStatus: SwitchOptions; } -/** Ifttt trigger with all request data in one JSON struct. */ -export declare interface IftttRawActionTriggerd extends IftttActionTriggeredRequest { +/** Ifttt trigger with all request data in one JSON structure. */ +export declare interface IftttRawActionTriggered extends IftttActionTriggeredRequest { minionId: string; setStatus: SwitchOptions; } @@ -724,7 +730,7 @@ export declare interface VersionInfo { /** Latest version (Git Tag) name */ version: string; /** Current local master/HEAD commit hash */ - commintHash: string; + commitHash: string; /** Time stamp of HEAD commit in UTC format */ timestamp: number; } @@ -733,8 +739,8 @@ export declare interface VersionInfo { export declare type ProgressStatus = 'inProgress' | 'finished' | 'fail'; /** Scanning progress status */ -export declare interface ScaningStatus { - scaningStatus: ProgressStatus; +export declare interface ScanningStatus { + scanningStatus: ProgressStatus; } /** Version update progress status */ diff --git a/backend/src/modules/brandModuleBase.ts b/backend/src/modules/brandModuleBase.ts index 5d6a16d4..36bb2761 100644 --- a/backend/src/modules/brandModuleBase.ts +++ b/backend/src/modules/brandModuleBase.ts @@ -1,4 +1,3 @@ -import * as fse from 'fs-extra'; import * as path from 'path'; import { PullBehavior } from 'pull-behavior'; import { BehaviorSubject } from 'rxjs'; @@ -16,7 +15,7 @@ export abstract class BrandModuleBase { /** * Cache file pull path. */ - private get cacheFilePath(): string { + protected get cacheFilePath(): string { return `${path.join(CACHE_DIRECTORY, this.brandName)}.json`; } @@ -47,16 +46,16 @@ export abstract class BrandModuleBase { /** * Get current status of minion. (such as minion status on off etc.) - * @param miniom minion to get status for. + * @param minion minion to get status for. */ - public abstract getStatus(miniom: Minion): Promise; + public abstract getStatus(minion: Minion): Promise; /** * Set minion new status. (such as turn minion on off etc.) - * @param miniom minion to set status for. + * @param minion minion to set status for. * @param setStatus the new status to set. */ - public abstract setStatus(miniom: Minion, setStatus: MinionStatus): Promise; + public abstract setStatus(minion: Minion, setStatus: MinionStatus): Promise; /** * Record data for currrent minion status. @@ -89,38 +88,4 @@ export abstract class BrandModuleBase { * Used for cleaning up communication before re-reading data, after communication auth changed or just hard reset module etc. */ public abstract refreshCommunication(): Promise; - - /** - * Get cache JSON data sync. - * Use it in init only. else the app will black until read finish. - */ - protected getCacheDataSync(): any { - try { - return fse.readJSONSync(this.cacheFilePath); - } catch (error) { - return undefined; - } - } - - /** - * Get cache JSON data. - */ - protected async getCacheData(): Promise { - const data = await fse.readJSON(this.cacheFilePath).catch(err => { - logger.warn(`Fail to read ${this.cacheFilePath} cache file, ${err}`); - throw new Error('Fail to read cache data'); - }); - return data; - } - - /** - * Save JSON to module cache. - * @param data Data to save in cache. - */ - protected async setCacheData(data: any): Promise { - await fse.outputFile(this.cacheFilePath, JSON.stringify(data, null, 2)).catch(err => { - logger.warn(`Fail to write ${this.cacheFilePath} cache file, ${err}`); - throw new Error('Fail to write cache data'); - }); - } } diff --git a/backend/src/modules/broadlink/broadlinkHandler.ts b/backend/src/modules/broadlink/broadlinkHandler.ts index c27e5ec0..9362aa47 100644 --- a/backend/src/modules/broadlink/broadlinkHandler.ts +++ b/backend/src/modules/broadlink/broadlinkHandler.ts @@ -15,6 +15,7 @@ import { MinionStatus, SwitchOptions, } from '../../models/sharedInterfaces'; +import { CommandsCacheManager } from '../../utilities/cacheManager'; import { Delay } from '../../utilities/sleep'; import { BrandModuleBase } from '../brandModuleBase'; @@ -31,73 +32,63 @@ interface BroadlinkAPI { setPower: (status: boolean, callback: (err: any) => void) => void; } -interface Cache { - minionId: string; - lastStatus: MinionStatus; - toggleCommands?: ToggleCommands; - acCommands?: AcCommands; - rollerCommands?: RollerCommands; -} - export class BroadlinkHandler extends BrandModuleBase { + public readonly brandName: string = 'broadlink'; public readonly devices: DeviceKind[] = [ { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: 1, model: 'SP2', - suppotedMinionType: 'switch', + supportedMinionType: 'switch', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'RM3 / RM Pro as IR AC', - suppotedMinionType: 'airConditioning', + supportedMinionType: 'airConditioning', isRecordingSupported: true, + isFetchCommandsAvailable: true, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'RM Pro as RF toggle', - suppotedMinionType: 'toggle', + supportedMinionType: 'toggle', isRecordingSupported: true, + isFetchCommandsAvailable: true, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'RM Pro as RF roller', - suppotedMinionType: 'roller', + supportedMinionType: 'roller', isRecordingSupported: true, + isFetchCommandsAvailable: true, }, ]; - private cache: Cache[] = []; - constructor() { - super(); - const cache = super.getCacheDataSync(); - if (cache) { - this.cache = cache; - } - } + private commandsCacheManager = new CommandsCacheManager(super.cacheFilePath) - public async getStatus(miniom: Minion): Promise { - switch (miniom.device.model) { + public async getStatus(minion: Minion): Promise { + switch (minion.device.model) { case 'SP2': - return await this.getSP2Status(miniom); + return await this.getSP2Status(minion); case 'RM Pro as RF toggle': case 'RM3 / RM Pro as IR AC': case 'RM Pro as RF roller': - return await this.getCachedStatus(miniom); + return await this.getCachedStatus(minion); } throw { responseCode: 8404, @@ -105,16 +96,16 @@ export class BroadlinkHandler extends BrandModuleBase { } as ErrorResponse; } - public async setStatus(miniom: Minion, setStatus: MinionStatus): Promise { - switch (miniom.device.model) { + public async setStatus(minion: Minion, setStatus: MinionStatus): Promise { + switch (minion.device.model) { case 'SP2': - return await this.setSP2Status(miniom, setStatus); + return await this.setSP2Status(minion, setStatus); case 'RM Pro as RF toggle': - return await this.setRFToggleStatus(miniom, setStatus); + return await this.setRFToggleStatus(minion, setStatus); case 'RM3 / RM Pro as IR AC': - return await this.setIRACStatus(miniom, setStatus); + return await this.setIrAcStatus(minion, setStatus); case 'RM Pro as RF roller': - return await this.setRFRollerStatus(miniom, setStatus); + return await this.setRFRollerStatus(minion, setStatus); } throw { responseCode: 8404, @@ -122,14 +113,14 @@ export class BroadlinkHandler extends BrandModuleBase { } as ErrorResponse; } - public async enterRecordMode(miniom: Minion, statusToRecordFor: MinionStatus): Promise { - switch (miniom.device.model) { + public async enterRecordMode(minion: Minion, statusToRecordFor: MinionStatus): Promise { + switch (minion.device.model) { case 'RM Pro as RF toggle': - return await this.recordRFToggleCommands(miniom, statusToRecordFor); + return await this.recordRFToggleCommands(minion, statusToRecordFor); case 'RM3 / RM Pro as IR AC': - return await this.recordIRACCommands(miniom, statusToRecordFor); + return await this.recordIRACommands(minion, statusToRecordFor); case 'RM Pro as RF roller': - return await this.recordRollerRFCommand(miniom, statusToRecordFor); + return await this.recordRollerRFCommand(minion, statusToRecordFor); } throw { responseCode: 8404, @@ -137,12 +128,12 @@ export class BroadlinkHandler extends BrandModuleBase { } as ErrorResponse; } - public async generateCommand(miniom: Minion, statusToRecordFor: MinionStatus): Promise { - switch (miniom.device.model) { + public async generateCommand(minion: Minion, statusToRecordFor: MinionStatus): Promise { + switch (minion.device.model) { case 'RM Pro as RF toggle': - return await this.generateToggleRFCommand(miniom, statusToRecordFor); + return await this.generateToggleRFCommand(minion, statusToRecordFor); case 'RM Pro as RF roller': - return await this.generateRollerRFCommand(miniom, statusToRecordFor); + return await this.generateRollerRFCommand(minion, statusToRecordFor); } throw { responseCode: 8404, @@ -151,77 +142,19 @@ export class BroadlinkHandler extends BrandModuleBase { } public async setFetchedCommands(minion: Minion, commandsSet: CommandsSet): Promise { - const minionCache = this.getOrCreateMinionCache(minion); - - switch (minion.minionType) { - case 'toggle': - minionCache.toggleCommands = commandsSet.commands.toggle; - break; - case 'airConditioning': - minionCache.acCommands = commandsSet.commands.airConditioning; - break; - case 'roller': - minionCache.rollerCommands = commandsSet.commands.roller; - break; - } - this.updateCache(); + await this.commandsCacheManager.setFetchedCommands(minion, commandsSet); } public async refreshCommunication(): Promise { // There's nothing to do. } - private updateCache() { - this.setCacheData(this.cache) - .then(() => {}) - .catch(() => {}); - } - - private getOrCreateMinionCache(miniom: Minion): Cache { - for (const minionCache of this.cache) { - if (minionCache.minionId === miniom.minionId) { - return minionCache; - } - } - - /** Case there is not cache struct for minion, create it */ - const newMinionCache: Cache = { - minionId: miniom.minionId, - lastStatus: undefined, - }; - - this.cache.push(newMinionCache); - this.updateCache(); - return newMinionCache; - } - - /** - * Get IR command (HEX string) for given status. for AC only. - * @param airConditioningCommands array of all commands to find command in. - * @param airConditioningStatus The AC status to get command for. - * @returns IR code struct or undefined if not exist. - */ - private getMinionACStatusCommand( - airConditioningCommands: AirConditioningCommand[], - airConditioningStatus: AirConditioning, - ): AirConditioningCommand { - for (const airConditioningCommand of airConditioningCommands) { - if ( - airConditioningCommand.status.fanStrength === airConditioningStatus.fanStrength && - airConditioningCommand.status.mode === airConditioningStatus.mode && - airConditioningCommand.status.temperature === airConditioningStatus.temperature - ) { - return airConditioningCommand; - } - } - } - /** Get broadlink protocol handler instance for given minion */ - private async getBroadlinkInstance(minoin: Minion): Promise { + private async getBroadlinkInstance(minion: Minion): Promise { return new Promise((resolve, reject) => { const broadlinkDevice = new Broadlink( - { address: minoin.device.pysicalDevice.ip, port: 80 }, - minoin.device.pysicalDevice.mac, + { address: minion.device.pysicalDevice.ip, port: 80 }, + minion.device.pysicalDevice.mac, err => { if (err) { reject({ @@ -306,24 +239,16 @@ export class BroadlinkHandler extends BrandModuleBase { /** * Get last status, use in all devices that not holing any data, such as IR transmitter. - * @param miniom minion to get last status for. + * @param minion minion to get last status for. */ - private async getCachedStatus(miniom: Minion): Promise { - await this.getBroadlinkInstance(miniom); - - const minionCache = this.getOrCreateMinionCache(miniom); - if (!minionCache.lastStatus) { - throw { - responseCode: 5503, - message: 'Current status is unknown, no history for current one-way transmitter', - } as ErrorResponse; - } - - return minionCache.lastStatus; + private async getCachedStatus(minion: Minion): Promise { + // Detect if the broadlink device communication is alive + await this.getBroadlinkInstance(minion); + return await this.commandsCacheManager.getCachedStatus(minion); } - private async getSP2Status(miniom: Minion): Promise { - const broadlink = (await this.getBroadlinkInstance(miniom)) as BroadlinkAPI; + private async getSP2Status(minion: Minion): Promise { + const broadlink = (await this.getBroadlinkInstance(minion)) as BroadlinkAPI; const status = (await this.getBroadlinkPowerMode(broadlink)) as SwitchOptions; return { @@ -333,202 +258,65 @@ export class BroadlinkHandler extends BrandModuleBase { }; } - private async setSP2Status(miniom: Minion, setStatus: MinionStatus): Promise { - const broadlink = (await this.getBroadlinkInstance(miniom)) as BroadlinkAPI; + private async setSP2Status(minion: Minion, setStatus: MinionStatus): Promise { + const broadlink = (await this.getBroadlinkInstance(minion)) as BroadlinkAPI; await this.setBroadlinkPowerMode(broadlink, setStatus.switch.status); } - private async setRFToggleStatus(miniom: Minion, setStatus: MinionStatus): Promise { - const broadlink = (await this.getBroadlinkInstance(miniom)) as BroadlinkAPI; - - const minionCache = this.getOrCreateMinionCache(miniom); - - if (!minionCache.toggleCommands) { - throw { - responseCode: 4503, - message: 'there is no availble command. record a on off commands set.', - } as ErrorResponse; - } + private async setRFToggleStatus(minion: Minion, setStatus: MinionStatus): Promise { + const broadlink = (await this.getBroadlinkInstance(minion)) as BroadlinkAPI; - const hexCommandCode = - setStatus.toggle.status === 'on' ? minionCache.toggleCommands.on : minionCache.toggleCommands.off; + const hexCommandCode = await this.commandsCacheManager.getRFToggleCommand(minion, setStatus) as string; - if (!hexCommandCode) { - throw { - responseCode: 4503, - message: 'there is no availble command. record a on off commands set.', - } as ErrorResponse; - } await this.sendBeamCommand(broadlink, hexCommandCode); - minionCache.lastStatus = setStatus; - this.updateCache(); + await this.commandsCacheManager.setLastStatus(minion, setStatus); } - private async setRFRollerStatus(miniom: Minion, setStatus: MinionStatus): Promise { - const broadlink = (await this.getBroadlinkInstance(miniom)) as BroadlinkAPI; - - const minionCache = this.getOrCreateMinionCache(miniom); + private async setRFRollerStatus(minion: Minion, setStatus: MinionStatus): Promise { + const broadlink = (await this.getBroadlinkInstance(minion)) as BroadlinkAPI; - if (!minionCache.rollerCommands) { - throw { - responseCode: 4503, - message: 'there is no availble command. record a roller commands set.', - } as ErrorResponse; - } - - const hexCommandCode = - setStatus.roller.status === 'off' - ? minionCache.rollerCommands.off - : setStatus.roller.direction === 'up' - ? minionCache.rollerCommands.up - : minionCache.rollerCommands.down; - - if (!hexCommandCode) { - throw { - responseCode: 4503, - message: 'there is no availble command. record a roller commands set.', - } as ErrorResponse; - } + const hexCommandCode = await this.commandsCacheManager.getRFToggleCommand(minion, setStatus) as string; await this.sendBeamCommand(broadlink, hexCommandCode); - minionCache.lastStatus = setStatus; - this.updateCache(); + await this.commandsCacheManager.setLastStatus(minion, setStatus); } - private async setIRACStatus(miniom: Minion, setStatus: MinionStatus): Promise { - const broadlink = (await this.getBroadlinkInstance(miniom)) as BroadlinkAPI; - - const minionCache = this.getOrCreateMinionCache(miniom); - - if (!minionCache.acCommands) { - throw { - responseCode: 3503, - message: 'there is no any command', - } as ErrorResponse; - } - - let hexCommandCode: string; - - /** - * If the request is to set off, get the off command. - */ - if (setStatus.airConditioning.status === 'off') { - hexCommandCode = minionCache.acCommands.off; - } else { - /** - * Else try to get the correct command for given status to set. - */ - const acCommand = this.getMinionACStatusCommand(minionCache.acCommands.statusCommands, setStatus.airConditioning); - - /** If there is command, get it. */ - hexCommandCode = acCommand ? acCommand.command : ''; - } + private async setIrAcStatus(minion: Minion, setStatus: MinionStatus): Promise { + const broadlink = (await this.getBroadlinkInstance(minion)) as BroadlinkAPI; - if (!hexCommandCode) { - throw { - responseCode: 4503, - message: 'there is no availble command for current status. record a new command.', - } as ErrorResponse; - } + const hexCommandCode = await this.commandsCacheManager.getIrCommand(minion, setStatus) as string; await this.sendBeamCommand(broadlink, hexCommandCode); /** In case AC has missed the sent command, send it again. */ await Delay(moment.duration(1, 'seconds')); await this.sendBeamCommand(broadlink, hexCommandCode); - minionCache.lastStatus = setStatus; - this.updateCache(); + await this.commandsCacheManager.setLastStatus(minion, setStatus); } - private async recordIRACCommands(miniom: Minion, statusToRecordFor: MinionStatus): Promise { - const broadlink = (await this.getBroadlinkInstance(miniom)) as BroadlinkAPI; - - const minionCache = this.getOrCreateMinionCache(miniom); - - if (!minionCache.acCommands) { - minionCache.acCommands = { - off: '', - statusCommands: [], - }; - } + private async recordIRACommands(minion: Minion, statusToRecordFor: MinionStatus): Promise { + const broadlink = (await this.getBroadlinkInstance(minion)) as BroadlinkAPI; const hexIRCommand = await this.enterBeamLearningMode(broadlink); - /** If status is off, jusr save it. */ - if (statusToRecordFor.airConditioning.status === 'off') { - minionCache.acCommands.off = hexIRCommand; - } else { - /** Else, get record objec if exsit and update command */ - let statusCommand = this.getMinionACStatusCommand( - minionCache.acCommands.statusCommands, - statusToRecordFor.airConditioning, - ); - - /** If command object not exist yet, create new one and add it to commands array */ - if (!statusCommand) { - statusCommand = { - command: '', - status: statusToRecordFor.airConditioning, - }; - minionCache.acCommands.statusCommands.push(statusCommand); - } - - statusCommand.command = hexIRCommand; - } - this.updateCache(); + await this.commandsCacheManager.setIRACommands(minion, statusToRecordFor, hexIRCommand); } - private async recordRollerRFCommand(miniom: Minion, statusToRecordFor: MinionStatus): Promise { - const broadlink = (await this.getBroadlinkInstance(miniom)) as BroadlinkAPI; - - const minionCache = this.getOrCreateMinionCache(miniom); - - if (!minionCache.rollerCommands) { - minionCache.rollerCommands = { - up: '', - down: '', - off: '', - }; - } - + private async recordRollerRFCommand(minion: Minion, statusToRecordFor: MinionStatus): Promise { + const broadlink = (await this.getBroadlinkInstance(minion)) as BroadlinkAPI; const hexIRCommand = await this.enterBeamLearningMode(broadlink); - - if (statusToRecordFor.roller.status === 'off') { - minionCache.rollerCommands.off = hexIRCommand; - } else if (statusToRecordFor.roller.direction === 'up') { - minionCache.rollerCommands.up = hexIRCommand; - } else { - minionCache.rollerCommands.down = hexIRCommand; - } - - this.updateCache(); + await this.commandsCacheManager.setRollerRFCommand(minion, statusToRecordFor, hexIRCommand); } private async generateToggleRFCommand( - miniom: Minion, + minion: Minion, statusToRecordFor: MinionStatus, ): Promise { const generatedCode = BroadlinkCodeGeneration.generate('RF433'); - - const minionCache = this.getOrCreateMinionCache(miniom); - - if (!minionCache.toggleCommands) { - minionCache.toggleCommands = { - on: undefined, - off: undefined, - }; - } - - if (statusToRecordFor.toggle.status === 'on') { - minionCache.toggleCommands.on = generatedCode; - } else { - minionCache.toggleCommands.off = generatedCode; - } - - this.updateCache(); + await this.commandsCacheManager.setToggleRFCommand(minion, statusToRecordFor, generatedCode); } private async generateRollerRFCommand( @@ -536,29 +324,10 @@ export class BroadlinkHandler extends BrandModuleBase { statusToRecordFor: MinionStatus, ): Promise { const generatedCode = BroadlinkCodeGeneration.generate('RF433'); - - const minionCache = this.getOrCreateMinionCache(minion); - - if (!minionCache.rollerCommands) { - minionCache.rollerCommands = { - down: undefined, - up: undefined, - off: undefined, - }; - } - - if (statusToRecordFor.roller.status === 'off') { - minionCache.rollerCommands.off = generatedCode; - } else if (statusToRecordFor.roller.direction === 'up') { - minionCache.rollerCommands.up = generatedCode; - } else { - minionCache.rollerCommands.down = generatedCode; - } - - this.updateCache(); + await this.commandsCacheManager.setRollerRFCommand(minion, statusToRecordFor, generatedCode); } - private async recordRFToggleCommands(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + private async recordRFToggleCommands(minion: Minion, statusToRecordFor: MinionStatus): Promise { // TODO: swap and then record. throw { responseCode: 5501, diff --git a/backend/src/modules/ifttt/iftttHandler.ts b/backend/src/modules/ifttt/iftttHandler.ts index 6a6d4fc2..70339020 100644 --- a/backend/src/modules/ifttt/iftttHandler.ts +++ b/backend/src/modules/ifttt/iftttHandler.ts @@ -14,66 +14,73 @@ export class IftttHandler extends BrandModuleBase { public readonly devices: DeviceKind[] = [ { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: true, + isTokenRequired: false, + isIdRequired: true, minionsPerDevice: -1, model: 'toggle', - suppotedMinionType: 'toggle', + supportedMinionType: 'toggle', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: true, + isTokenRequired: false, + isIdRequired: true, minionsPerDevice: -1, model: 'switch', - suppotedMinionType: 'switch', + supportedMinionType: 'switch', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: true, + isTokenRequired: false, + isIdRequired: true, minionsPerDevice: -1, model: 'air conditioning', - suppotedMinionType: 'airConditioning', + supportedMinionType: 'airConditioning', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: true, + isTokenRequired: false, + isIdRequired: true, minionsPerDevice: -1, model: 'light', - suppotedMinionType: 'light', + supportedMinionType: 'light', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: true, + isTokenRequired: false, + isIdRequired: true, minionsPerDevice: -1, model: 'temperature light', - suppotedMinionType: 'temperatureLight', + supportedMinionType: 'temperatureLight', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: true, + isTokenRequired: false, + isIdRequired: true, minionsPerDevice: -1, model: 'color light', - suppotedMinionType: 'colorLight', + supportedMinionType: 'colorLight', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: true, + isTokenRequired: false, + isIdRequired: true, minionsPerDevice: -1, model: 'roller', - suppotedMinionType: 'roller', + supportedMinionType: 'roller', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, ]; @@ -81,16 +88,16 @@ export class IftttHandler extends BrandModuleBase { super(); } - public async getStatus(miniom: Minion): Promise { + public async getStatus(minion: Minion): Promise { /** Currently there is no API to get the real current status. */ - return miniom.minionStatus; + return minion.minionStatus; } - public async setStatus(miniom: Minion, setStatus: MinionStatus): Promise { - let triggerPayload = `${miniom.minionId}-${setStatus[miniom.minionType].status}`; + public async setStatus(minion: Minion, setStatus: MinionStatus): Promise { + let triggerPayload = `${minion.minionId}-${setStatus[minion.minionType].status}`; - if (setStatus[miniom.minionType].status === 'on') { - switch (miniom.minionType) { + if (setStatus[minion.minionType].status === 'on') { + switch (minion.minionType) { case 'airConditioning': // tslint:disable-next-line:max-line-length triggerPayload += `-${setStatus.airConditioning.mode}-${setStatus.airConditioning.fanStrength}-${setStatus.airConditioning.temperature}`; @@ -113,9 +120,9 @@ export class IftttHandler extends BrandModuleBase { try { // tslint:disable-next-line:max-line-length - await request(`https://maker.ifttt.com/trigger/${triggerPayload}/with/key/${miniom.device.deviceId}`); + await request(`https://maker.ifttt.com/trigger/${triggerPayload}/with/key/${minion.device.deviceId}`); } catch (error) { - logger.warn(`Sent IFTTT trigger for ${miniom.minionId} fail, ${JSON.stringify(error.message)}`); + logger.warn(`Sent IFTTT trigger for ${minion.minionId} fail, ${JSON.stringify(error.message)}`); throw { responseCode: 7409, message: 'Ifttt triggger fail.', @@ -123,14 +130,14 @@ export class IftttHandler extends BrandModuleBase { } } - public async enterRecordMode(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async enterRecordMode(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 6409, message: 'the ifttt module not support any recording mode', } as ErrorResponse; } - public async generateCommand(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async generateCommand(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 6409, message: 'the ifttt module not support any recording mode', diff --git a/backend/src/modules/miio/miioHandler.ts b/backend/src/modules/miio/miioHandler.ts index 7eab709a..d1fc229e 100644 --- a/backend/src/modules/miio/miioHandler.ts +++ b/backend/src/modules/miio/miioHandler.ts @@ -23,33 +23,35 @@ export class MiioHandler extends BrandModuleBase { public readonly devices: DeviceKind[] = [ { brand: this.brandName, - isTokenRequierd: true, - isIdRequierd: false, + isTokenRequired: true, + isIdRequired: false, minionsPerDevice: 1, model: 'Robot vacuum', - suppotedMinionType: 'cleaner', + supportedMinionType: 'cleaner', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: true, - isIdRequierd: false, + isTokenRequired: true, + isIdRequired: false, minionsPerDevice: 1, model: 'Philips ceiling', - suppotedMinionType: 'temperatureLight', + supportedMinionType: 'temperatureLight', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, ]; constructor() { super(); } - public async getStatus(miniom: Minion): Promise { + public async getStatus(minion: Minion): Promise { try { - const device = await miio.device({ address: miniom.device.pysicalDevice.ip, token: miniom.device.token }); + const device = await miio.device({ address: minion.device.pysicalDevice.ip, token: minion.device.token }); let currentStatus: MinionStatus; - switch (miniom.minionType) { + switch (minion.minionType) { case 'cleaner': currentStatus = { cleaner: await this.getVaccumStatus(device), @@ -78,11 +80,11 @@ export class MiioHandler extends BrandModuleBase { } } - public async setStatus(miniom: Minion, setStatus: MinionStatus): Promise { + public async setStatus(minion: Minion, setStatus: MinionStatus): Promise { try { - const device = await miio.device({ address: miniom.device.pysicalDevice.ip, token: miniom.device.token }); + const device = await miio.device({ address: minion.device.pysicalDevice.ip, token: minion.device.token }); - switch (miniom.minionType) { + switch (minion.minionType) { case 'cleaner': await this.setVaccumStatus(device, setStatus.cleaner); break; @@ -105,14 +107,14 @@ export class MiioHandler extends BrandModuleBase { } } - public async enterRecordMode(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async enterRecordMode(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 6409, message: 'the miio module not support any recording mode', } as ErrorResponse; } - public async generateCommand(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async generateCommand(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 6409, message: 'the miio module not support any recording mode', diff --git a/backend/src/modules/mock/mockHandler.ts b/backend/src/modules/mock/mockHandler.ts index d2be687c..0ceaf280 100644 --- a/backend/src/modules/mock/mockHandler.ts +++ b/backend/src/modules/mock/mockHandler.ts @@ -13,66 +13,73 @@ export class MockHandler extends BrandModuleBase { public readonly devices: DeviceKind[] = [ { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: 1, model: 'switch demo', - suppotedMinionType: 'switch', + supportedMinionType: 'switch', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'ac demo', - suppotedMinionType: 'airConditioning', + supportedMinionType: 'airConditioning', isRecordingSupported: true, + isFetchCommandsAvailable: true, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'RF toggle demo', - suppotedMinionType: 'toggle', + supportedMinionType: 'toggle', isRecordingSupported: true, + isFetchCommandsAvailable: true, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'Light demo', - suppotedMinionType: 'light', + supportedMinionType: 'light', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'Temperature Light demo', - suppotedMinionType: 'temperatureLight', + supportedMinionType: 'temperatureLight', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'Color Light demo', - suppotedMinionType: 'colorLight', + supportedMinionType: 'colorLight', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'Roller demo', - suppotedMinionType: 'roller', + supportedMinionType: 'roller', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, ]; /** @@ -122,10 +129,10 @@ export class MockHandler extends BrandModuleBase { }); }, this.AC_CHANGED_INTERVAL.asMilliseconds()); } - public async getStatus(miniom: Minion): Promise { + public async getStatus(minion: Minion): Promise { await Delay(moment.duration(0.5, 'seconds')); // Here shuold be the real communication with device. - switch (miniom.device.model) { + switch (minion.device.model) { case 'switch demo': return { switch: { @@ -182,16 +189,16 @@ export class MockHandler extends BrandModuleBase { } as ErrorResponse; } - public async setStatus(miniom: Minion, setStatus: MinionStatus): Promise { + public async setStatus(minion: Minion, setStatus: MinionStatus): Promise { await Delay(moment.duration(0.5, 'seconds')); // Here shuold be the real communication with device. if ( - miniom.device.model === 'switch demo' || - miniom.device.model === 'ac demo' || - miniom.device.model === 'RF toggle demo' || - miniom.device.model === 'Roller demo' || - miniom.device.model === 'Light demo' || - miniom.device.model === 'Temperature Light demo' || - miniom.device.model === 'Color Light demo' + minion.device.model === 'switch demo' || + minion.device.model === 'ac demo' || + minion.device.model === 'RF toggle demo' || + minion.device.model === 'Roller demo' || + minion.device.model === 'Light demo' || + minion.device.model === 'Temperature Light demo' || + minion.device.model === 'Color Light demo' ) { return; } @@ -202,11 +209,11 @@ export class MockHandler extends BrandModuleBase { } as ErrorResponse; } - public async enterRecordMode(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async enterRecordMode(minion: Minion, statusToRecordFor: MinionStatus): Promise { await Delay(moment.duration(0.5, 'seconds')); // Here shuold be the real communication with device. } - public async generateCommand(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async generateCommand(minion: Minion, statusToRecordFor: MinionStatus): Promise { await Delay(moment.duration(0.5, 'seconds')); // Here shuold be the real command generation. } diff --git a/backend/src/modules/modulesManager.ts b/backend/src/modules/modulesManager.ts index f15fba89..3a1b25eb 100644 --- a/backend/src/modules/modulesManager.ts +++ b/backend/src/modules/modulesManager.ts @@ -66,19 +66,19 @@ export class ModulesManager { * Get current status of minion. (such as minion status on off etc.) */ @MutexMinionsAccess - public async getStatus(miniom: Minion): Promise { - const minionModule = this.getMinionModule(miniom.device.brand); + public async getStatus(minion: Minion): Promise { + const minionModule = this.getMinionModule(minion.device.brand); if (!minionModule) { const errorResponse: ErrorResponse = { responseCode: 7404, - message: `there is not module for -${miniom.device.brand}- brand`, + message: `there is not module for -${minion.device.brand}- brand`, }; throw errorResponse; } try { - return await withTimeout(minionModule.getStatus(miniom), this.COMMUNICATE_DEVICE_TIMEOUT.asMilliseconds()); + return await withTimeout(minionModule.getStatus(minion), this.COMMUNICATE_DEVICE_TIMEOUT.asMilliseconds()); } catch (error) { if (typeof error.message === 'string' && error.message.indexOf('Promise not resolved after') !== -1) { throw { @@ -93,24 +93,24 @@ export class ModulesManager { /** * Set minion new status. (such as turn minion on off etc.) - * @param miniom minion to set status for. + * @param minion minion to set status for. * @param setStatus the new status to set. */ @MutexMinionsAccess - public async setStatus(miniom: Minion, setStatus: MinionStatus): Promise { - const minionModule = this.getMinionModule(miniom.device.brand); + public async setStatus(minion: Minion, setStatus: MinionStatus): Promise { + const minionModule = this.getMinionModule(minion.device.brand); if (!minionModule) { const errorResponse: ErrorResponse = { responseCode: 7404, - message: `there is not module for -${miniom.device.brand}- brand`, + message: `there is not module for -${minion.device.brand}- brand`, }; throw errorResponse; } try { return await withTimeout( - minionModule.setStatus(miniom, setStatus), + minionModule.setStatus(minion, setStatus), this.COMMUNICATE_DEVICE_TIMEOUT.asMilliseconds(), ); } catch (error) { @@ -235,12 +235,12 @@ export class ModulesManager { throw errorResponse; } - /** Make sure that minion supprt recording */ + /** Make sure that minion support recording */ const modelKind = this.getModelKind(minionModule, minion.device); - if (!modelKind || !modelKind.isRecordingSupported) { + if (!modelKind || !modelKind.isFetchCommandsAvailable) { const errorResponse: ErrorResponse = { responseCode: 6409, - message: `the minioin not support command recording or sending`, + message: `the minion not support command recording or sending`, }; throw errorResponse; } diff --git a/backend/src/modules/mqtt/mqttHandler.ts b/backend/src/modules/mqtt/mqttHandler.ts index bbf749a9..831383bc 100644 --- a/backend/src/modules/mqtt/mqttHandler.ts +++ b/backend/src/modules/mqtt/mqttHandler.ts @@ -16,66 +16,73 @@ export class MqttHandler extends BrandModuleBase { public readonly devices: DeviceKind[] = [ { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'toggle', - suppotedMinionType: 'toggle', + supportedMinionType: 'toggle', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'switch', - suppotedMinionType: 'switch', + supportedMinionType: 'switch', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'air conditioning', - suppotedMinionType: 'airConditioning', + supportedMinionType: 'airConditioning', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'light', - suppotedMinionType: 'light', + supportedMinionType: 'light', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'temperature light', - suppotedMinionType: 'temperatureLight', + supportedMinionType: 'temperatureLight', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'color light', - suppotedMinionType: 'colorLight', + supportedMinionType: 'colorLight', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'roller', - suppotedMinionType: 'roller', + supportedMinionType: 'roller', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, ]; @@ -92,25 +99,25 @@ export class MqttHandler extends BrandModuleBase { this.loadMqttBroker(); } - public async getStatus(miniom: Minion): Promise { - await this.mqttClient.publish(`get/casanet/${miniom.minionId}`, ''); + public async getStatus(minion: Minion): Promise { + await this.mqttClient.publish(`get/casanet/${minion.minionId}`, ''); /** Current there is no option to 'ask' and wait for respone, only to send request and the update will arrive by status topic. */ - return miniom.minionStatus; + return minion.minionStatus; } - public async setStatus(miniom: Minion, setStatus: MinionStatus): Promise { + public async setStatus(minion: Minion, setStatus: MinionStatus): Promise { /** Publish set status topic */ - await this.mqttClient.publish(`set/casanet/${miniom.minionId}`, JSON.stringify(setStatus)); + await this.mqttClient.publish(`set/casanet/${minion.minionId}`, JSON.stringify(setStatus)); } - public async enterRecordMode(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async enterRecordMode(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 6409, message: 'the mqtt module not support any recording mode', } as ErrorResponse; } - public async generateCommand(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async generateCommand(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 6409, message: 'the mqtt module not support any recording mode', @@ -140,7 +147,7 @@ export class MqttHandler extends BrandModuleBase { } } - /** Load broker (or init new one if not configurate one) */ + /** Load broker (or init new one if not configured one) */ private async loadMqttBroker() { /** If there is broker set by env vars */ if (mqttBrokerUri) { @@ -150,7 +157,7 @@ export class MqttHandler extends BrandModuleBase { return; } - logger.info(`There is no MQTT_BROKER_IP env var, invokeing internal mqtt broker.`); + logger.info(`There is no MQTT_BROKER_IP env var, invoking internal mqtt broker.`); this.mqttBroker = new MqttBroker(); /** Get broker port */ @@ -196,7 +203,7 @@ export class MqttHandler extends BrandModuleBase { } if (!status[minion.minionType]) { - logger.warn(`MQTT message for ${topic}:${payload.toString()} data not matchs the minion type`); + logger.warn(`MQTT message for ${topic}:${payload.toString()} data not match's the minion type`); return; } diff --git a/backend/src/modules/orvibo/orviboHandler.ts b/backend/src/modules/orvibo/orviboHandler.ts index 96c31bd3..f0e03a86 100644 --- a/backend/src/modules/orvibo/orviboHandler.ts +++ b/backend/src/modules/orvibo/orviboHandler.ts @@ -13,12 +13,13 @@ export class OrviboHandler extends BrandModuleBase { public readonly devices: DeviceKind[] = [ { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: 1, model: 'S20', - suppotedMinionType: 'switch', + supportedMinionType: 'switch', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, ]; @@ -123,14 +124,14 @@ export class OrviboHandler extends BrandModuleBase { }); } - public async enterRecordMode(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async enterRecordMode(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 5010, message: 'the orvibo module not support any recording mode', } as ErrorResponse; } - public async generateCommand(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async generateCommand(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 6409, message: 'the orvibo module not support any recording mode', @@ -232,9 +233,9 @@ export class OrviboHandler extends BrandModuleBase { /** * Re-subscribe to current orivbo device, use to know the status * (orvibo send it by subscribe and button pressed only) and also to alow set status. - * @param miniom The minion of device. + * @param minion The minion of device. */ - private async reSubsribeOrvibo(miniom: Minion) { + private async reSubsribeOrvibo(minion: Minion) { /** If there is no connection, try to init it */ if (!this.orviboCommunication) { try { @@ -249,17 +250,17 @@ export class OrviboHandler extends BrandModuleBase { } /** Reload device each time befor sending data using UDP */ - const currentOrviboDevice = this.orviboCommunication.getDevice(miniom.device.pysicalDevice.mac); + const currentOrviboDevice = this.orviboCommunication.getDevice(minion.device.pysicalDevice.mac); if (currentOrviboDevice) { this.orviboCommunication.devices.splice(this.orviboCommunication.devices.indexOf(currentOrviboDevice), 1); } /** Create device object */ const orvibo = { - macAddress: miniom.device.pysicalDevice.mac, + macAddress: minion.device.pysicalDevice.mac, macPadding: '202020202020', type: 'Socket', - ip: miniom.device.pysicalDevice.ip, + ip: minion.device.pysicalDevice.ip, // Takes the last character from the message and turns it into a boolean. // This is our socket's initial state state: false, diff --git a/backend/src/modules/tasmota/tasmotaHandler.ts b/backend/src/modules/tasmota/tasmotaHandler.ts index 62146f6a..458acbfe 100644 --- a/backend/src/modules/tasmota/tasmotaHandler.ts +++ b/backend/src/modules/tasmota/tasmotaHandler.ts @@ -1,11 +1,14 @@ +import { broadlinkToPulesArray, pulesArrayToBroadlink } from 'broadlink-ir-converter'; import * as moment from 'moment'; import { Duration } from 'moment'; import * as request from 'request-promise'; import { BehaviorSubject } from 'rxjs'; import { CommandsSet } from '../../models/backendInterfaces'; import { DeviceKind, ErrorResponse, Minion, MinionStatus, SwitchOptions, Toggle } from '../../models/sharedInterfaces'; +import { CommandsCacheManager } from '../../utilities/cacheManager'; import { DeepCopy } from '../../utilities/deepCopy'; import { logger } from '../../utilities/logger'; +import { Delay } from '../../utilities/sleep'; import { BrandModuleBase } from '../brandModuleBase'; export class TasmotaHandler extends BrandModuleBase { @@ -14,30 +17,83 @@ export class TasmotaHandler extends BrandModuleBase { public readonly devices: DeviceKind[] = [ { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: 1, model: 'switch', - suppotedMinionType: 'switch', + supportedMinionType: 'switch', isRecordingSupported: false, + isFetchCommandsAvailable: false, + }, + { + brand: this.brandName, + isTokenRequired: false, + isIdRequired: false, + minionsPerDevice: -1, + model: 'IR Transmitter', + supportedMinionType: 'airConditioning', + isRecordingSupported: false, + isFetchCommandsAvailable: true, }, ]; + private commandsCacheManager = new CommandsCacheManager(super.cacheFilePath) + constructor() { super(); } - public async getStatus(miniom: Minion): Promise { + public async getStatus(minion: Minion): Promise { + switch (minion.minionType) { + case 'switch': + return await this.getSwitchStatus(minion); + case 'airConditioning': + return await this.getAcStatus(minion); + } + } + + public async setStatus(minion: Minion, setStatus: MinionStatus): Promise { + switch (minion.minionType) { + case 'switch': + return await this.setSwitchStatus(minion, setStatus); + case 'airConditioning': + return await this.setAcStatus(minion, setStatus); + } + } + + public async enterRecordMode(minion: Minion, statusToRecordFor: MinionStatus): Promise { + throw { + responseCode: 6409, + message: 'the tasmota module not support any recording mode', + } as ErrorResponse; + } + + public async generateCommand(minion: Minion, statusToRecordFor: MinionStatus): Promise { + throw { + responseCode: 6409, + message: 'the tasmota module not support any recording mode', + } as ErrorResponse; + } + + public async setFetchedCommands(minion: Minion, commandsSet: CommandsSet): Promise { + await this.commandsCacheManager.setFetchedCommands(minion, commandsSet); + } + + public async refreshCommunication(): Promise { + // There's nothing to do. + } + + private async getSwitchStatus(minion: Minion): Promise { try { - const tosmotaStatus = await request(`http://${miniom.device.pysicalDevice.ip}/cm?cmnd=Power`); - const status = JSON.parse(tosmotaStatus).POWER.toLowerCase() as SwitchOptions; + const tasmotaStatus = await request(`http://${minion.device.pysicalDevice.ip}/cm?cmnd=Power`); + const status = JSON.parse(tasmotaStatus).POWER.toLowerCase() as SwitchOptions; return { switch: { status, }, }; } catch (error) { - logger.warn(`Sent Tosmota command ${miniom.minionId} fail, ${JSON.stringify(error.message)}`); + logger.warn(`Sent Tosmota command ${minion.minionId} fail, ${JSON.stringify(error.message)}`); throw { responseCode: 1503, message: 'tosmota request fail.', @@ -45,37 +101,61 @@ export class TasmotaHandler extends BrandModuleBase { } } - public async setStatus(miniom: Minion, setStatus: MinionStatus): Promise { + private async getAcStatus(minion: Minion): Promise { try { - await request(`http://${miniom.device.pysicalDevice.ip}/cm?cmnd=Power%20${setStatus[miniom.minionType].status}`); + await request(`http://${minion.device.pysicalDevice.ip}/cm?cmnd=State`); + return await this.commandsCacheManager.getCachedStatus(minion); + } catch (error) { - logger.warn(`Sent TOsmota command ${miniom.minionId} fail, ${JSON.stringify(error.message)}`); + logger.warn(`Sent Tasmota command ${minion.minionId} fail, ${JSON.stringify(error.message)}`); throw { responseCode: 1503, - message: 'tosmota request fail.', + message: 'tasmota request fail.', } as ErrorResponse; } } - public async enterRecordMode(miniom: Minion, statusToRecordFor: MinionStatus): Promise { - throw { - responseCode: 6409, - message: 'the tosmota module not support any recording mode', - } as ErrorResponse; + private async setSwitchStatus(minion: Minion, setStatus: MinionStatus): Promise { + try { + await request(`http://${minion.device.pysicalDevice.ip}/cm?cmnd=Power%20${setStatus[minion.minionType].status}`); + } catch (error) { + logger.warn(`Sent Tasmota command ${minion.minionId} fail, ${JSON.stringify(error.message)}`); + throw { + responseCode: 1503, + message: 'tasmota request fail.', + } as ErrorResponse; + } } - public async generateCommand(miniom: Minion, statusToRecordFor: MinionStatus): Promise { - throw { - responseCode: 6409, - message: 'the tosmota module not support any recording mode', - } as ErrorResponse; - } + private async setAcStatus(minion: Minion, setStatus: MinionStatus): Promise { + try { + const hexCommandCode = await this.commandsCacheManager.getIrCommand(minion, setStatus) as string; + // Convert the broadlink command format to the pules array + const pulesArray = broadlinkToPulesArray(hexCommandCode); - public async setFetchedCommands(minion: Minion, commandsSet: CommandsSet): Promise { - // There's nothing to do. - } + // Convert the pules array to string separated by coma + let pulsString = (pulesArray.reduce((pulesString, pules) => { + return `${pulesString}${pules},`; + }, '')); - public async refreshCommunication(): Promise { - // There's nothing to do. + // Remove the last coma + pulsString = pulsString.substring(0, pulsString.length - 1); + + const irSendFullUrl = `http://${minion.device.pysicalDevice.ip}/cm?cmnd=IRsend%20${pulsString}`; + await request(irSendFullUrl); + await Delay(moment.duration(1, 'seconds')); + const rawResults = await request(irSendFullUrl); + const results = JSON.parse(rawResults); + if (results.IRSend !== 'Done') { + throw new Error(`[tasmotaHandler.setAcStatus] Sending IR command failed ${JSON.stringify(results)}`); + } + await this.commandsCacheManager.setLastStatus(minion, setStatus); + } catch (error) { + logger.warn(`Sent Tasmota command ${minion.minionId} fail, ${JSON.stringify(error.message)}`); + throw { + responseCode: 1503, + message: 'tosmota request fail.', + } as ErrorResponse; + } } } diff --git a/backend/src/modules/tuya/tuyaHandler.ts b/backend/src/modules/tuya/tuyaHandler.ts index be2ca142..2d6b1f81 100644 --- a/backend/src/modules/tuya/tuyaHandler.ts +++ b/backend/src/modules/tuya/tuyaHandler.ts @@ -20,39 +20,43 @@ export class TuyaHandler extends BrandModuleBase { public readonly devices: DeviceKind[] = [ { brand: this.brandName, - isTokenRequierd: true, - isIdRequierd: true, + isTokenRequired: true, + isIdRequired: true, minionsPerDevice: 3, model: 'wall switch, 3 gangs, first one', - suppotedMinionType: 'switch', + supportedMinionType: 'switch', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: true, - isIdRequierd: true, + isTokenRequired: true, + isIdRequired: true, minionsPerDevice: 3, model: 'wall switch, 3 gangs, second one', - suppotedMinionType: 'switch', + supportedMinionType: 'switch', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: true, - isIdRequierd: true, + isTokenRequired: true, + isIdRequired: true, minionsPerDevice: 3, model: 'wall switch, 3 gangs, third one', - suppotedMinionType: 'switch', + supportedMinionType: 'switch', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: true, - isIdRequierd: true, + isTokenRequired: true, + isIdRequired: true, minionsPerDevice: 1, model: 'curtain', - suppotedMinionType: 'roller', + supportedMinionType: 'roller', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, ]; @@ -68,17 +72,17 @@ export class TuyaHandler extends BrandModuleBase { super(); } - public async getStatus(miniom: Minion): Promise { + public async getStatus(minion: Minion): Promise { /** * Get tuya device instance */ - const tuyaDevice = await this.getTuyaDevice(miniom.device); + const tuyaDevice = await this.getTuyaDevice(minion.device); - if (miniom.device.model.indexOf('curtain') !== -1) { + if (minion.device.model.indexOf('curtain') !== -1) { try { const rowStatus = await tuyaDevice.get(); - this.releasDevice(tuyaDevice, miniom.device); + this.releaseDevice(tuyaDevice, minion.device); return { roller: { @@ -87,7 +91,7 @@ export class TuyaHandler extends BrandModuleBase { }, }; } catch (err) { - logger.warn(`Fail to get status of ${miniom.minionId}, ${err}`); + logger.warn(`Fail to get status of ${minion.minionId}, ${err}`); if (typeof err === 'object' && err.message === 'fffffffffffffff') { throw { @@ -105,7 +109,7 @@ export class TuyaHandler extends BrandModuleBase { } const stausResult = await tuyaDevice.get({ schema: true }).catch((err: Error) => { - logger.warn(`Fail to get status of ${miniom.minionId}, ${err}`); + logger.warn(`Fail to get status of ${minion.minionId}, ${err}`); if ( typeof err === 'object' && @@ -137,7 +141,7 @@ export class TuyaHandler extends BrandModuleBase { * Extract the current minion status. */ let currentGangStatus: boolean; - switch (miniom.device.model) { + switch (minion.device.model) { case 'wall switch, 3 gangs, first one': currentGangStatus = stausResult.dps[1]; break; @@ -149,7 +153,7 @@ export class TuyaHandler extends BrandModuleBase { break; } - this.releasDevice(tuyaDevice, miniom.device); + this.releaseDevice(tuyaDevice, minion.device); return { switch: { @@ -158,22 +162,22 @@ export class TuyaHandler extends BrandModuleBase { }; } - public async setStatus(miniom: Minion, setStatus: MinionStatus): Promise { + public async setStatus(minion: Minion, setStatus: MinionStatus): Promise { /** * Get tuya device instance */ - const tuyaDevice = await this.getTuyaDevice(miniom.device); + const tuyaDevice = await this.getTuyaDevice(minion.device); - if (miniom.device.model.indexOf('curtain') !== -1) { + if (minion.device.model.indexOf('curtain') !== -1) { try { await tuyaDevice.set({ set: setStatus.roller.status === 'off' ? '3' : setStatus.roller.direction === 'up' ? '1' : '2', }); - this.releasDevice(tuyaDevice, miniom.device); + this.releaseDevice(tuyaDevice, minion.device); return; } catch (err) { - logger.warn(`Fail to get status of ${miniom.minionId}, ${err}`); + logger.warn(`Fail to get status of ${minion.minionId}, ${err}`); if ( typeof err === 'object' && @@ -198,7 +202,7 @@ export class TuyaHandler extends BrandModuleBase { * Get current minion gang index. */ let gangIndex: number; - switch (miniom.device.model) { + switch (minion.device.model) { case 'wall switch, 3 gangs, first one': gangIndex = 1; break; @@ -211,7 +215,7 @@ export class TuyaHandler extends BrandModuleBase { } await tuyaDevice.set({ set: setStatus.switch.status === 'on', dps: gangIndex }).catch(err => { - logger.warn(`Fail to get status of ${miniom.minionId}, ${err}`); + logger.warn(`Fail to get status of ${minion.minionId}, ${err}`); if ( typeof err === 'object' && @@ -231,17 +235,17 @@ export class TuyaHandler extends BrandModuleBase { } as ErrorResponse; }); - this.releasDevice(tuyaDevice, miniom.device); + this.releaseDevice(tuyaDevice, minion.device); } - public async enterRecordMode(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async enterRecordMode(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 6409, message: 'the tuya module not support any recording mode', } as ErrorResponse; } - public async generateCommand(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async generateCommand(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 6409, message: 'the tuya module not support any recording mode', @@ -298,7 +302,7 @@ export class TuyaHandler extends BrandModuleBase { * @param tuyaDevice * @param minionDevice */ - private async releasDevice(tuyaDevice: Tuyapi, minionDevice: MinionDevice) { + private async releaseDevice(tuyaDevice: Tuyapi, minionDevice: MinionDevice) { // Keep the device this.pysicalDevicesMap[minionDevice.pysicalDevice.mac] = tuyaDevice; diff --git a/backend/src/modules/yeelight/yeelightHandler.ts b/backend/src/modules/yeelight/yeelightHandler.ts index e1f7c27f..208bbe8c 100644 --- a/backend/src/modules/yeelight/yeelightHandler.ts +++ b/backend/src/modules/yeelight/yeelightHandler.ts @@ -25,21 +25,23 @@ export class YeelightHandler extends BrandModuleBase { public readonly devices: DeviceKind[] = [ { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: 1, model: 'Temperature light', - suppotedMinionType: 'temperatureLight', + supportedMinionType: 'temperatureLight', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: this.brandName, - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: 1, model: 'RGBW light', - suppotedMinionType: 'colorLight', + supportedMinionType: 'colorLight', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, ]; @@ -159,14 +161,14 @@ export class YeelightHandler extends BrandModuleBase { } } - public async enterRecordMode(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async enterRecordMode(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 6409, message: 'the yeelight module not support any recording mode', } as ErrorResponse; } - public async generateCommand(miniom: Minion, statusToRecordFor: MinionStatus): Promise { + public async generateCommand(minion: Minion, statusToRecordFor: MinionStatus): Promise { throw { responseCode: 6409, message: 'the yeelight module not support any recording mode', diff --git a/backend/src/routers/routes.ts b/backend/src/routers/routes.ts index e5942abc..3dde7a54 100644 --- a/backend/src/routers/routes.ts +++ b/backend/src/routers/routes.ts @@ -161,7 +161,7 @@ const models: TsoaRoute.Models = { "TimeoutTiming": { "properties": { "startDate": { "dataType": "double", "required": true }, - "durationInMimutes": { "dataType": "double", "required": true }, + "durationInMinutes": { "dataType": "double", "required": true }, }, }, "TimingProperties": { @@ -202,10 +202,11 @@ const models: TsoaRoute.Models = { "brand": { "dataType": "string", "required": true }, "model": { "dataType": "string", "required": true }, "minionsPerDevice": { "dataType": "double", "required": true }, - "isTokenRequierd": { "dataType": "boolean", "required": true }, - "isIdRequierd": { "dataType": "boolean", "required": true }, - "suppotedMinionType": { "dataType": "enum", "enums": ["toggle", "switch", "roller", "cleaner", "airConditioning", "light", "temperatureLight", "colorLight"], "required": true }, + "isTokenRequired": { "dataType": "boolean", "required": true }, + "isIdRequired": { "dataType": "boolean", "required": true }, + "supportedMinionType": { "dataType": "enum", "enums": ["toggle", "switch", "roller", "cleaner", "airConditioning", "light", "temperatureLight", "colorLight"], "required": true }, "isRecordingSupported": { "dataType": "boolean", "required": true }, + "isFetchCommandsAvailable": { "dataType": "boolean", "required": true }, }, }, "MinionTimeline": { @@ -230,9 +231,9 @@ const models: TsoaRoute.Models = { "setAutoTurnOffMS": { "dataType": "double", "required": true }, }, }, - "ScaningStatus": { + "ScanningStatus": { "properties": { - "scaningStatus": { "dataType": "enum", "enums": ["inProgress", "finished", "fail"], "required": true }, + "scanningStatus": { "dataType": "enum", "enums": ["inProgress", "finished", "fail"], "required": true }, }, }, "IftttOnChanged": { @@ -281,7 +282,7 @@ const models: TsoaRoute.Models = { "enableIntegration": { "dataType": "boolean", "required": true }, }, }, - "IftttRawActionTriggerd": { + "IftttRawActionTriggered": { "properties": { "apiKey": { "dataType": "string", "required": true }, "localMac": { "dataType": "string" }, @@ -315,7 +316,7 @@ const models: TsoaRoute.Models = { "VersionInfo": { "properties": { "version": { "dataType": "string", "required": true }, - "commintHash": { "dataType": "string", "required": true }, + "commitHash": { "dataType": "string", "required": true }, "timestamp": { "dataType": "double", "required": true }, }, }, @@ -1624,7 +1625,7 @@ export function RegisterRoutes(app: express.Express) { authenticateMiddleware([{ "iftttAuth": [] }]), function(request: any, response: any, next: any) { const args = { - iftttRawActionTriggerd: { "in": "body", "name": "iftttRawActionTriggerd", "required": true, "ref": "IftttRawActionTriggerd" }, + iftttRawActionTriggerd: { "in": "body", "name": "iftttRawActionTriggerd", "required": true, "ref": "IftttRawActionTriggered" }, }; let validatedArgs: any[] = []; diff --git a/backend/src/utilities/cacheManager.ts b/backend/src/utilities/cacheManager.ts new file mode 100644 index 00000000..825bebd8 --- /dev/null +++ b/backend/src/utilities/cacheManager.ts @@ -0,0 +1,307 @@ +import * as fse from 'fs-extra'; +import { logger } from './logger'; +import { MinionStatus, Minion, ErrorResponse, AirConditioning } from '../models/sharedInterfaces'; +import { ToggleCommands, AcCommands, RollerCommands, CommandsSet, AirConditioningCommand } from '../models/backendInterfaces'; + + +export interface CommandsCache { + minionId: string; + lastStatus: MinionStatus; + toggleCommands?: ToggleCommands; + acCommands?: AcCommands; + rollerCommands?: RollerCommands; +} + +/** + * A simple json cache implementation to use for any module needs. + */ +export class CacheManager { + constructor(private cacheFilePath: string) { + + } + + /** + * Get cache JSON data sync. + * Use it in init only. else the app will black until read finish. + */ + public getCacheDataSync(): any { + try { + return fse.readJSONSync(this.cacheFilePath); + } catch (error) { + return undefined; + } + } + + /** + * Get cache JSON data. + */ + public async getCacheData(): Promise { + const data = await fse.readJSON(this.cacheFilePath).catch(err => { + logger.warn(`Fail to read ${this.cacheFilePath} cache file, ${err}`); + throw new Error('Fail to read cache data'); + }); + return data; + } + + /** + * Save JSON to module cache. + * @param data Data to save in cache. + */ + public async setCacheData(data: any): Promise { + await fse.outputFile(this.cacheFilePath, JSON.stringify(data, null, 2)).catch(err => { + logger.warn(`Fail to write ${this.cacheFilePath} cache file, ${err}`); + throw new Error('Fail to write cache data'); + }); + } +} + +/** + * Ir/Rf commands cache manager, used to get and update devices commands + * and get and update last devices status + */ +export class CommandsCacheManager extends CacheManager { + // The commands of a device + public cache: CommandsCache[] = []; + + constructor(cacheFilePath: string) { + super(cacheFilePath); + const cache = super.getCacheDataSync(); + if (cache) { + this.cache = cache; + } + } + + /** Save the current cache json to the cache json file */ + private async saveCache() { + try { + await this.setCacheData(this.cache); + } catch (error) { + + } + } + + /** + * Get minion cache commands and status, if not exists create it. + * @param minion + */ + private getOrCreateMinionCache(minion: Minion): CommandsCache { + for (const minionCache of this.cache) { + if (minionCache.minionId === minion.minionId) { + return minionCache; + } + } + + /** Case there is not cache structure for minion, create it */ + const newMinionCache: CommandsCache = { + minionId: minion.minionId, + lastStatus: undefined, + }; + + this.cache.push(newMinionCache); + this.saveCache(); + return newMinionCache; + } + + /** + * Get IR command (HEX string) for given status. for AC only. + * @param airConditioningCommands array of all commands to find command in. + * @param airConditioningStatus The AC status to get command for. + * @returns IR code struct or undefined if not exist. + */ + private getMinionACStatusCommand( + airConditioningCommands: AirConditioningCommand[], + airConditioningStatus: AirConditioning, + ): AirConditioningCommand { + for (const airConditioningCommand of airConditioningCommands) { + if ( + airConditioningCommand.status.fanStrength === airConditioningStatus.fanStrength && + airConditioningCommand.status.mode === airConditioningStatus.mode && + airConditioningCommand.status.temperature === airConditioningStatus.temperature + ) { + return airConditioningCommand; + } + } + } + + /** + * Override minion commands with the new fetched commands set + * @param minion + * @param commandsSet + */ + public async setFetchedCommands(minion: Minion, commandsSet: CommandsSet) { + const minionCache = this.getOrCreateMinionCache(minion); + + switch (minion.minionType) { + case 'toggle': + minionCache.toggleCommands = commandsSet.commands.toggle; + break; + case 'airConditioning': + minionCache.acCommands = commandsSet.commands.airConditioning; + break; + case 'roller': + minionCache.rollerCommands = commandsSet.commands.roller; + break; + } + await this.saveCache(); + } + + /** + * Get last status, use in all devices that not holing any data, such as IR transmitter. + * @param minion minion to get last status for. + */ + public async getCachedStatus(minion: Minion): Promise { + const minionCache = this.getOrCreateMinionCache(minion); + if (!minionCache.lastStatus) { + throw { + responseCode: 5503, + message: 'Current status is unknown, no history for current one-way transmitter', + } as ErrorResponse; + } + + return minionCache.lastStatus; + } + + public async getRFToggleCommand(minion: Minion, status: MinionStatus): Promise { + + const minionCache = this.getOrCreateMinionCache(minion); + + if (!minionCache.toggleCommands) { + throw { + responseCode: 4503, + message: 'there is no available command. record a on off commands set.', + } as ErrorResponse; + } + + const hexCommandCode = + status.toggle.status === 'on' ? minionCache.toggleCommands.on : minionCache.toggleCommands.off; + + if (!hexCommandCode) { + throw { + responseCode: 4503, + message: 'there is no available command. record a on off commands set.', + } as ErrorResponse; + } + + return hexCommandCode; + } + + public async getIrCommand(minion: Minion, setStatus: MinionStatus): Promise { + + const minionCache = this.getOrCreateMinionCache(minion); + + if (!minionCache.acCommands) { + throw { + responseCode: 3503, + message: 'there is no any command', + } as ErrorResponse; + } + + let hexCommandCode: string; + + /** + * If the request is to set off, get the off command. + */ + if (setStatus.airConditioning.status === 'off') { + hexCommandCode = minionCache.acCommands.off; + } else { + /** + * Else try to get the correct command for given status to set. + */ + const acCommand = this.getMinionACStatusCommand(minionCache.acCommands.statusCommands, setStatus.airConditioning); + + /** If there is command, get it. */ + hexCommandCode = acCommand ? acCommand.command : ''; + } + + if (!hexCommandCode) { + throw { + responseCode: 4503, + message: 'there is no availble command for current status. record a new command.', + } as ErrorResponse; + } + + return hexCommandCode; + } + + public async setLastStatus(minion: Minion, setStatus: MinionStatus) { + const minionCache = this.getOrCreateMinionCache(minion); + minionCache.lastStatus = setStatus; + await this.saveCache(); + } + + public async setIRACommands(minion: Minion, statusToRecordFor: MinionStatus, hexIRCommand: string): Promise { + + const minionCache = this.getOrCreateMinionCache(minion); + + /** If status is off, just save it. */ + if (statusToRecordFor.airConditioning.status === 'off') { + minionCache.acCommands.off = hexIRCommand; + } else { + /** Else, get record object if exist and update command */ + let statusCommand = this.getMinionACStatusCommand( + minionCache.acCommands.statusCommands, + statusToRecordFor.airConditioning, + ); + + /** If command object not exist yet, create new one and add it to commands array */ + if (!statusCommand) { + statusCommand = { + command: '', + status: statusToRecordFor.airConditioning, + }; + minionCache.acCommands.statusCommands.push(statusCommand); + } + + statusCommand.command = hexIRCommand; + } + await this.saveCache(); + } + + public async setRollerRFCommand(minion: Minion, statusToRecordFor: MinionStatus, hexRfCommand: string): Promise { + + const minionCache = this.getOrCreateMinionCache(minion); + + if (!minionCache.rollerCommands) { + minionCache.rollerCommands = { + up: '', + down: '', + off: '', + }; + } + + + if (statusToRecordFor.roller.status === 'off') { + minionCache.rollerCommands.off = hexRfCommand; + } else if (statusToRecordFor.roller.direction === 'up') { + minionCache.rollerCommands.up = hexRfCommand; + } else { + minionCache.rollerCommands.down = hexRfCommand; + } + + await this.saveCache(); + } + + public async setToggleRFCommand( + minion: Minion, + statusToRecordFor: MinionStatus, + hexRfCommand: string + ): Promise { + + const minionCache = this.getOrCreateMinionCache(minion); + + if (!minionCache.toggleCommands) { + minionCache.toggleCommands = { + on: undefined, + off: undefined, + }; + } + + if (statusToRecordFor.toggle.status === 'on') { + minionCache.toggleCommands.on = hexRfCommand; + } else { + minionCache.toggleCommands.off = hexRfCommand; + } + + await this.saveCache(); + } +} diff --git a/backend/swagger.yaml b/backend/swagger.yaml index 174abf43..3a591ead 100644 --- a/backend/swagger.yaml +++ b/backend/swagger.yaml @@ -458,13 +458,13 @@ definitions: type: number format: double description: 'UTC time.' - durationInMimutes: + durationInMinutes: type: number format: double description: 'Duration to activate timing from the start timeout time in minutes.' required: - startDate - - durationInMimutes + - durationInMinutes type: object TimingProperties: description: 'Timing properties, values depend on timing type.' @@ -567,13 +567,13 @@ definitions: type: number format: double description: "The max minions that can be in one device, or -1 if unlimited.\r\nFor example, a simple smart socket can be 1 minion per device,\r\nWall switch with 3 switches can be 3 minions per device,\r\nAnd IR transmitter can be unlimited minions per device." - isTokenRequierd: + isTokenRequired: type: boolean description: 'Is the device require a token for communication API.' - isIdRequierd: + isIdRequired: type: boolean description: 'Is device require id for communication API.' - suppotedMinionType: + supportedMinionType: type: string enum: - toggle @@ -588,14 +588,18 @@ definitions: isRecordingSupported: type: boolean description: 'Some of the devices supported recording (for example IR transmitter).' + isFetchCommandsAvailable: + type: boolean + description: "Whenever the device and module supported fetching commands data from \r\nthe https://github.com/casanet/rf-commands-repo project" required: - brand - model - minionsPerDevice - - isTokenRequierd - - isIdRequierd - - suppotedMinionType + - isTokenRequired + - isIdRequired + - supportedMinionType - isRecordingSupported + - isFetchCommandsAvailable type: object MinionTimeline: description: 'Minion timeline node' @@ -640,17 +644,17 @@ definitions: required: - setAutoTurnOffMS type: object - ScaningStatus: + ScanningStatus: description: 'Scanning progress status' properties: - scaningStatus: + scanningStatus: type: string enum: - inProgress - finished - fail required: - - scaningStatus + - scanningStatus type: object IftttOnChanged: description: "Ifttt webhook request body to notify minion status changed.\r\n*Used in ifttt module interface only*" @@ -769,8 +773,8 @@ definitions: required: - enableIntegration type: object - IftttRawActionTriggerd: - description: 'Ifttt trigger with all request data in one JSON struct.' + IftttRawActionTriggered: + description: 'Ifttt trigger with all request data in one JSON structure.' properties: apiKey: type: string @@ -850,7 +854,7 @@ definitions: version: type: string description: 'Latest version (Git Tag) name' - commintHash: + commitHash: type: string description: 'Current local master/HEAD commit hash' timestamp: @@ -859,7 +863,7 @@ definitions: description: 'Time stamp of HEAD commit in UTC format' required: - version - - commintHash + - commitHash - timestamp type: object CommandsRepoDevice: @@ -1386,7 +1390,7 @@ paths: '200': description: Ok schema: - $ref: '#/definitions/ScaningStatus' + $ref: '#/definitions/ScanningStatus' '501': description: 'Server error' schema: @@ -2340,7 +2344,7 @@ paths: name: iftttRawActionTriggerd required: true schema: - $ref: '#/definitions/IftttRawActionTriggerd' + $ref: '#/definitions/IftttRawActionTriggered' '/ifttt/trigger/minions/{minionId}': post: operationId: TriggeredMinionAction diff --git a/backend/tests/unit/data-layer/timingsDal.mock.spec.ts b/backend/tests/unit/data-layer/timingsDal.mock.spec.ts index e6022982..fb90aedc 100644 --- a/backend/tests/unit/data-layer/timingsDal.mock.spec.ts +++ b/backend/tests/unit/data-layer/timingsDal.mock.spec.ts @@ -49,7 +49,7 @@ export class TimingsDalMock { timingProperties: { timeout: { startDate: new Date().getTime(), - durationInMimutes: 1, + durationInMinutes: 1, }, }, timingType: 'timeout', diff --git a/backend/tests/unit/modules/modulesManager.mock.spec.ts b/backend/tests/unit/modules/modulesManager.mock.spec.ts index 55ca037c..455b7c40 100644 --- a/backend/tests/unit/modules/modulesManager.mock.spec.ts +++ b/backend/tests/unit/modules/modulesManager.mock.spec.ts @@ -19,52 +19,56 @@ export class ModulesManagerMock { return [ { brand: 'test mock', - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: 1, model: 'switch demo', - suppotedMinionType: 'switch', + supportedMinionType: 'switch', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: 'test mock', - isTokenRequierd: true, - isIdRequierd: false, + isTokenRequired: true, + isIdRequired: false, minionsPerDevice: 1, model: 'switch demo with token', - suppotedMinionType: 'switch', + supportedMinionType: 'switch', isRecordingSupported: false, + isFetchCommandsAvailable: false, }, { brand: 'test mock', - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'ac demo', - suppotedMinionType: 'airConditioning', + supportedMinionType: 'airConditioning', isRecordingSupported: true, + isFetchCommandsAvailable: true, }, { brand: 'test mock', - isTokenRequierd: false, - isIdRequierd: false, + isTokenRequired: false, + isIdRequired: false, minionsPerDevice: -1, model: 'ac 2 demo', - suppotedMinionType: 'airConditioning', + supportedMinionType: 'airConditioning', isRecordingSupported: true, + isFetchCommandsAvailable: false, }, ]; } - public async getStatus(miniom: Minion): Promise { + public async getStatus(minion: Minion): Promise { await Delay(moment.duration(1, 'seconds')); - if (miniom.device.model === 'switch demo') { + if (minion.device.model === 'switch demo') { return { switch: { status: 'on', }, }; - } else if (miniom.device.model === 'ac demo') { + } else if (minion.device.model === 'ac demo') { return { airConditioning: { fanStrength: 'med', @@ -80,12 +84,12 @@ export class ModulesManagerMock { } as ErrorResponse; } - public async setStatus(miniom: Minion, setStatus: MinionStatus): Promise { + public async setStatus(minion: Minion, setStatus: MinionStatus): Promise { await Delay(moment.duration(0.5, 'seconds')); // Here shuold be the real communication with device. return; } - public async refreshModules(): Promise {} + public async refreshModules(): Promise { } - public async refreshModule(brand: string): Promise {} + public async refreshModule(brand: string): Promise { } } diff --git a/frontend/src/app/core/sidebar/sidebar.component.ts b/frontend/src/app/core/sidebar/sidebar.component.ts index 92a11033..1a3f4fb1 100644 --- a/frontend/src/app/core/sidebar/sidebar.component.ts +++ b/frontend/src/app/core/sidebar/sidebar.component.ts @@ -110,7 +110,7 @@ export class SidebarComponent implements OnInit, OnDestroy { try { const currVersion = await this.settingsService.getCurrentVersion(); this.currentVersionName = currVersion.version; - this.currentVersionCommitHash = currVersion.commintHash; + this.currentVersionCommitHash = currVersion.commitHash; this.currentVersionReleaseDate = new Date(currVersion.timestamp).toLocaleDateString(); } catch (error) { this.currentVersionName = 'unknown'; diff --git a/frontend/src/app/dashboard-crm/minions/minions.component.html b/frontend/src/app/dashboard-crm/minions/minions.component.html index 0fc10982..d77d5330 100644 --- a/frontend/src/app/dashboard-crm/minions/minions.component.html +++ b/frontend/src/app/dashboard-crm/minions/minions.component.html @@ -101,12 +101,12 @@ slow_motion_video {{ 'CALIBRATION_LOCKS' | translate}} - - diff --git a/frontend/src/app/dashboard-crm/minions/minions.component.ts b/frontend/src/app/dashboard-crm/minions/minions.component.ts index fece2f82..e1cbc0d7 100644 --- a/frontend/src/app/dashboard-crm/minions/minions.component.ts +++ b/frontend/src/app/dashboard-crm/minions/minions.component.ts @@ -121,7 +121,7 @@ export class MinionsComponent implements OnInit, OnDestroy { return minionSwitchStatus.status; } - public isMinionRecordble(minion: Minion): boolean { + public isMinionRecordable(minion: Minion): boolean { for (const deviceKind of this.devicesService.devicesKinds) { if (deviceKind.brand === minion.device.brand && deviceKind.model === minion.device.model) { return deviceKind.isRecordingSupported; @@ -129,6 +129,14 @@ export class MinionsComponent implements OnInit, OnDestroy { } } + public isFetchCommandsAvailable(minion: Minion): boolean { + for (const deviceKind of this.devicesService.devicesKinds) { + if (deviceKind.brand === minion.device.brand && deviceKind.model === minion.device.model) { + return deviceKind.isFetchCommandsAvailable; + } + } + } + public getMinionColor(minion: Minion): { dark: string; light: string } { const switchStatus = this.getMinionOnOffStatus(minion, minion['updateSet']); diff --git a/frontend/src/app/dashboard-crm/timings/timings.component.html b/frontend/src/app/dashboard-crm/timings/timings.component.html index b268c02c..6f89ca3c 100644 --- a/frontend/src/app/dashboard-crm/timings/timings.component.html +++ b/frontend/src/app/dashboard-crm/timings/timings.component.html @@ -148,7 +148,7 @@
diff --git a/frontend/src/app/dialogs/create-minion-dialog/create-minion-dialog.component.ts b/frontend/src/app/dialogs/create-minion-dialog/create-minion-dialog.component.ts index 00231c1b..a6706b24 100644 --- a/frontend/src/app/dialogs/create-minion-dialog/create-minion-dialog.component.ts +++ b/frontend/src/app/dialogs/create-minion-dialog/create-minion-dialog.component.ts @@ -99,8 +99,8 @@ export class CreateMinionDialogComponent implements OnInit { if (!selectedKind) { return; } - this.requireToken = selectedKind.isTokenRequierd; - this.requireDeviceId = selectedKind.isIdRequierd; + this.requireToken = selectedKind.isTokenRequired; + this.requireDeviceId = selectedKind.isIdRequired; } public async createMinion() { diff --git a/frontend/src/app/dialogs/create-timing-dialog/create-timing-dialog.component.html b/frontend/src/app/dialogs/create-timing-dialog/create-timing-dialog.component.html index 706a96a0..1535216a 100644 --- a/frontend/src/app/dialogs/create-timing-dialog/create-timing-dialog.component.html +++ b/frontend/src/app/dialogs/create-timing-dialog/create-timing-dialog.component.html @@ -122,7 +122,7 @@ + (change)="timingProperties.durationInMinutes = $event.target.value">
diff --git a/frontend/src/app/dialogs/create-timing-dialog/create-timing-dialog.component.ts b/frontend/src/app/dialogs/create-timing-dialog/create-timing-dialog.component.ts index 1a3432b5..6ffd3660 100644 --- a/frontend/src/app/dialogs/create-timing-dialog/create-timing-dialog.component.ts +++ b/frontend/src/app/dialogs/create-timing-dialog/create-timing-dialog.component.ts @@ -127,14 +127,14 @@ export class CreateTimingDialogComponent implements OnInit { const timeoutTiming: TimeoutTiming = this.timingProperties; /** Fix duration to be number */ - timeoutTiming.durationInMimutes = - timeoutTiming.durationInMimutes - ? parseFloat(timeoutTiming.durationInMimutes as unknown as string) + timeoutTiming.durationInMinutes = + timeoutTiming.durationInMinutes + ? parseFloat(timeoutTiming.durationInMinutes as unknown as string) : undefined; /** Mark start date as 'now' */ timeoutTiming.startDate = new Date().getTime(); - return timeoutTiming.durationInMimutes; + return timeoutTiming.durationInMinutes; } } diff --git a/frontend/src/app/services/minions.service.ts b/frontend/src/app/services/minions.service.ts index c288e9d7..5543fde1 100644 --- a/frontend/src/app/services/minions.service.ts +++ b/frontend/src/app/services/minions.service.ts @@ -7,7 +7,7 @@ import { MinionTimeline, CommandsRepoDevice, ProgressStatus, - ScaningStatus, + ScanningStatus, MinionCalibrate } from '../../../../backend/src/models/sharedInterfaces'; import { HttpClient, HttpResponse, HttpErrorResponse, HttpParams } from '@angular/common/http'; @@ -87,10 +87,10 @@ export class MinionsService { try { let updateStatus: ProgressStatus = 'inProgress'; while (updateStatus === 'inProgress') { - const currentStatus = await this.httpClient.get(`${environment.baseUrl}/minions/rescan`, { + const currentStatus = await this.httpClient.get(`${environment.baseUrl}/minions/rescan`, { withCredentials: true }).toPromise(); - updateStatus = currentStatus.scaningStatus; + updateStatus = currentStatus.scanningStatus; await this.sleep(5000); }