Enable TypeScript (#8074)

* Enable Typescript

* Fix tests

* Fix tests again

* Automatically (re)build

* Updates

* Updates

* Update shrinkwrap

* Enable sourcemaps
This commit is contained in:
Koen Kanters 2021-07-21 19:35:14 +02:00 committed by GitHub
parent 321b34721f
commit 7b65dc631b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 4278 additions and 419 deletions

View File

@ -1,5 +1,5 @@
.eslintignore
.eslintrc.json
.eslintrc.js
.git
.github
.gitignore
@ -13,4 +13,5 @@ images
node_modules
scripts
test
coverage
update.sh

45
.eslintrc.js Normal file
View File

@ -0,0 +1,45 @@
module.exports = {
'env': {
'jest/globals': true,
'es6': true,
'node': true,
},
'extends': ['eslint:recommended', 'google', 'plugin:jest/recommended', 'plugin:jest/style'],
'parserOptions': {
'ecmaVersion': 2018,
'sourceType': 'module',
},
'rules': {
'require-jsdoc': 'off',
'indent': ['error', 4],
'max-len': ['error', {'code': 120}],
'no-prototype-builtins': 'off',
},
'plugins': [
'jest',
],
'overrides': [{
files: ['*.ts'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended'],
parserOptions: {
project: './tsconfig.json',
},
rules: {
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/semi': ['error'],
'array-bracket-spacing': ['error', 'never'],
'indent': ['error', 4],
'max-len': ['error', {'code': 120}],
'no-return-await': 'error',
'object-curly-spacing': ['error', 'never'],
},
}],
};

View File

@ -1,21 +0,0 @@
{
"env": {
"jest/globals": true,
"es6": true,
"node": true
},
"extends": ["eslint:recommended", "google", "plugin:jest/recommended", "plugin:jest/style"],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"require-jsdoc": "off",
"indent": ["error", 4],
"max-len": ["error", { "code": 120 }],
"no-prototype-builtins": "off"
},
"plugins": [
"jest"
]
}

View File

@ -24,6 +24,8 @@ jobs:
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: npm ci
- name: Build
run: npm run build
- name: Test
run: npm run test-with-coverage
- name: Lint

3
.gitignore vendored
View File

@ -11,6 +11,9 @@ pids
*.seed
*.pid.lock
# Compiled source
dist/*
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

View File

@ -7,6 +7,7 @@ coverage
.jenkins.yml
.codeclimate.yml
.github
babel.config.js
#linters
.jscsrc
@ -14,11 +15,13 @@ coverage
.eslintrc*
.dockerignore
.eslintignore
.gitignore
#editor settings
.vscode
#src
lib
docs
docker
images

View File

@ -69,6 +69,9 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because
### Internal Architecture
Zigbee2MQTT is made up of three modules, each developed in its own Github project. Starting from the hardware (adapter) and moving up; [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) connects to your Zigbee adapter an makes an API available to the higher levels of the stack. For e.g. Texas Instruments hardware, zigbee-herdsman uses the [TI zStack monitoring and test API](https://github.com/koenkk/zigbee-herdsman/raw/master/docs/Z-Stack%20Monitor%20and%20Test%20API.pdf) to communicate with the adapter. Zigbee-herdsman handles the core Zigbee communication. The module [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters) handles the mapping from individual device models to the Zigbee clusters they support. [Zigbee clusters](https://github.com/Koenkk/zigbee-herdsman/raw/master/docs/Zigbee%20Cluster%20Library%20Specification%20v7.pdf) are the layers of the Zigbee protocol on top of the base protocol that define things like how lights, sensors and switches talk to each other over the Zigbee network. Finally, the Zigbee2MQTT module drives zigbee-herdsman and maps the zigbee messages to MQTT messages. Zigbee2MQTT also keeps track of the state of the system. It uses a `database.db` file to store this state; a text file with a JSON database of connected devices and their capabilities.
### Developing
Zigbee2MQTT uses TypeScript (partially for now). Therefore after making changes to files in the `lib/` directory you need to recompile Zigbee2MQTT. This can be done by executing `npm run build`. For faster development instead of running `npm run build` you can run `npm run build-watch` in another terminal session, this will recompile as you change files.
## Supported devices
See [Supported devices](https://www.zigbee2mqtt.io/information/supported_devices.html) to check whether your device is supported. There is quite an extensive list, including devices from vendors like Xiaomi, Ikea, Philips, OSRAM and more.

6
babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};

View File

@ -5,20 +5,24 @@ RUN apk add --no-cache tzdata eudev tini
COPY package.json ./
# Dependencies
FROM base as dependencies
# Dependencies and build
FROM base as dependencies_and_build
COPY npm-shrinkwrap.json ./
COPY npm-shrinkwrap.json tsconfig.json index.js ./
COPY lib ./lib
RUN apk add --no-cache --virtual .buildtools make gcc g++ python3 linux-headers git && \
npm ci --production && \
npm ci --no-audit --no-optional --no-update-notifier && \
npm run build && \
rm -rf node_modules && \
npm ci --production --no-audit --no-optional --no-update-notifier && \
apk del .buildtools
# Release
FROM base as release
COPY --from=dependencies /app/node_modules ./node_modules
COPY lib ./lib
COPY --from=dependencies_and_build /app/node_modules ./node_modules
COPY --from=dependencies_and_build /app/dist ./dist
COPY LICENSE index.js data/configuration.yaml ./
COPY docker/docker-entrypoint.sh /usr/local/bin/
@ -27,7 +31,7 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh
RUN mkdir /app/data
ARG COMMIT
RUN echo "{\"hash\": \"$COMMIT\"}" > .hash.json
RUN echo "$COMMIT" > dist/.hash
ENTRYPOINT ["docker-entrypoint.sh"]
CMD [ "/sbin/tini", "--", "node", "index.js"]

View File

@ -1 +0,0 @@
qemu-aarch64-static and qemu-arm-static are taken from https://github.com/resin-io/qemu/releases/tag/v2.9.0%2Bresin1

View File

@ -1,10 +1,16 @@
const semver = require('semver');
const engines = require('./package.json').engines;
const indexJsRestart = 'indexjs.restart';
const fs = require('fs');
const path = require('path');
const {exec} = require('child_process');
const rimraf = require('rimraf');
let controller;
let stopping = false;
const hashFile = path.join('dist', '.hash');
async function restart() {
await stop(indexJsRestart);
await start();
@ -16,14 +22,56 @@ async function exit(code, reason) {
}
}
async function currentHash() {
const git = require('git-last-commit');
return new Promise((resolve) => {
git.getLastCommit((err, commit) => {
if (err) resolve('unknown');
else resolve(commit.shortHash);
});
});
}
async function build(reason) {
return new Promise((resolve, reject) => {
process.stdout.write(`Building Zigbee2MQTT... (${reason})`);
rimraf.sync('dist');
exec('npm run build', {cwd: __dirname}, async (err, stdout, stderr) => {
if (err) {
process.stdout.write(', failed\n');
reject(err);
} else {
process.stdout.write(', finished\n');
const hash = await currentHash();
fs.writeFileSync(hashFile, hash);
resolve();
}
});
});
}
async function checkDist() {
if (!fs.existsSync(hashFile)) {
await build('initial build');
}
const distHash = fs.readFileSync(hashFile, 'utf-8');
const hash = await currentHash();
if (hash !== 'unknown' && distHash !== hash) {
await build('hash changed');
}
}
async function start() {
await checkDist();
const version = engines.node;
if (!semver.satisfies(process.version, version)) {
console.log(`\t\tZigbee2MQTT requires node version ${version}, you are running ${process.version}!\n`); // eslint-disable-line
}
// Validate settings
const settings = require('./lib/util/settings');
const settings = require('./dist/util/settings');
settings.reRead();
const errors = settings.validate();
if (errors.length > 0) {
@ -38,7 +86,7 @@ async function start() {
exit(1);
}
const Controller = require('./lib/controller');
const Controller = require('./dist/controller');
controller = new Controller(restart, exit);
await controller.start();
}

View File

@ -1,7 +1,7 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const utils = require('../util/utils');
const zigbee2mqttVersion = require('../../package.json').version;
const zigbee2mqttVersion = require('../..' + '/package.json').version;
const Extension = require('./extension');
const stringify = require('json-stable-stringify-without-jsonify');
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');

View File

@ -52,7 +52,7 @@ function capitalize(s) {
async function getZigbee2mqttVersion() {
return new Promise((resolve, reject) => {
const git = require('git-last-commit');
const packageJSON = require('../../package.json');
const packageJSON = require('../..' + '/package.json');
const version = packageJSON.version;
git.getLastCommit((err, commit) => {
@ -60,8 +60,9 @@ async function getZigbee2mqttVersion() {
if (err) {
try {
commitHash = require('../../.hash.json').hash;
commitHash = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', '.hash'), 'utf-8');
} catch (error) {
/* istanbul ignore next */
commitHash = 'unknown';
}
} else {
@ -75,7 +76,7 @@ async function getZigbee2mqttVersion() {
async function getDependencyVersion(depend) {
return new Promise((resolve, reject) => {
const packageJSON = require('../../node_modules/'+depend+'/package.json');
const packageJSON = require(path.join(__dirname, '..', '..', 'node_modules', depend, 'package.json'));
const version = packageJSON.version;
resolve({version});
});

View File

@ -1,9 +1,12 @@
const yaml = require('js-yaml');
const fs = require('fs');
const equals = require('fast-deep-equal/es6');
import yaml from 'js-yaml';
import fs from 'fs';
import equals from 'fast-deep-equal/es6';
import 'source-map-support/register';
function read(file) {
function read(file: string): Record<string, unknown> {
try {
// eslint-disable-next-line
// @ts-ignore
return yaml.load(fs.readFileSync(file, 'utf8'));
} catch (error) {
if (error.name === 'YAMLException') {
@ -14,18 +17,18 @@ function read(file) {
}
}
function readIfExists(file, default_=null) {
function readIfExists(file: string, default_?: Record<string, unknown>): Record<string, unknown> {
return fs.existsSync(file) ? read(file) : default_;
}
function writeIfChanged(file, content) {
function writeIfChanged(file: string, content: Record<string, unknown>): void {
const before = readIfExists(file);
if (!equals(before, content)) {
fs.writeFileSync(file, yaml.dump(content));
}
}
function updateIfChanged(file, key, value) {
function updateIfChanged(file: string, key: string, value: unknown): void {
const content = read(file);
if (content[key] !== value) {
content[key] = value;

4271
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,9 +20,11 @@
"cc2531"
],
"scripts": {
"test-with-coverage": "jest test --coverage",
"eslint": "node_modules/.bin/eslint lib/",
"build": "node_modules/.bin/tsc",
"build-watch": "node_modules/.bin/tsc --watch",
"eslint": "node_modules/.bin/eslint lib/ --max-warnings=0",
"start": "node index.js",
"test-with-coverage": "jest test --coverage",
"test": "jest test",
"test-watch": "jest test --watch"
},
@ -41,7 +43,7 @@
"git-last-commit": "^1.0.0",
"humanize-duration": "^3.27.0",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "=1.0.1",
"json-stable-stringify-without-jsonify": "^1.0.1",
"mkdir-recursive": "^0.4.0",
"moment": "^2.29.1",
"mqtt": "4.2.8",
@ -57,11 +59,22 @@
"zigbee2mqtt-frontend": "0.4.43"
},
"devDependencies": {
"eslint": "*",
"eslint-config-google": "*",
"eslint-plugin-jest": "*",
"jest": "*",
"tmp": "*"
"@babel/core": "^7.14.6",
"@babel/preset-env": "^7.14.7",
"@babel/preset-typescript": "^7.14.5",
"@types/jest": "^26.0.24",
"@types/js-yaml": "^4.0.2",
"@types/ws": "^7.4.6",
"@typescript-eslint/eslint-plugin": "^4.28.3",
"@typescript-eslint/parser": "^4.28.3",
"babel-jest": "^27.0.6",
"eslint": "^7.30.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-jest": "^24.3.6",
"jest": "^27.0.6",
"source-map-support": "^0.5.19",
"tmp": "^0.2.1",
"typescript": "^4.3.5"
},
"jest": {
"coverageThreshold": {

View File

@ -12,7 +12,7 @@ const debounce = require('debounce');
describe('Bind', () => {
let controller;
mockClear = (device) => {
const mockClear = (device) => {
for (const endpoint of device.endpoints) {
endpoint.read.mockClear();
endpoint.write.mockClear();
@ -33,7 +33,6 @@ describe('Bind', () => {
controller = new Controller(jest.fn(), jest.fn());
await controller.start();
await flushPromises();
this.coordinatorEndoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
});
beforeEach(async () => {
@ -47,7 +46,7 @@ describe('Bind', () => {
await resetExtension();
MQTT.publish.mockClear();
});
afterAll(async () => {
jest.useRealTimers();
})

View File

@ -12,13 +12,14 @@ const mocksClear = [MQTT.publish, logger.warn, logger.debug];
describe('Configure', () => {
let controller;
let coordinatorEndpoint;
expectRemoteConfigured = () => {
const expectRemoteConfigured = () => {
const device = zigbeeHerdsman.devices.remote;
const endpoint1 = device.getEndpoint(1);
expect(endpoint1.bind).toHaveBeenCalledTimes(2);
expect(endpoint1.bind).toHaveBeenCalledWith('genOnOff', this.coordinatorEndoint);
expect(endpoint1.bind).toHaveBeenCalledWith('genLevelCtrl', this.coordinatorEndoint);
expect(endpoint1.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint);
expect(endpoint1.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint);
const endpoint2 = device.getEndpoint(2);
expect(endpoint2.write).toHaveBeenCalledTimes(1);
@ -26,7 +27,7 @@ describe('Configure', () => {
expect(device.meta.configured).toBe(1);
}
expectBulbConfigured = () => {
const expectBulbConfigured = () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint1 = device.getEndpoint(1);
expect(endpoint1.read).toHaveBeenCalledTimes(2);
@ -34,19 +35,19 @@ describe('Configure', () => {
expect(endpoint1.read).toHaveBeenCalledWith('lightingColorCtrl', [ 'colorTempPhysicalMin', 'colorTempPhysicalMax' ]);
}
expectBulbNotConfigured = () => {
const expectBulbNotConfigured = () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint1 = device.getEndpoint(1);
expect(endpoint1.read).toHaveBeenCalledTimes(0);
}
expectRemoteNotConfigured = () => {
const expectRemoteNotConfigured = () => {
const device = zigbeeHerdsman.devices.remote;
const endpoint1 = device.getEndpoint(1);
expect(endpoint1.bind).toHaveBeenCalledTimes(0);
}
mockClear = (device) => {
const mockClear = (device) => {
for (const endpoint of device.endpoints) {
endpoint.read.mockClear();
endpoint.write.mockClear();
@ -71,10 +72,10 @@ describe('Configure', () => {
data.writeDefaultConfiguration();
settings.reRead();
mocksClear.forEach((m) => m.mockClear());
this.coordinatorEndoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
await resetExtension();
});
afterAll(async () => {
jest.useRealTimers();
})

View File

@ -57,10 +57,9 @@ describe('Loads external converters', () => {
data.writeDefaultConfiguration();
settings.reRead();
mocksClear.forEach((m) => m.mockClear());
this.coordinatorEndoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
await resetExtension();
});
afterAll(async () => {
jest.useRealTimers();
});

View File

@ -13,9 +13,11 @@ const HomeAssistant = require('../lib/extension/homeassistant');
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
describe('HomeAssistant extension', () => {
let version;
beforeEach(async () => {
this.version = await require('../lib/util/utils').getZigbee2mqttVersion();
this.version = `Zigbee2MQTT ${this.version.version}`;
version = await require('../lib/util/utils').getZigbee2mqttVersion();
version = `Zigbee2MQTT ${version.version}`;
data.writeDefaultConfiguration();
settings.reRead();
data.writeEmptyState();
@ -52,7 +54,7 @@ describe('HomeAssistant extension', () => {
});
it('Should discover devices and groups', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
let payload;
@ -67,7 +69,7 @@ describe('HomeAssistant extension', () => {
"device":{
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
"name":"ha_discovery_group",
"sw_version":this.version,
"sw_version":version,
},
"max_mireds": 454,
"min_mireds": 250,
@ -95,7 +97,7 @@ describe('HomeAssistant extension', () => {
"device":{
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
"name":"ha_discovery_group",
"sw_version":this.version,
"sw_version":version,
},
"json_attributes_topic":"zigbee2mqtt/ha_discovery_group",
"name":"ha_discovery_group",
@ -125,7 +127,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -151,7 +153,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -177,7 +179,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -203,7 +205,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -230,7 +232,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -258,7 +260,7 @@ describe('HomeAssistant extension', () => {
"manufacturer":"Xiaomi",
"model":"Aqara double key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG03LM)",
"name":"wall_switch_double",
"sw_version":this.version
"sw_version":version
},
"json_attributes_topic":"zigbee2mqtt/wall_switch_double",
"name":"wall_switch_double_left",
@ -290,7 +292,7 @@ describe('HomeAssistant extension', () => {
"manufacturer":"Xiaomi",
"model":"Aqara double key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG03LM)",
"name":"wall_switch_double",
"sw_version":this.version
"sw_version":version
},
"json_attributes_topic":"zigbee2mqtt/wall_switch_double",
"name":"wall_switch_double_right",
@ -328,7 +330,7 @@ describe('HomeAssistant extension', () => {
"manufacturer":"IKEA",
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
"name":"bulb",
"sw_version":this.version,
"sw_version":version,
},
"effect":true,
"effect_list":[
@ -363,7 +365,7 @@ describe('HomeAssistant extension', () => {
retain: false,
})
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
let payload;
@ -381,7 +383,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -407,7 +409,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -433,7 +435,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -472,7 +474,7 @@ describe('HomeAssistant extension', () => {
retain: false,
})
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
let payload;
@ -517,7 +519,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'custom model',
'manufacturer': 'Not from Xiaomi',
},
@ -550,7 +552,7 @@ describe('HomeAssistant extension', () => {
},
})
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
let payload;
@ -566,7 +568,7 @@ describe('HomeAssistant extension', () => {
"manufacturer": "Xiaomi",
"model": "Aqara single key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG04LM)",
"name": "my_switch",
"sw_version": this.version
"sw_version": version
},
"json_attributes_topic": "zigbee2mqtt/my_switch",
"name": "my_light_name_override",
@ -593,7 +595,7 @@ describe('HomeAssistant extension', () => {
retain: false,
})
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
@ -611,7 +613,7 @@ describe('HomeAssistant extension', () => {
retain: false,
})
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
@ -622,7 +624,7 @@ describe('HomeAssistant extension', () => {
});
it('Should discover devices with fan', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
let payload;
@ -652,7 +654,7 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x0017880104e45548"
],
"name":"fan",
"sw_version":this.version,
"sw_version":version,
"model":"Universal wink enabled white ceiling fan premier remote control (99432)",
"manufacturer":"Hampton Bay"
},
@ -668,7 +670,7 @@ describe('HomeAssistant extension', () => {
});
it('Should discover thermostat devices', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
let payload;
@ -688,7 +690,7 @@ describe('HomeAssistant extension', () => {
"manufacturer":"TuYa",
"model":"Radiator valve with thermostat (TS0601_thermostat)",
"name":"TS0601_thermostat",
"sw_version": this.version,
"sw_version": version,
},
"hold_command_topic":"zigbee2mqtt/TS0601_thermostat/set/preset",
"hold_modes":[
@ -728,7 +730,7 @@ describe('HomeAssistant extension', () => {
});
it('Should discover devices with cover_position', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
let payload;
@ -747,7 +749,7 @@ describe('HomeAssistant extension', () => {
{
identifiers: [ 'zigbee2mqtt_0x0017880104e45551' ],
name: 'smart vent',
sw_version: this.version,
sw_version: version,
model: 'Smart vent (SV01)',
manufacturer: 'Keen Home'
},
@ -764,7 +766,7 @@ describe('HomeAssistant extension', () => {
it('Should discover devices with custom homeassistant_discovery_topic', async () => {
settings.set(['advanced', 'homeassistant_discovery_topic'], 'my_custom_discovery_topic')
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
let payload;
@ -782,7 +784,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -800,20 +802,20 @@ describe('HomeAssistant extension', () => {
it('Should throw error when starting with attributes output', async () => {
settings.set(['experimental', 'output'], 'attribute')
expect(() => {
controller = new Controller(false);
const controller = new Controller(false);
}).toThrowError('Home Assitant integration is not possible with attribute output!');
});
it('Should warn when starting with cache_state false', async () => {
settings.set(['advanced', 'cache_state'], false);
logger.warn.mockClear();
controller = new Controller(false);
const controller = new Controller(false);
expect(logger.warn).toHaveBeenCalledWith("In order for HomeAssistant integration to work properly set `cache_state: true");
});
it('Should set missing values to null', async () => {
// https://github.com/Koenkk/zigbee2mqtt/issues/6987
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
@ -832,7 +834,7 @@ describe('HomeAssistant extension', () => {
});
it('Should copy hue/saturtion to h/s if present', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
const device = zigbeeHerdsman.devices.bulb_color;
@ -851,7 +853,7 @@ describe('HomeAssistant extension', () => {
});
it('Should not copy hue/saturtion if properties are missing', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
const device = zigbeeHerdsman.devices.bulb_color;
@ -870,7 +872,7 @@ describe('HomeAssistant extension', () => {
});
it('Should not copy hue/saturtion if color is missing', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
const device = zigbeeHerdsman.devices.bulb_color;
@ -889,7 +891,7 @@ describe('HomeAssistant extension', () => {
});
it('Shouldt discover when already discovered', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
@ -903,7 +905,7 @@ describe('HomeAssistant extension', () => {
});
it('Should discover when not discovered yet', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
controller.extensions.find((e) => e.constructor.name === 'HomeAssistant').discovered = {};
@ -925,7 +927,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -941,7 +943,7 @@ describe('HomeAssistant extension', () => {
});
it('Shouldnt discover when device leaves', async () => {
controller = new Controller(jest.fn(), jest.fn());
const controller = new Controller(jest.fn(), jest.fn());
await controller.start();
await flushPromises();
controller.extensions.find((e) => e.constructor.name === 'HomeAssistant').discovered = {};
@ -954,7 +956,7 @@ describe('HomeAssistant extension', () => {
it('Should send all status when home assistant comes online (default topic)', async () => {
data.writeDefaultState();
controller = new Controller(jest.fn(), jest.fn());
const controller = new Controller(jest.fn(), jest.fn());
await controller.start();
expect(MQTT.subscribe).toHaveBeenCalledWith('homeassistant/status');
await flushPromises();
@ -979,7 +981,7 @@ describe('HomeAssistant extension', () => {
it('Should send all status when home assistant comes online', async () => {
data.writeDefaultState();
controller = new Controller(jest.fn(), jest.fn());
const controller = new Controller(jest.fn(), jest.fn());
await controller.start();
await flushPromises();
expect(MQTT.subscribe).toHaveBeenCalledWith('hass/status');
@ -1004,7 +1006,7 @@ describe('HomeAssistant extension', () => {
it('Shouldnt send all status when home assistant comes offline', async () => {
data.writeDefaultState();
controller = new Controller(jest.fn(), jest.fn());
const controller = new Controller(jest.fn(), jest.fn());
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
@ -1017,7 +1019,7 @@ describe('HomeAssistant extension', () => {
it('Shouldnt send all status when home assistant comes online with different topic', async () => {
data.writeDefaultState();
controller = new Controller(jest.fn(), jest.fn());
const controller = new Controller(jest.fn(), jest.fn());
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
@ -1030,7 +1032,7 @@ describe('HomeAssistant extension', () => {
it('Should discover devices with availability', async () => {
settings.set(['advanced', 'availability_timeout'], 1)
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
let payload;
@ -1048,7 +1050,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -1064,7 +1066,7 @@ describe('HomeAssistant extension', () => {
});
it('Should clear discovery when device is removed', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
@ -1104,7 +1106,7 @@ describe('HomeAssistant extension', () => {
});
it('Should not clear discovery when unsupported device is removed', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
@ -1114,7 +1116,7 @@ describe('HomeAssistant extension', () => {
});
it('Should refresh discovery when device is renamed', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
await MQTT.events.message('homeassistant/device_automation/0x0017880104e45522/action_double/config', stringify({topic: 'zigbee2mqtt/weather_sensor/action'}));
@ -1135,7 +1137,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor_renamed',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -1169,7 +1171,7 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x0017880104e45522"
],
"name":"weather_sensor_renamed",
"sw_version": this.version,
"sw_version": version,
"model":"Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)",
"manufacturer":"Xiaomi"
}
@ -1180,7 +1182,7 @@ describe('HomeAssistant extension', () => {
});
it('Shouldnt refresh discovery when device is renamed and homeassistant_rename is false', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
@ -1206,7 +1208,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor_renamed',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -1222,7 +1224,7 @@ describe('HomeAssistant extension', () => {
});
it('Should discover update_available sensor when device supports it', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
const payload = {
@ -1239,7 +1241,7 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x000b57fffec6a5b2"
],
"name":"bulb",
'sw_version': this.version,
'sw_version': version,
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
"manufacturer":"IKEA"
},
@ -1255,7 +1257,7 @@ describe('HomeAssistant extension', () => {
});
it('Should discover trigger when click is published', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
@ -1282,7 +1284,7 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x0017880104e45520"
],
"name":"button",
"sw_version": this.version,
"sw_version": version,
"model":"Aqara wireless switch (WXKG11LM)",
"manufacturer":"Xiaomi"
}
@ -1306,7 +1308,7 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x0017880104e45520"
],
"name":"button",
"sw_version": this.version,
"sw_version": version,
"model":"Aqara wireless switch (WXKG11LM)",
"manufacturer":"Xiaomi"
}
@ -1411,7 +1413,7 @@ describe('HomeAssistant extension', () => {
settings.set(['device_options'], {
homeassistant: {device_automation: null},
})
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
@ -1435,7 +1437,7 @@ describe('HomeAssistant extension', () => {
friendly_name: 'weather_sensor',
retain: false,
})
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
@ -1448,7 +1450,7 @@ describe('HomeAssistant extension', () => {
it('Should disable Home Assistant legacy triggers', async () => {
settings.set(['advanced', 'homeassistant_legacy_triggers'], false);
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
@ -1476,7 +1478,7 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x0017880104e45520"
],
"name":"button",
"sw_version": this.version,
"sw_version": version,
"model":"Aqara wireless switch (WXKG11LM)",
"manufacturer":"Xiaomi"
}
@ -1507,7 +1509,7 @@ describe('HomeAssistant extension', () => {
});
it('Should republish payload to postfix topic with lightWithPostfix config', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
@ -1520,7 +1522,7 @@ describe('HomeAssistant extension', () => {
});
it('Shouldnt crash in onPublishEntityState on group publish', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
logger.error.mockClear();
@ -1532,7 +1534,7 @@ describe('HomeAssistant extension', () => {
});
it('Should counter an action payload with an empty payload', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
@ -1556,7 +1558,7 @@ describe('HomeAssistant extension', () => {
it('Load Home Assistant mapping from external converters', async () => {
fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'), path.join(data.mockDir, 'mock-external-converter-multiple.js'));
settings.set(['external_converters'], ['mock-external-converter-multiple.js']);
controller = new Controller(jest.fn(), jest.fn());
const controller = new Controller(jest.fn(), jest.fn());
const ha = controller.extensions.find((e) => e.constructor.name === 'HomeAssistant');
await controller.start();
await flushPromises();
@ -1575,7 +1577,7 @@ describe('HomeAssistant extension', () => {
});
it('Should clear outdated configs', async () => {
controller = new Controller(jest.fn(), jest.fn());
let controller = new Controller(jest.fn(), jest.fn());
await controller.start();
await flushPromises();
@ -1670,7 +1672,7 @@ describe('HomeAssistant extension', () => {
it('Should not have Home Assistant legacy entity attributes when disabled', async () => {
settings.set(['advanced', 'homeassistant_legacy_entity_attributes'], false);
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
let payload;
@ -1687,7 +1689,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': this.version,
'sw_version': version,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@ -1703,7 +1705,7 @@ describe('HomeAssistant extension', () => {
});
it('Should rediscover group when device is added to it', async () => {
controller = new Controller(false);
const controller = new Controller(false);
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
@ -1719,7 +1721,7 @@ describe('HomeAssistant extension', () => {
"device":{
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
"name":"ha_discovery_group",
"sw_version":this.version,
"sw_version":version,
},
"json_attributes_topic":"zigbee2mqtt/ha_discovery_group",
"max_mireds": 454,

View File

@ -12,10 +12,11 @@ const flushPromises = require('../lib/flushPromises');
describe('Bridge legacy', () => {
let controller;
let version;
beforeAll(async () => {
jest.useFakeTimers();
this.version = await require('../../lib/util/utils').getZigbee2mqttVersion();
version = await require('../../lib/util/utils').getZigbee2mqttVersion();
controller = new Controller(jest.fn(), jest.fn());
await controller.start();
})
@ -34,7 +35,7 @@ describe('Bridge legacy', () => {
it('Should publish bridge configuration on startup', async () => {
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/config',
stringify({"version":this.version.version,"commit":this.version.commitHash,"coordinator":{"type":"z-Stack","meta":{"version":1, "revision": 20190425}},"network":{"panID":5674,"extendedPanID":[0,11,22],"channel":15},"log_level":'info',"permit_join":false}),
stringify({"version":version.version,"commit":version.commitHash,"coordinator":{"type":"z-Stack","meta":{"version":1, "revision": 20190425}},"network":{"panID":5674,"extendedPanID":[0,11,22],"channel":15},"log_level":'info',"permit_join":false}),
{ retain: true, qos: 0 },
expect.any(Function)
);

View File

@ -60,7 +60,7 @@ describe('Report', () => {
}
}
mockClear = (device) => {
const mockClear = (device) => {
for (const endpoint of device.endpoints) {
endpoint.read.mockClear();
endpoint.write.mockClear();

View File

@ -11,11 +11,11 @@ const stringify = require('json-stable-stringify-without-jsonify');
describe('OTA update', () => {
let controller;
mockClear = (mapped) => {
const mockClear = (mapped) => {
mapped.ota.updateToLatest = jest.fn();
mapped.ota.isUpdateAvailable = jest.fn();
}
beforeAll(async () => {
data.writeDefaultConfiguration();
settings.reRead();

View File

@ -39,7 +39,7 @@ describe('Utils', () => {
expect(await utils.getZigbee2mqttVersion()).toStrictEqual({"commitHash": "123", "version": version});
mockReturnValue = [true, null]
expect(await utils.getZigbee2mqttVersion()).toStrictEqual({"commitHash": "unknown", "version": version});
expect(await utils.getZigbee2mqttVersion()).toStrictEqual({"commitHash": expect.any(String), "version": version});
})
it('Check dependency version', async () => {

28
tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es2018",
"lib": ["es2018"],
"noImplicitAny": true,
"noImplicitThis": true,
"moduleResolution": "node",
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"baseUrl": ".",
"allowJs": true,
"rootDir": "lib",
"inlineSourceMap": true,
"resolveJsonModule": true,
"paths": {
"*": [
"node_modules/*",
"lib/types/*"
]
},
},
"include": [
"lib/**/*"
],
}