diff --git a/.eslintignore b/.eslintignore index aba100d4a8cb4..32fa9fa91f027 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,5 @@ test/* index.* converters/* devices/* -lib/* \ No newline at end of file +lib/* +scripts/* \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 0ca99beb72c19..69603b245d842 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,8 +11,7 @@ ], "root": true, "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module" + "project": "./tsconfig.json" }, "rules": { "require-jsdoc": "off", @@ -22,6 +21,7 @@ "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-floating-promises": "error", "tsdoc/syntax": "warn", "valid-jsdoc": "off" }, diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 07c56d76a423a..0000000000000 --- a/jest.config.js +++ /dev/null @@ -1,10 +0,0 @@ -// eslint-disable-next-line tsdoc/syntax -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: [ - 'test/', - 'src/', - ], -}; diff --git a/package.json b/package.json index aee17c76cc207..a18769e30fbf7 100644 --- a/package.json +++ b/package.json @@ -67,5 +67,13 @@ "ts-jest": "^29.1.2", "ts-morph": "^22.0.0", "typescript": "^5.4.5" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "roots": [ + "test/", + "src/" + ] } } diff --git a/src/converters/toZigbee.ts b/src/converters/toZigbee.ts index 4a2df2df3e89f..6d73f9e832a55 100644 --- a/src/converters/toZigbee.ts +++ b/src/converters/toZigbee.ts @@ -1262,9 +1262,9 @@ const converters2 = { const transition = meta.message.transition ?? 15; utils.assertNumber(transition, 'transition'); const speed = Math.min(255, Math.max(1, Math.round(255 / transition))); - converters2.light_hue_saturation_move.convertSet(entity, 'hue_move', speed, meta); + await converters2.light_hue_saturation_move.convertSet(entity, 'hue_move', speed, meta); } else if (value === 'stop_colorloop') { - converters2.light_hue_saturation_move.convertSet(entity, 'hue_move', 'stop', meta); + await converters2.light_hue_saturation_move.convertSet(entity, 'hue_move', 'stop', meta); } else { const payload = {effectid: utils.getFromLookup(value, lookup), effectvariant: 0}; await entity.command('genIdentify', 'triggerEffect', payload, utils.getOptions(meta.mapped, entity)); @@ -4189,7 +4189,7 @@ const converters2 = { convertSet: async (entity, key, value, meta) => { utils.assertEndpoint(entity); const keypadLockout = utils.getKey(constants.keypadLockoutMode, value, value, Number); - entity.write('hvacUserInterfaceCfg', {keypadLockout}); + await entity.write('hvacUserInterfaceCfg', {keypadLockout}); entity.saveClusterAttributeKeyValue('hvacUserInterfaceCfg', {keypadLockout}); return {state: {keypad_lockout: value}}; }, @@ -4275,7 +4275,7 @@ const converters2 = { key: ['local_temperature_calibration'], convertSet: async (entity, key, value, meta) => { utils.assertNumber(value); - entity.write('hvacThermostat', {localTemperatureCalibration: Math.round(value * 10)}, + await entity.write('hvacThermostat', {localTemperatureCalibration: Math.round(value * 10)}, {srcEndpoint: 11, disableDefaultResponse: true}); return {state: {local_temperature_calibration: value}}; }, diff --git a/src/devices/danfoss.ts b/src/devices/danfoss.ts index a7b66a6ca781f..059040b8dab46 100644 --- a/src/devices/danfoss.ts +++ b/src/devices/danfoss.ts @@ -215,7 +215,7 @@ const definitions: Definition[] = [ const time = Math.round(((new Date()).getTime() - constants.OneJanuary2000) / 1000); // Time-master + synchronised const values = {timeStatus: 3, time: time, timeZone: ((new Date()).getTimezoneOffset() * -1) * 60}; - endpoint.write('genTime', values); + await endpoint.write('genTime', values); }, }, { diff --git a/src/devices/heiman.ts b/src/devices/heiman.ts index 3300023a3ae88..125b507802c0d 100644 --- a/src/devices/heiman.ts +++ b/src/devices/heiman.ts @@ -539,7 +539,7 @@ const definitions: Definition[] = [ const time = Math.round(((new Date()).getTime() - constants.OneJanuary2000) / 1000); // Time-master + synchronised const values = {timeStatus: 3, time: time, timeZone: ((new Date()).getTimezoneOffset() * -1) * 60}; - endpoint.write('genTime', values); + await endpoint.write('genTime', values); }, exposes: [e.battery(), e.temperature(), e.humidity(), e.pm25(), e.hcho(), e.voc(), e.aqi(), e.pm10(), e.enum('battery_state', ea.STATE, ['not_charging', 'charging', 'charged'])], diff --git a/src/devices/shinasystem.ts b/src/devices/shinasystem.ts index fe488ad1fce19..3875148e01b93 100644 --- a/src/devices/shinasystem.ts +++ b/src/devices/shinasystem.ts @@ -46,10 +46,10 @@ const fzLocal = { GCM300Z_valve_status: { cluster: 'genOnOff', type: ['attributeReport', 'readResponse'], - convert: (model, msg, publish, options, meta) => { + convert: async (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('onOff')) { const endpoint = meta.device.getEndpoint(1); - endpoint.read('genOnOff', [0x9007]); // for update : close_remain_timeout + await endpoint.read('genOnOff', [0x9007]); // for update : close_remain_timeout return {gas_valve_state: msg.data['onOff'] === 1 ? 'OPEN' : 'CLOSE'}; } }, diff --git a/src/devices/sunricher.ts b/src/devices/sunricher.ts index ff320fc4329ec..5cf34d111ddfc 100644 --- a/src/devices/sunricher.ts +++ b/src/devices/sunricher.ts @@ -448,7 +448,7 @@ const definitions: Definition[] = [ const endpoint = device.getEndpoint(1); const hours24 = 1000 * 60 * 60 * 24; // Device does not ask for the time with binding, therefore we write the time every 24 hours - const interval = setInterval(async () => syncTime(endpoint), hours24); + const interval = setInterval(async () => await syncTime(endpoint), hours24); globalStore.putValue(device, 'time', interval); } }, @@ -648,7 +648,7 @@ const definitions: Definition[] = [ ); // Device does not asks for the time with binding, we need to write time during configure - syncTime(endpoint); + await syncTime(endpoint); // Trigger initial read await endpoint.read('hvacThermostat', ['systemMode', 'runningState', 'occupiedHeatingSetpoint']); diff --git a/src/devices/tuya.ts b/src/devices/tuya.ts index 56f15787426de..139152c359023 100644 --- a/src/devices/tuya.ts +++ b/src/devices/tuya.ts @@ -6677,7 +6677,7 @@ const definitions: Definition[] = [ configure: async (device, coordinatorEndpoint) => { await tuya.configureMagicPacket(device, coordinatorEndpoint); const endpoint = device.getEndpoint(1); - endpoint.command('genBasic', 'tuyaSetup', {}); + await endpoint.command('genBasic', 'tuyaSetup', {}); await reporting.bind(endpoint, coordinatorEndpoint, ['msTemperatureMeasurement']); await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff', 'haElectricalMeasurement', 'seMetering']); await reporting.rmsVoltage(endpoint, {change: 5}); diff --git a/src/devices/ubisys.ts b/src/devices/ubisys.ts index a97db58062a7b..fa0b78aeb37d5 100644 --- a/src/devices/ubisys.ts +++ b/src/devices/ubisys.ts @@ -229,7 +229,7 @@ const ubisys = { await sleepSeconds(2); // re-read and dump all relevant attributes log(' Done - will now read back the results.'); - ubisys.tz.configure_j1.convertGet(entity, key, meta); + await ubisys.tz.configure_j1.convertGet(entity, key, meta); } }, convertGet: async (entity, key, meta) => { @@ -287,7 +287,7 @@ const ubisys = { await entity.write('manuSpecificUbisysDimmerSetup', {'mode': utils.getFromLookup(phaseControl, phaseControlValues)}, manufacturerOptions.ubisysNull); } - ubisys.tz.dimmer_setup.convertGet(entity, key, meta); + await ubisys.tz.dimmer_setup.convertGet(entity, key, meta); }, convertGet: async (entity, key, meta) => { await entity.read('manuSpecificUbisysDimmerSetup', ['capabilities'], manufacturerOptions.ubisysNull); @@ -301,7 +301,7 @@ const ubisys = { if (key === 'minimum_on_level') { await entity.write('genLevelCtrl', {'ubisysMinimumOnLevel': value}, manufacturerOptions.ubisys); } - ubisys.tz.dimmer_setup_genLevelCtrl.convertGet(entity, key, meta); + await ubisys.tz.dimmer_setup_genLevelCtrl.convertGet(entity, key, meta); }, convertGet: async (entity, key, meta) => { await entity.read('genLevelCtrl', ['ubisysMinimumOnLevel'], manufacturerOptions.ubisys); diff --git a/src/lib/ikea.ts b/src/lib/ikea.ts index 732fa4c31e9db..4b4de4f80c0c3 100644 --- a/src/lib/ikea.ts +++ b/src/lib/ikea.ts @@ -45,11 +45,11 @@ const bulbOnEvent: OnEvent = async (type, data, device, options, state: KeyValue // we only restore if true, to save unneeded network writes const colorOptions = state.color_options as KeyValue; if (colorOptions?.execute_if_off === true) { - device.endpoints[0].write('lightingColorCtrl', {'options': 1}); + await device.endpoints[0].write('lightingColorCtrl', {'options': 1}); } const levelConfig = state.level_config as KeyValue; if (levelConfig?.execute_if_off === true) { - device.endpoints[0].write('genLevelCtrl', {'options': 1}); + await device.endpoints[0].write('genLevelCtrl', {'options': 1}); } if (levelConfig?.on_level !== undefined) { const onLevelRaw = levelConfig.on_level; @@ -62,7 +62,7 @@ const bulbOnEvent: OnEvent = async (type, data, device, options, state: KeyValue if (onLevel > 255) onLevel = 254; if (onLevel < 1) onLevel = 1; - device.endpoints[0].write('genLevelCtrl', {onLevel: onLevel}); + await device.endpoints[0].write('genLevelCtrl', {onLevel: onLevel}); } } }; diff --git a/src/lib/lumi.ts b/src/lib/lumi.ts index 5ccd197803110..d498bf8febe5b 100644 --- a/src/lib/lumi.ts +++ b/src/lib/lumi.ts @@ -1501,7 +1501,7 @@ export const lumiModernExtend = { const fromZigbee: Fz.Converter[] = [{ cluster: 'manuSpecificLumi', type: ['attributeReport', 'readResponse'], - convert: (model, msg, publish, options, meta) => { + convert: async (model, msg, publish, options, meta) => { // At least the Aqara TVOC sensor does not send a deviceAnnounce after comming back online. // The reconfigureReportingsOnDeviceAnnounce modernExtend is not usable because of this, // there is however an outage counter published in the 'special' buffer data reported @@ -1520,12 +1520,12 @@ export const lumiModernExtend = { for (const endpoint of meta.device.endpoints) { // restore bindings for (const b of endpoint.binds) { - endpoint.bind(b.cluster.name, b.target); + await endpoint.bind(b.cluster.name, b.target); } // restore reporting for (const c of endpoint.configuredReportings) { - endpoint.configureReporting(c.cluster.name, [{ + await endpoint.configureReporting(c.cluster.name, [{ attribute: c.attribute.name, minimumReportInterval: c.minimumReportInterval, maximumReportInterval: c.maximumReportInterval, reportableChange: c.reportableChange, }]); diff --git a/src/lib/ota/common.ts b/src/lib/ota/common.ts index b2bc3d880dbe5..7a2c6c6ff4691 100644 --- a/src/lib/ota/common.ts +++ b/src/lib/ota/common.ts @@ -535,52 +535,64 @@ export async function updateToLatest(device: Zh.Device, onProgress: Ota.OnProgre const answerNextImageRequest = () => { waiters.nextImageRequest = endpoint.waitForCommand('genOta', 'queryNextImageRequest', null, maxTimeout); - waiters.nextImageRequest.promise.then((payload) => { - answerNextImageRequest(); - sendQueryNextImageResponse(endpoint, image, payload.header.transactionSequenceNumber); - }); + waiters.nextImageRequest.promise.then( + (payload) => { + answerNextImageRequest(); + sendQueryNextImageResponse(endpoint, image, payload.header.transactionSequenceNumber); + }, + () => { + cancelWaiters(waiters); + reject(new Error(`OTA: Failed queryNextImageRequest`)); + } + ); }; // No need to timeout here, will already be done in answerNextImageBlockRequest waiters.upgradeEndRequest = endpoint.waitForCommand('genOta', 'upgradeEndRequest', null, maxTimeout); - waiters.upgradeEndRequest.promise.then((data) => { - logger.debug(`Got upgrade end request for '${device.ieeeAddr}' (${device.modelID}): ${JSON.stringify(data.payload)}`, NS); - cancelWaiters(waiters); - - if (data.payload.status === 0) { - const payload = { - manufacturerCode: image.header.manufacturerCode, imageType: image.header.imageType, - fileVersion: image.header.fileVersion, currentTime: 0, upgradeTime: 1, - }; - - endpoint.commandResponse('genOta', 'upgradeEndResponse', payload, null, data.header.transactionSequenceNumber).then( - () => { - logger.debug(`Update succeeded, waiting for device announce`, NS); - onProgress(100, null); - - let timer: ReturnType = null; - const cb = () => { - logger.debug(`Got device announce or timed out, call resolve`, NS); - clearInterval(timer); - device.removeListener('deviceAnnounce', cb); - resolve(image.header.fileVersion); - }; - timer = setTimeout(cb, 120 * 1000); // timeout after 2 minutes - device.once('deviceAnnounce', cb); - }, - (e) => { - const message = `OTA: Upgrade end response failed (${e.message})`; - logger.debug(message, NS); - reject(new Error(message)); - }, - ); - } else { - // @ts-expect-error - const error = `Update failed with reason: '${endRequestCodeLookup[data.payload.status]}'`; - logger.debug(error, NS); - reject(new Error(error)); + waiters.upgradeEndRequest.promise.then( + (data) => { + logger.debug(`Got upgrade end request for '${device.ieeeAddr}' (${device.modelID}): ${JSON.stringify(data.payload)}`, NS); + cancelWaiters(waiters); + + if (data.payload.status === 0) { + const payload = { + manufacturerCode: image.header.manufacturerCode, imageType: image.header.imageType, + fileVersion: image.header.fileVersion, currentTime: 0, upgradeTime: 1, + }; + + endpoint.commandResponse('genOta', 'upgradeEndResponse', payload, null, data.header.transactionSequenceNumber).then( + () => { + logger.debug(`Update succeeded, waiting for device announce`, NS); + onProgress(100, null); + + let timer: ReturnType = null; + const cb = () => { + logger.debug(`Got device announce or timed out, call resolve`, NS); + clearInterval(timer); + device.removeListener('deviceAnnounce', cb); + resolve(image.header.fileVersion); + }; + timer = setTimeout(cb, 120 * 1000); // timeout after 2 minutes + device.once('deviceAnnounce', cb); + }, + (e) => { + const message = `OTA: Upgrade end response failed (${e.message})`; + logger.debug(message, NS); + reject(new Error(message)); + }, + ); + } else { + // @ts-expect-error + const error = `Update failed with reason: '${endRequestCodeLookup[data.payload.status]}'`; + logger.debug(error, NS); + reject(new Error(error)); + } + }, + () => { + cancelWaiters(waiters); + reject(new Error(`OTA: Failed upgradeEndRequest`)); } - }); + ); logger.debug(`Starting upgrade`, NS); answerNextImageBlockOrPageRequest(); diff --git a/tsconfig.json b/tsconfig.json index 17552619b1fdb..009e95d42160d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,21 @@ { "compilerOptions": { - "allowSyntheticDefaultImports": true, - "module": "commonjs", - "esModuleInterop": true, - "target": "ES2022", - "lib": ["ES2022"], - "noImplicitAny": true, - "noImplicitThis": true, - "moduleResolution": "node", - "sourceMap": true, - "declaration": true, - "declarationMap": true, - "outDir": ".", - "baseUrl": ".", - "resolveJsonModule": true, - "incremental": true + "allowSyntheticDefaultImports": true, + "module": "commonjs", + "esModuleInterop": true, + "target": "ES2022", + "lib": ["ES2022"], + "noImplicitAny": true, + "noImplicitThis": true, + "moduleResolution": "node", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "outDir": ".", + "baseUrl": ".", + "resolveJsonModule": true, + "incremental": true }, "include": ["./src/"], "exclude": [] - } +}