diff --git a/README.md b/README.md index fa0acdb..68ebee2 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Options: -v, --version output the version number -c, --country Country code for account (default: "US") -l, --language Language code for account (default: "en-US") + -t, --token Refresh token (optional) -s, --state-path State file path (default: "wideq-state.json") -h, --help output usage information @@ -56,14 +57,14 @@ polling... ## Implementation Status -| *Device* | *Implementation* | *Control* | *Status* | +| *Device* | *Implementation* | *Status* | *Control* | | --- | --- | --- | --- | | Dehumidifier | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | AC | :heavy_check_mark: | :warning: needs testing | :warning: needs testing | | Refrigerator | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Dishwasher | :heavy_check_mark: | :x: | :x: | -| Dryer | :heavy_check_mark: | :x: | :x: | -| Washer | :heavy_check_mark: | :x: | :x: | +| Dishwasher | :heavy_check_mark: | :warning: needs testing | :x: | +| Dryer | :heavy_check_mark: | :warning: needs testing | :x: | +| Washer | :heavy_check_mark: | :warning: needs testing | :x: | ## Credits diff --git a/lib/cli.ts b/lib/cli.ts index b45df73..e48783f 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -22,6 +22,7 @@ const input = (question: string) => new Promise((resolve) => { const options = { country: constants.DEFAULT_COUNTRY, language: constants.DEFAULT_LANGUAGE, + refreshToken: '', statePath: 'wideq-state.json', }; @@ -32,6 +33,8 @@ program .on('option:country', (value) => options.country = value) .option('-l, --language ', 'Language code for account', constants.DEFAULT_LANGUAGE) .on('option:language', (value) => options.language = value) + .option('-t, --token ', 'Refresh token') + .on('option:token', (value) => options.refreshToken = value) .option('-s, --state-path ', 'State file path', 'wideq-state.json') .on('option:statePath', (value) => options.statePath = value); @@ -39,9 +42,9 @@ program .command('auth') .description('Authenticate') .action(async () => { - const { country, language, statePath } = options; + const { country, language, refreshToken, statePath } = options; - const client = await init(country, language, statePath); + const client = await init(country, language, refreshToken, statePath); console.info('Refresh token: ' + client.auth.refreshToken); @@ -53,8 +56,8 @@ program .command('ls', { isDefault: true }) .description('List devices') .action(async () => { - const { country, language, statePath } = options; - const client = await init(country, language, statePath); + const { country, language, refreshToken, statePath } = options; + const client = await init(country, language, refreshToken, statePath); for (const device of client.devices) { console.info(String(device)); @@ -68,8 +71,8 @@ program .command('monitor ') .description('Monitor any device, displaying generic information about its status.') .action(async (deviceId: string) => { - const { country, language, statePath } = options; - const client = await init(country, language, statePath); + const { country, language, refreshToken, statePath } = options; + const client = await init(country, language, refreshToken, statePath); const dev = await client.getDevice(deviceId); saveState(statePath, client); @@ -121,7 +124,7 @@ async function authenticate(gateway: Gateway) { return Auth.fromUrl(gateway, callbackUrl); } -async function init(country: string, language: string, stateFilePath?: string) { +async function init(country: string, language: string, refreshToken: string, stateFilePath?: string) { let state: any = {}; if (stateFilePath) { @@ -132,12 +135,14 @@ async function init(country: string, language: string, stateFilePath?: string) { } } - const client = Client.loadFromState({ - country, - language, + const client = refreshToken ? + await Client.loadFromToken(refreshToken, country, language) : + Client.loadFromState({ + country, + language, - ...state, - }); + ...state, + }); if (!client.gateway) { client.gateway = await Gateway.discover(country, language); diff --git a/lib/client.ts b/lib/client.ts index da15b0e..d2570e4 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -1,21 +1,25 @@ -import { ACDevice } from './devices/ac'; -import { DishwasherDevice } from './devices/dishwasher'; import { Auth } from './core/auth'; -import { Gateway } from './core/gateway'; -import { Session } from './core/session'; import * as constants from './core/constants'; +import { DeviceType } from './core/constants'; import { Device } from './core/device'; import { DeviceInfo } from './core/device-info'; +import { Gateway } from './core/gateway'; +import { LangPackModel } from './core/lang-pack-model'; +import { LangPackProduct } from './core/lang-pack-product'; import { ModelInfo } from './core/model-info'; +import { Session } from './core/session'; +import { ACDevice } from './devices/ac'; import { DehumidifierDevice } from './devices/dehumidifier'; -import { RefrigeratorDevice } from './devices/refrigerator'; +import { DishwasherDevice } from './devices/dishwasher'; import { DryerDevice } from './devices/dryer'; +import { RefrigeratorDevice } from './devices/refrigerator'; import { WasherDevice } from './devices/washer'; -import { DeviceType } from './core/constants'; export class Client { public devices: DeviceInfo[] = []; public modelInfo: { [key: string]: any } = {}; + public langPackProduct: { [key: string]: any } = {}; + public langPackModel: { [key: string]: any } = {}; public constructor( public gateway: Gateway, @@ -37,7 +41,7 @@ export class Client { } public static loadFromState(state: { - [key in 'gateway' | 'auth' | 'session' | 'modelInfo' | 'country' | 'language']: any; + [key in 'gateway' | 'auth' | 'session' | 'modelInfo' | 'country' | 'language' | 'langPackProduct' | 'langPackModel']: any; }) { let gateway: Gateway; let auth: Auth; @@ -45,6 +49,8 @@ export class Client { let modelInfo: Client['modelInfo'] = {}; let country: string = constants.DEFAULT_COUNTRY; let language: string = constants.DEFAULT_LANGUAGE; + let langPackProduct: Client['langPackProduct'] = {}; + let langPackModel: Client['langPackModel'] = {}; for (const key of Object.keys(state)) { switch (key) { @@ -83,6 +89,14 @@ export class Client { case 'language': language = state.language; break; + + case 'langPackProduct': + langPackProduct = state.langPackProduct; + break; + + case 'langPackModel': + langPackModel = state.langPackModel; + break; } } @@ -94,6 +108,8 @@ export class Client { language, ); client.modelInfo = modelInfo; + client.langPackProduct = langPackProduct; + client.langPackModel = langPackModel; return client; } @@ -125,6 +141,9 @@ export class Client { country: this.country, language: this.language, + + langPackProduct: this.langPackProduct, + langPackModel: this.langPackModel, }; } @@ -149,7 +168,9 @@ export class Client { throw new Error(`Device not found: ${deviceInfo}`); } - const modelInfo = await this.getModelInfo(deviceInfo); + await this.getModelInfo(deviceInfo); + await this.getLangPackProduct(deviceInfo); + await this.getLangPackModel(deviceInfo); switch (deviceInfo.data.deviceType) { case DeviceType.AC: @@ -187,4 +208,22 @@ export class Client { return new ModelInfo(this.modelInfo[url]); } + + public async getLangPackProduct(device: DeviceInfo) { + const url = device.langPackProductUrl; + if (!(url in this.langPackProduct)) { + this.langPackProduct[url] = await device.loadLangPackProduct(); + } + + return new LangPackProduct(this.langPackProduct[url]); + } + + public async getLangPackModel(device: DeviceInfo) { + const url = device.langPackModelUrl; + if (!(url in this.langPackModel)) { + this.langPackModel[url] = await device.loadLangPackModel(); + } + + return new LangPackModel(this.langPackModel[url]); + } } diff --git a/lib/core/auth.ts b/lib/core/auth.ts index 69ed4a5..0e0f694 100644 --- a/lib/core/auth.ts +++ b/lib/core/auth.ts @@ -1,14 +1,14 @@ -import { TokenError } from './errors'; -import { Session } from './session'; -import { URL, resolve as resolveUrl } from 'url'; -import * as qs from 'qs'; import * as assert from 'assert'; -import * as crypto from 'crypto'; +import crypto from 'crypto'; +import { DateTime } from 'luxon'; +import * as qs from 'qs'; +import { resolve as resolveUrl, URL } from 'url'; -import { Gateway } from './gateway'; import * as constants from './constants'; +import { TokenError } from './errors'; +import { Gateway } from './gateway'; import { requestClient } from './request'; -import { DateTime } from 'luxon'; +import { Session } from './session'; export class Auth { public constructor( diff --git a/lib/core/constants.ts b/lib/core/constants.ts index eb6c320..237a7db 100644 --- a/lib/core/constants.ts +++ b/lib/core/constants.ts @@ -22,12 +22,14 @@ export enum DeviceType { MICROWAVE = 302, COOKTOP = 303, HOOD = 304, - /** Includes heat pumps, etc., possibly all HVAC devices. */ - AC = 401, + AC = 401, // Includes heat pumps, etc., possibly all HVAC devices. AIR_PURIFIER = 402, DEHUMIDIFIER = 403, - /** Robot vacuum cleaner? */ - ROBOT_KING = 501, + ROBOT_KING = 501, // This is Robotic vacuum cleaner + TV = 701, + BOILER = 801, + SPEAKER = 901, + HOMEVU = 902, ARCH = 1001, MISSG = 3001, SENSOR = 3002, @@ -38,4 +40,7 @@ export enum DeviceType { IOT_DUST_SENSOR = 3006, EMS_AIR_STATION = 4001, AIR_SENSOR = 4003, + PURICARE_AIR_DETECTOR = 4004, + V2PHONE = 6001, + HOMEROBOT = 9000, } diff --git a/lib/core/device-info.ts b/lib/core/device-info.ts index e86c886..17aed02 100644 --- a/lib/core/device-info.ts +++ b/lib/core/device-info.ts @@ -5,6 +5,9 @@ export interface DeviceData { modelNm: string; deviceId: string; modelJsonUrl: string; + langPackProductTypeUri: string; + langPackModelUri: string; + macAddress: string; alias: string; deviceType: DeviceType; } @@ -19,6 +22,14 @@ export class DeviceInfo { return requestClient.get(this.modelInfoUrl).then(resp => resp.data); } + public async loadLangPackProduct() { + return this.langPackProductUrl && requestClient.get(this.langPackProductUrl).then(resp => resp.data); + } + + public async loadLangPackModel() { + return this.langPackModelUrl && requestClient.get(this.langPackModelUrl).then(resp => resp.data); + } + public get modelId() { return this.data.modelNm; } @@ -31,6 +42,18 @@ export class DeviceInfo { return this.data.modelJsonUrl; } + public get langPackProductUrl() { + return this.data.langPackProductTypeUri; + } + + public get langPackModelUrl() { + return this.data.langPackModelUri; + } + + public get macAddress() { + return this.data.macAddress; + } + public get name() { return this.data.alias; } diff --git a/lib/core/device.ts b/lib/core/device.ts index 2eb967e..7dd53ef 100644 --- a/lib/core/device.ts +++ b/lib/core/device.ts @@ -2,10 +2,19 @@ import { Monitor } from './monitor'; import { Client } from '../client'; import { DeviceInfo } from './device-info'; +import { LangPackModel } from './lang-pack-model'; +import { LangPackProduct } from './lang-pack-product'; import { ModelInfo } from './model-info'; +export enum OnOffEnum { + OFF = '@CP_OFF_EN_W', + ON = '@CP_ON_EN_W', +} + export class Device { public model!: ModelInfo; + public langPackProduct!: LangPackProduct; + public langPackModel!: LangPackModel; public monitor?: Monitor; public constructor( @@ -20,6 +29,8 @@ export class Device { public async load() { this.model = await this.client.getModelInfo(this.device); + this.langPackProduct = await this.client.getLangPackProduct(this.device); + this.langPackModel = await this.client.getLangPackModel(this.device); } public async setControl(key: string, value: any) { diff --git a/lib/core/gateway.ts b/lib/core/gateway.ts index 8e59cb7..237b99e 100644 --- a/lib/core/gateway.ts +++ b/lib/core/gateway.ts @@ -1,9 +1,9 @@ -import * as url from 'url'; import * as qs from 'qs'; +import * as url from 'url'; import { GATEWAY_URL } from './constants'; -import { requestClient } from './request'; import * as constants from './constants'; +import { requestClient } from './request'; export class Gateway { public constructor( diff --git a/lib/core/lang-pack-model.ts b/lib/core/lang-pack-model.ts new file mode 100644 index 0000000..59af803 --- /dev/null +++ b/lib/core/lang-pack-model.ts @@ -0,0 +1,47 @@ +import _ from 'lodash'; + +export interface LangValue { + packs: any; +} + +/** + * A description of a device model's capabilities. + */ +export class LangPackModel { + public constructor( + public data: any, + ) { + } + + /** + * Look up information about a value. + */ + public value() { + return { + packs: this.data.pack, + } as LangValue; + } + + /** + * Look up the encoded value for a friendly enum name. + */ + public enumValue(name: string) { + const packs = (this.value() as LangValue).packs || {}; + // invert them pa + const packsInv = _.invert(packs); + + return packsInv[name]; + } + + /** + * Look up the friendly enum name for an encoded value. + */ + public enumName(value: string) { + const packs = (this.value() as LangValue).packs || {}; + if (!(value in packs)) { + return null; + } + + return packs[value]; + } +} diff --git a/lib/core/lang-pack-product.ts b/lib/core/lang-pack-product.ts new file mode 100644 index 0000000..36b4471 --- /dev/null +++ b/lib/core/lang-pack-product.ts @@ -0,0 +1,44 @@ +import _ from 'lodash'; +import { LangValue } from './lang-pack-model'; + +/** + * A description of a device model's capabilities. + */ +export class LangPackProduct { + public constructor( + public data: any, + ) { + } + + /** + * Look up information about a value. + */ + public value() { + return { + packs: this.data.pack, + } as LangValue; + } + + /** + * Look up the encoded value for a friendly enum name. + */ + public enumValue(name: string) { + const packs = (this.value() as LangValue).packs || {}; + // invert them pa + const packsInv = _.invert(packs); + + return packsInv[name]; + } + + /** + * Look up the friendly enum name for an encoded value. + */ + public enumName(value: string) { + const packs = (this.value() as LangValue).packs || {}; + if (!(value in packs)) { + return null; + } + + return packs[value]; + } +} diff --git a/lib/core/model-info.ts b/lib/core/model-info.ts index c3f4386..9fcbdd1 100644 --- a/lib/core/model-info.ts +++ b/lib/core/model-info.ts @@ -33,29 +33,29 @@ export interface ModelData { } export interface BitValue { - type: ValueType.Bit, + type: ValueType.Bit; options: any; } export interface EnumValue { - type: ValueType.Enum, + type: ValueType.Enum; options: any; } export interface RangeValue { - type: ValueType.Range, + type: ValueType.Range; min: number; max: number; step: number; } export interface ReferenceValue { - type: ValueType.Reference, + type: ValueType.Reference; reference: any; } export interface StringCommentValue { - type: ValueType.StringComment, + type: ValueType.StringComment; comment: string; } @@ -146,6 +146,10 @@ export class ModelInfo { return this.data.Monitoring.type === 'BINARY(BYTE)'; } + public decodeMonitor(data: any) { + return this.binaryMonitorData ? this.decodeMonitorBinary(data) : this.decodeMonitorJson(data); + } + private decodeMonitorBinary(data: any) { const decoded: { [key: string]: any } = {}; @@ -167,8 +171,4 @@ export class ModelInfo { return JSON.parse(data.toString('utf-8')); } - public decodeMonitor(data: any) { - return this.binaryMonitorData ? this.decodeMonitorBinary(data) : this.decodeMonitorJson(data); - } - } diff --git a/lib/core/monitor.ts b/lib/core/monitor.ts index 8a9295e..1facdf9 100644 --- a/lib/core/monitor.ts +++ b/lib/core/monitor.ts @@ -1,5 +1,5 @@ -import { Session, WorkId } from './session'; import { MonitorError } from './errors'; +import { Session, WorkId } from './session'; export class Monitor { public workId?: WorkId; diff --git a/lib/core/request.ts b/lib/core/request.ts index 922cb7d..1e16cff 100644 --- a/lib/core/request.ts +++ b/lib/core/request.ts @@ -1,5 +1,5 @@ -import { NotLoggedInError, NotConnectedError, APIError } from './errors'; import axios, { AxiosInstance } from 'axios'; +import { APIError, NotConnectedError, NotLoggedInError } from './errors'; import * as constants from './constants'; @@ -31,7 +31,6 @@ client.interceptors.response.use((resp) => { (client as any).lgedmPost = (async (url, data, accessToken, sessionId) => { const headers: { [key: string]: string } = {}; - if (typeof accessToken === 'string') { headers['x-thinq-token'] = accessToken; } diff --git a/lib/core/session.ts b/lib/core/session.ts index b35efd3..5e987f3 100644 --- a/lib/core/session.ts +++ b/lib/core/session.ts @@ -1,9 +1,9 @@ -import { Auth } from './auth'; +import * as _ from 'lodash'; import * as url from 'url'; -import { requestClient } from './request'; import * as uuid from 'uuid'; -import * as _ from 'lodash'; +import { Auth } from './auth'; import { MonitorError } from './errors'; +import { requestClient } from './request'; export type WorkId = typeof uuid['v4']; diff --git a/lib/devices/ac.ts b/lib/devices/ac.ts index c53452a..7ce0982 100644 --- a/lib/devices/ac.ts +++ b/lib/devices/ac.ts @@ -14,7 +14,7 @@ export enum ACVSwingMode { FOUR = '@4', FIVE = '@5', SIX = '@6', - ALL = '@100' + ALL = '@100', } /** @@ -35,23 +35,23 @@ export enum ACHSwingMode { FIVE = '@5', LEFT_HALF = '@13', RIGHT_HALF = '@35', - ALL = '@100' + ALL = '@100', } /** * The operation mode for an AC/HVAC device. */ export enum ACMode { - COOL = "@AC_MAIN_OPERATION_MODE_COOL_W", - DRY = "@AC_MAIN_OPERATION_MODE_DRY_W", - FAN = "@AC_MAIN_OPERATION_MODE_FAN_W", - AI = "@AC_MAIN_OPERATION_MODE_AI_W", - HEAT = "@AC_MAIN_OPERATION_MODE_HEAT_W", - AIRCLEAN = "@AC_MAIN_OPERATION_MODE_AIRCLEAN_W", - ACO = "@AC_MAIN_OPERATION_MODE_ACO_W", - AROMA = "@AC_MAIN_OPERATION_MODE_AROMA_W", - ENERGY_SAVING = "@AC_MAIN_OPERATION_MODE_ENERGY_SAVING_W", - ENERGY_SAVER = "@AC_MAIN_OPERATION_MODE_ENERGY_SAVER_W" + COOL = '@AC_MAIN_OPERATION_MODE_COOL_W', + DRY = '@AC_MAIN_OPERATION_MODE_DRY_W', + FAN = '@AC_MAIN_OPERATION_MODE_FAN_W', + AI = '@AC_MAIN_OPERATION_MODE_AI_W', + HEAT = '@AC_MAIN_OPERATION_MODE_HEAT_W', + AIRCLEAN = '@AC_MAIN_OPERATION_MODE_AIRCLEAN_W', + ACO = '@AC_MAIN_OPERATION_MODE_ACO_W', + AROMA = '@AC_MAIN_OPERATION_MODE_AROMA_W', + ENERGY_SAVING = '@AC_MAIN_OPERATION_MODE_ENERGY_SAVING_W', + ENERGY_SAVER = '@AC_MAIN_OPERATION_MODE_ENERGY_SAVER_W', } /** @@ -66,18 +66,18 @@ export enum ACFanSpeed { MID_HIGH = '@AC_MAIN_WIND_STRENGTH_MID_HIGH_W', HIGH = '@AC_MAIN_WIND_STRENGTH_HIGH_W', POWER = '@AC_MAIN_WIND_STRENGTH_POWER_W', - AUTO = '@AC_MAIN_WIND_STRENGTH_AUTO_W' + AUTO = '@AC_MAIN_WIND_STRENGTH_AUTO_W', } /** * Whether a device is on or off. */ export enum ACOperation { - OFF = "@AC_MAIN_OPERATION_OFF_W", + OFF = '@AC_MAIN_OPERATION_OFF_W', /** This one seems to mean "on" ? */ - RIGHT_ON = "@AC_MAIN_OPERATION_RIGHT_ON_W", - LEFT_ON = "@AC_MAIN_OPERATION_LEFT_ON_W", - ALL_ON = "@AC_MAIN_OPERATION_ALL_ON_W" + RIGHT_ON = '@AC_MAIN_OPERATION_RIGHT_ON_W', + LEFT_ON = '@AC_MAIN_OPERATION_LEFT_ON_W', + ALL_ON = '@AC_MAIN_OPERATION_ALL_ON_W', } export class ACDevice extends Device { @@ -102,7 +102,7 @@ export class ACDevice extends Device { if (mapping.type === 'Enum') { return Object.entries(mapping.options).reduce((obj, [f, c]) => ({ ...obj, - [Number(c)]: c, + [Number(f)]: c, }), {}); } } @@ -205,7 +205,7 @@ export class ACDevice extends Device { export class ACStatus { public constructor( public device: ACDevice, - public data: any + public data: any, ) { } public get currentTempInCelsius() { @@ -234,14 +234,14 @@ export class ACStatus { return asEnum(ACFanSpeed, key); } - public get HorizontalSwing() { + public get horizontalSwing() { const key = lookupEnum('WDirHStep', this.data, this.device); - return asEnum(ACOperation, key); + return asEnum(ACHSwingMode, key); } - public getVerticalSwing() { + public get verticalSwing() { const key = lookupEnum('WDirVStep', this.data, this.device); - return asEnum(ACOperation, key); + return asEnum(ACVSwingMode, key); } public get isOn() { diff --git a/lib/devices/dehumidifier.ts b/lib/devices/dehumidifier.ts index 7a70056..67cdd38 100644 --- a/lib/devices/dehumidifier.ts +++ b/lib/devices/dehumidifier.ts @@ -1,5 +1,5 @@ -import { asEnum, lookupEnum } from '../utils'; import { Device } from '../core/device'; +import { asEnum, lookupEnum } from '../utils'; export enum DehumidifierOperationMode { SLEEP = '@AP_MAIN_MID_OPMODE_SLEEP_W', @@ -88,7 +88,7 @@ export class DehumidifierDevice extends Device { export class DehumidifierStatus { public constructor( public device: DehumidifierDevice, - public data: any + public data: any, ) { } diff --git a/lib/devices/dishwasher.ts b/lib/devices/dishwasher.ts index f3298d3..e91bb87 100644 --- a/lib/devices/dishwasher.ts +++ b/lib/devices/dishwasher.ts @@ -1,5 +1,5 @@ import { Device } from '../core/device'; -import { lookupEnum, asEnum, lookupReference } from '../utils'; +import { asEnum, asTime, lookupEnum, lookupReference } from '../utils'; /** * The state of the dishwasher device. @@ -7,10 +7,10 @@ import { lookupEnum, asEnum, lookupReference } from '../utils'; export enum DishwasherState { INITIAL = '@DW_STATE_INITIAL_W', RUNNING = '@DW_STATE_RUNNING_W', - PAUSED = "@DW_STATE_PAUSE_W", + PAUSED = '@DW_STATE_PAUSE_W', OFF = '@DW_STATE_POWER_OFF_W', COMPLETE = '@DW_STATE_COMPLETE_W', - POWER_FAIL = "@DW_STATE_POWER_FAIL_W" + POWER_FAIL = '@DW_STATE_POWER_FAIL_W', } /** @@ -23,7 +23,7 @@ export enum DishwasherProcess { DRYING = '@DW_STATE_DRYING_W', COMPLETE = '@DW_STATE_COMPLETE_W', NIGHT_DRYING = '@DW_STATE_NIGHTDRY_W', - CANCELLED = '@DW_STATE_CANCEL_W' + CANCELLED = '@DW_STATE_CANCEL_W', } export class DishwasherDevice extends Device { @@ -45,7 +45,7 @@ export class DishwasherDevice extends Device { export class DishwasherStatus { public constructor( public device: DishwasherDevice, - public data: any + public data: any, ) { } public get state() { @@ -63,15 +63,15 @@ export class DishwasherStatus { } public get remainingTime() { - return Number(this.data['Remain_Time_H']) * 60 + Number(this.data['Remain_Time_M']); + return asTime('Remain_Time_H', 'Remain_Time_M', this.data); } public get initialTime() { - return Number(this.data['Initial_Time_H']) * 60 + Number(this.data['Initial_Time_M']); + return asTime('Initial_Time_H', 'Initial_Time_M', this.data); } public get reserveTime() { - return Number(this.data['Reserve_Time_H']) * 60 + Number(this.data['Reserve_Time_M']); + return asTime('Reserve_Time_H', 'Reserve_Time_M', this.data); } public get course() { diff --git a/lib/devices/dryer.ts b/lib/devices/dryer.ts index 1190361..15933c2 100644 --- a/lib/devices/dryer.ts +++ b/lib/devices/dryer.ts @@ -1,5 +1,5 @@ import { Device } from '../core/device'; -import { lookupEnum, asEnum, lookupReference } from '../utils'; +import { asEnum, asTime, lookupEnum, lookupReference } from '../utils'; /** * The state of the dryer device. @@ -14,7 +14,7 @@ export enum DryerState { PAUSE = '@WM_STATE_PAUSE_W', RUNNING = '@WM_STATE_RUNNING_W', SMART_DIAGNOSIS = '@WM_STATE_SMART_DIAGNOSIS_W', - WRINKLE_CARE = '@WM_STATE_WRINKLECARE_W' + WRINKLE_CARE = '@WM_STATE_WRINKLECARE_W', } /** @@ -29,7 +29,7 @@ export enum DryLevel { MORE = '@WM_DRY27_DRY_LEVEL_MORE_W', NORMAL = '@WM_DRY27_DRY_LEVEL_NORMAL_W', OFF = '-', - VERY = '@WM_DRY27_DRY_LEVEL_VERY_W' + VERY = '@WM_DRY27_DRY_LEVEL_VERY_W', } /** @@ -51,7 +51,7 @@ export enum DryerError { ERROR_TE1 = '@WM_US_DRYER_ERROR_TE1_W', ERROR_TE2 = '@WM_US_DRYER_ERROR_TE2_W', ERROR_TE5 = '@WM_US_DRYER_ERROR_TE5_W', - ERROR_TE6 = '@WM_US_DRYER_ERROR_TE6_W' + ERROR_TE6 = '@WM_US_DRYER_ERROR_TE6_W', } /** @@ -63,7 +63,7 @@ export enum TempControl { LOW = '@WM_DRY27_TEMP_LOW_W', MEDIUM = '@WM_DRY27_TEMP_MEDIUM_W', MID_HIGH = '@WM_DRY27_TEMP_MID_HIGH_W', - HIGH = '@WM_DRY27_TEMP_HIGH_W' + HIGH = '@WM_DRY27_TEMP_HIGH_W', } /** @@ -75,7 +75,7 @@ export enum TimeDry { THIRTY = '30', FOURTY = '40', FIFTY = '50', - SIXTY = '60' + SIXTY = '60', } export class DryerDevice extends Device { @@ -97,7 +97,7 @@ export class DryerDevice extends Device { export class DryerStatus { public constructor( public device: DryerDevice, - public data: any + public data: any, ) { } public get state() { @@ -130,11 +130,11 @@ export class DryerStatus { } public get remainingTime() { - return Number(this.data['Remain_Time_H']) * 60 + Number(this.data['Remain_Time_M']); + return asTime('Remain_Time_H', 'Remain_Time_M', this.data); } public get initialTime() { - return Number(this.data['Initial_Time_H']) * 60 + Number(this.data['Initial_Time_M']); + return asTime('Initial_Time_H', 'Initial_Time_M', this.data); } public get course() { diff --git a/lib/devices/refrigerator.ts b/lib/devices/refrigerator.ts index 18365bd..c6a95c8 100644 --- a/lib/devices/refrigerator.ts +++ b/lib/devices/refrigerator.ts @@ -1,59 +1,46 @@ -import { asEnum, lookupEnum } from '../utils'; -import { Device } from '../core/device'; - -export enum IcePlus { - OFF = "@CP_OFF_EN_W", - ON = "@CP_ON_EN_W", - ICE_PLUS = "@RE_TERM_ICE_PLUS_W", - ICE_PLUS_FREEZE = "@RE_MAIN_SPEED_FREEZE_TERM_W", - ICE_PLUS_OFF = "@CP_TERM_OFF_KO_W" -} +import { Device, OnOffEnum } from '../core/device'; +import { asEnum, lookupEnum, lookupEnumLang } from '../utils'; export enum FreshAirFilter { - OFF = "@CP_TERM_OFF_KO_W", - AUTO = "@RE_STATE_FRESH_AIR_FILTER_MODE_AUTO_W", - POWER = "@RE_STATE_FRESH_AIR_FILTER_MODE_POWER_W", - REPLACE_FILTER = "@RE_STATE_REPLACE_FILTER_W", - SMARTCARE_RUN = "@RE_SMARTCARE_RUN_W", - SMARTCARE_ON = "@RE_STATE_SMART_SMART_CARE_ON", - SMARTCARE_OFF = "@RE_STATE_SMART_SMART_CARE_OFF", - EMPTY = "" + OFF = '@CP_TERM_OFF_KO_W', + AUTO = '@RE_STATE_FRESH_AIR_FILTER_MODE_AUTO_W', + POWER = '@RE_STATE_FRESH_AIR_FILTER_MODE_POWER_W', + REPLACE_FILTER = '@RE_STATE_REPLACE_FILTER_W', + SMARTCARE_RUN = '@RE_SMARTCARE_RUN_W', + SMARTCARE_ON = '@RE_STATE_SMART_SMART_CARE_ON', + SMARTCARE_OFF = '@RE_STATE_SMART_SMART_CARE_OFF', + EMPTY = '', } export enum SmartSavingMode { - OFF = "@CP_TERM_USE_NOT_W", - NIGHT = "@RE_SMARTSAVING_MODE_NIGHT_W", - CUSTOM = "@RE_SMARTSAVING_MODE_CUSTOM_W", - EMPTY = "" + OFF = '@CP_TERM_USE_NOT_W', + NIGHT = '@RE_SMARTSAVING_MODE_NIGHT_W', + CUSTOM = '@RE_SMARTSAVING_MODE_CUSTOM_W', + EMPTY = '', } export enum SmartSavingModeStatus { - OFF = "OFF", - ON = "ON", - EMPTY = "" -} - -export enum EcoFriendly { - OFF = "@CP_OFF_EN_W", - ON = "@CP_ON_EN_W" + OFF = 'OFF', + ON = 'ON', + EMPTY = '', } export enum LockingStatus { - UNLOCK = "UNLOCK", - LOCK = "LOCK", - EMPTY = "" + UNLOCK = 'UNLOCK', + LOCK = 'LOCK', + EMPTY = '', } export enum DoorOpenState { - OPEN = "OPEN", - CLOSE = "CLOSE", - EMPTY = "" + OPEN = 'OPEN', + CLOSE = 'CLOSE', + EMPTY = '', } export enum TempUnit { - F = "F", - C = "℃", - EMPTY = "" + F = 'F', + C = '℃', + EMPTY = '', } export class RefrigeratorDevice extends Device { @@ -78,20 +65,30 @@ export class RefrigeratorDevice extends Device { // "REIP":"{{IcePlus}}", // "REEF":"{{EcoFriendly}}" // } - const opValue = this.model.enumValue('TempRefrigerator', temp.toString()) + const opValue = this.model.enumValue('TempRefrigerator', temp.toString()); await this.setControl('RETM', opValue); } public async setTempFreezerC(temp: number) { - const opValue = this.model.enumValue('TempFreezer', temp.toString()) + const opValue = this.model.enumValue('TempFreezer', temp.toString()); await this.setControl('REFT', opValue); } + + public async setEcoEnabled(val: boolean) { + const opValue = this.model.enumValue('EcoFriendly', val ? OnOffEnum.ON : OnOffEnum.OFF); + await this.setControl('REEF', opValue); + } + + public async setIcePlusStatus(val: boolean) { + const opValue = this.model.enumValue('IcePlus', val ? OnOffEnum.ON : OnOffEnum.OFF); + await this.setControl('REIP', opValue); + } } export class RefrigeratorStatus { public constructor( public device: RefrigeratorDevice, - public data: any + public data: any, ) { } @@ -107,7 +104,7 @@ export class RefrigeratorStatus { public get icePlusStatus() { const key = lookupEnum('IcePlus', this.data, this.device); - return asEnum(IcePlus, key); + return asEnum(OnOffEnum, key) === OnOffEnum.ON; } public get freshAirFilterStatus() { @@ -115,6 +112,10 @@ export class RefrigeratorStatus { return asEnum(FreshAirFilter, key); } + public get freshAirFilterStatusText() { + return lookupEnumLang('FreshAirFilter', this.data, this.device); + } + public get energySavingMode() { const key = lookupEnum('SmartSavingMode', this.data, this.device); return asEnum(SmartSavingMode, key); @@ -122,7 +123,7 @@ export class RefrigeratorStatus { public get doorOpened() { const key = lookupEnum('DoorOpenState', this.data, this.device); - return asEnum(DoorOpenState, key) == DoorOpenState.OPEN; + return asEnum(DoorOpenState, key) === DoorOpenState.OPEN; } public get tempUnit() { @@ -132,24 +133,24 @@ export class RefrigeratorStatus { public get energySavingEnabled() { const key = lookupEnum('SmartSavingModeStatus', this.data, this.device); - return asEnum(SmartSavingModeStatus, key) == SmartSavingModeStatus.ON; + return asEnum(SmartSavingModeStatus, key) === SmartSavingModeStatus.ON; } public get locked() { const key = lookupEnum('LockingStatus', this.data, this.device); - return asEnum(LockingStatus, key) == LockingStatus.LOCK; + return asEnum(LockingStatus, key) === LockingStatus.LOCK; } public get activeSavingStatus() { - return this.data['ActiveSavingStatus'] + return this.data['ActiveSavingStatus']; } public get ecoEnabled() { const value = lookupEnum('EcoFriendly', this.data, this.device); - return asEnum(EcoFriendly, value) == EcoFriendly.ON; + return asEnum(OnOffEnum, value) === OnOffEnum.ON; } public get waterFilterUsedMonth() { - return this.data['WaterFilterUsedMonth'] + return this.data['WaterFilterUsedMonth']; } } diff --git a/lib/devices/washer.ts b/lib/devices/washer.ts index 2f46f0f..402c6e3 100644 --- a/lib/devices/washer.ts +++ b/lib/devices/washer.ts @@ -1,5 +1,5 @@ -import { Device } from '../core/device'; -import { asEnum, lookupEnum, lookupReference } from '../utils'; +import { Device, OnOffEnum } from '../core/device'; +import { asEnum, asTime, lookupEnum, lookupEnumLang, lookupReference } from '../utils'; /** * The state of the washer device. @@ -28,7 +28,7 @@ export enum WasherState { SMART_DIAGNOSIS_DATA = '@WM_STATE_SMART_DIAGDATA_W', SPINNING = '@WM_STATE_SPINNING_W', TCL_ALARM_NORMAL = 'TCL_ALARM_NORMAL', - TUBCLEAN_COUNT_ALARM = '@WM_STATE_TUBCLEAN_COUNT_ALRAM_W' + TUBCLEAN_COUNT_ALARM = '@WM_STATE_TUBCLEAN_COUNT_ALRAM_W', } export class WasherDevice extends Device { @@ -45,12 +45,17 @@ export class WasherDevice extends Device { return null; } + + public async setOn(isOn: boolean) { + const value = isOn ? 'On' : 'Off'; + await this.setControl('Power', value); + } } export class WasherStatus { public constructor( public device: WasherDevice, - public data: any + public data: any, ) { } public get state() { @@ -58,25 +63,47 @@ export class WasherStatus { return asEnum(WasherState, key); } + public get stateText() { + return lookupEnumLang('State', this.data, this.device); + } + public get previousState() { const key = lookupEnum('PreState', this.data, this.device); return asEnum(WasherState, key); } + public get previousStateText() { + return lookupEnumLang('PreState', this.data, this.device); + } + public get isOn() { return this.state !== WasherState.OFF; } + public get isRemoteStart() { + const key = lookupEnum('RemoteStart', this.data, this.device); + return asEnum(OnOffEnum, key) === OnOffEnum.ON; + } + + public get isChildLock() { + const key = lookupEnum('ChildLock', this.data, this.device); + return asEnum(OnOffEnum, key) === OnOffEnum.ON; + } + public get remainingTime() { - return Number(this.data['Remain_Time_H']) * 60 + Number(this.data['Remain_Time_M']); + return asTime('Remain_Time_H', 'Remain_Time_M', this.data); } public get initialTime() { - return Number(this.data['Initial_Time_H']) * 60 + Number(this.data['Initial_Time_M']); + return asTime('Initial_Time_H', 'Initial_Time_M', this.data); + } + + public get reserveTime() { + return asTime('Reserve_Time_H', 'Reserve_Time_M', this.data); } public get course() { - const value = lookupReference('APCourse', this.data, this.device); + const value = lookupReference('Course', this.data, this.device); return value; } diff --git a/lib/index.ts b/lib/index.ts index fc19daa..8195579 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -7,6 +7,12 @@ export * from './core/gateway'; export * from './core/model-info'; export * from './core/monitor'; export * from './core/session'; +export * from './devices/ac'; +export * from './devices/dehumidifier'; +export * from './devices/dishwasher'; +export * from './devices/dryer'; +export * from './devices/refrigerator'; +export * from './devices/washer'; export * from './client'; export { diff --git a/lib/utils.ts b/lib/utils.ts index 83c49b5..0284022 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -4,34 +4,54 @@ type Not = [T] extends [never] ? unknown : never; type Extractable = Not : never>; export function asEnum, K extends string | number>( - e: E, k: K & Extractable + e: E, k: K & Extractable, ): Extract { // runtime guard, shouldn't need it at compiler time if (k && Object.values(e).indexOf(k) < 0) { - throw new Error("Expected one of " + Object.values(e).join(", ") + '; got ' + k); + throw new Error('Expected one of ' + Object.values(e).join(', ') + '; got ' + k); } return k as any; // assertion -}; +} + +export function asTime(hoursKey: string, minutesKey: string, data: any) { + return Number(data[hoursKey]) * 60 + Number(data[minutesKey]); +} +/** + * Looks up an enum value for the provided attr. + * @param attr The attribute to lookup in the enum. + * @param data The JSON data from the API. + * @param device A sub-class instance of a Device. + * @returns The enum value. + */ export function lookupEnum(attr: string, data: any, device: Device) { - /** - * Looks up an enum value for the provided attr. - * @param attr: The attribute to lookup in the enum. - * @param data: The JSON data from the API. - * @param device: A sub-class instance of a Device. - * @returns: The enum value. - */ return device.model.enumName(attr, data[attr]); } -export function lookupReference(attr: string, data: any, device: Device) { /** * Look up a reference value for the provided attribute. - * @param attr: The attribute to find the value for. - * @param data: The JSON data from the API. - * @param device: A sub-class instance of a Device. - * @returns: The looked up value. + * @param attr The attribute to find the value for. + * @param data The JSON data from the API. + * @param device A sub-class instance of a Device. + * @returns The looked up value. */ +export function lookupReference(attr: string, data: any, device: Device) { + if (!(attr in data)) { + return null; + } return device.model.referenceName(attr, data[attr]); } + +/** + * Looks up an enum value for the provided attr. + * @param attr The attribute to lookup in the enum. + * @param data The JSON data from the API. + * @param device A sub-class instance of a Device. + * @returns The enum value. + */ +export function lookupEnumLang(attr: string, data: any, device: Device) { + const value = device.model.enumName(attr, data[attr]); + const lang = value && (device.langPackProduct.enumName(value) || device.langPackModel.enumName(value)); + return String(lang); +} diff --git a/package-lock.json b/package-lock.json index 3c7aab7..281bcc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -600,6 +600,15 @@ "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==", "dev": true }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -907,6 +916,12 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -3222,6 +3237,24 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -4637,6 +4670,12 @@ "extend-shallow": "^3.0.0" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -4936,6 +4975,50 @@ "yn": "^3.0.0" } }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + }, + "tslint": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 94d8918..2aaa3d0 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "shx": "^0.3.2", "ts-jest": "^24.0.2", "ts-node": "^8.3.0", + "tslint": "^5.20.1", "typedoc": "^0.15.0", "typescript": "^3.5.3" } diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..4cdab7c --- /dev/null +++ b/tslint.json @@ -0,0 +1,13 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "interface-name": false, + "max-classes-per-file": [true, 5], + "quotemark": [true, "single"] + }, + "rulesDirectory": [] +}