mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 15:18:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2dd21e164 | ||
|
|
01f4f1f940 | ||
|
|
563eb3a890 | ||
|
|
ab4101dae8 | ||
|
|
9ab37735fe | ||
|
|
8f275f414b | ||
|
|
55b17c95a4 | ||
|
|
13c7348a97 | ||
|
|
62453a5d2a | ||
|
|
9840433fc0 | ||
|
|
8fe36d74ec | ||
|
|
8e85a1e081 | ||
|
|
ce9a8ed86f | ||
|
|
49a1f56641 | ||
|
|
76cf6fa444 | ||
|
|
e1ecbd97d0 | ||
|
|
c389ab28cd | ||
|
|
4c0c69b15f | ||
|
|
704bbb54d3 | ||
|
|
17947aa809 | ||
|
|
33811fec9f | ||
|
|
76cd07a701 | ||
|
|
e2b18d8f02 | ||
|
|
b3c886638a | ||
|
|
f9121b2f0f | ||
|
|
fb827f9dc0 | ||
|
|
e5be5b0219 | ||
|
|
c360f5a7b1 | ||
|
|
15f741b446 | ||
|
|
3b648c9e63 | ||
|
|
951360beba | ||
|
|
71df01d613 | ||
|
|
a8f4daff30 | ||
|
|
d7dc083351 | ||
|
|
45d55c9c71 | ||
|
|
085d5c9f8b | ||
|
|
09a5650345 | ||
|
|
24050a3a10 | ||
|
|
c6a2400f56 | ||
|
|
9da7f53e78 | ||
|
|
f5a2c802ca | ||
|
|
372d991db9 | ||
|
|
bab0a3e902 | ||
|
|
b85aaf8e54 | ||
|
|
64d744ea88 | ||
|
|
7748dd3321 | ||
|
|
24c6b2e2c9 | ||
|
|
9589d12353 | ||
|
|
dcc13b7b53 | ||
|
|
2d1733d0e7 | ||
|
|
1368a78767 | ||
|
|
f29073e413 | ||
|
|
7005497cba | ||
|
|
298c595deb | ||
|
|
bdbe2b4a70 | ||
|
|
653334b208 | ||
|
|
24c6c0bf8e | ||
|
|
3e252e4307 | ||
|
|
ae2de04251 | ||
|
|
07d238d5b9 | ||
|
|
407f0df6ad | ||
|
|
2e593e37a6 | ||
|
|
9f7fd479c4 | ||
|
|
0407a3b67c | ||
|
|
2c57bf81b0 | ||
|
|
960456401e | ||
|
|
d4774526c3 | ||
|
|
2a365d39ac | ||
|
|
347c023968 | ||
|
|
4272d9a13e | ||
|
|
54900c3680 | ||
|
|
461519cf39 | ||
|
|
8edb176dc0 | ||
|
|
61cdda96ff | ||
|
|
43394b3376 | ||
|
|
ca7f9ec8ff | ||
|
|
0857d087bd | ||
|
|
818a9e3946 | ||
|
|
51460aacc8 | ||
|
|
354beb486d | ||
|
|
8261d4efc6 | ||
|
|
bbb19371f3 | ||
|
|
04ae93f00c | ||
|
|
999c01086e | ||
|
|
0e67f8fa0a | ||
|
|
f352d18854 | ||
|
|
f865c976e3 | ||
|
|
aa90765db8 | ||
|
|
017fc03157 |
@@ -0,0 +1,64 @@
|
||||
name: Wrong device picture/vendor/model/description
|
||||
description: Use if device is detected as supported and is fully functional but has a wrong picture, vendor, model or description
|
||||
title: "[Wrong device]: "
|
||||
labels: [wrong device]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Only use this if the device is detected as **supported** and is **fully functional** but has a wrong picture, vendor, model or description.
|
||||
- type: input
|
||||
id: link
|
||||
attributes:
|
||||
label: Link
|
||||
description: Link of this device (product page)
|
||||
placeholder: https://www.linktomydevice.org
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: model
|
||||
attributes:
|
||||
label: Model
|
||||
description: Expected model
|
||||
placeholder: RTCGQ01LM
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Expected description
|
||||
placeholder: Motion sensor
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: vendor
|
||||
attributes:
|
||||
label: Vendor
|
||||
description: Expected vendor
|
||||
placeholder: Xiaomi
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: picture
|
||||
attributes:
|
||||
label: Picture (link)
|
||||
description: Expected picture
|
||||
placeholder: https://www.linktomydevice.org/RTCGQ01LM.jpg
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: database
|
||||
attributes:
|
||||
label: Database entry
|
||||
description: Entry of this device in `data/database.db` after pairing it
|
||||
placeholder: '{"id":53,"type":"Router","ieeeAddr":"0x10458d00024284f69","nwkAddr":10148,"manufId":4151,"manufName":"LUMI","powerSource":"DC Source","modelId":"lumi.relay.c2acn01","epList":[1,2],"endpoints":{"1":{"profId":260,"epId":1,"devId":257,"inClusterList":[0,3,4,5,1,2,10,6,16,2820,12],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"modelId":"lumi.relay.c2acn01","appVersion":1,"manufacturerName":"LUMI","powerSource":4,"zclVersion":0,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020"}},"genAnalogInput":{"attributes":{"presentValue":129.04425048828125}},"genOnOff":{"attributes":{"61440":117440715,"onOff":1}}},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":260,"epId":2,"devId":257,"inClusterList":[6,16,4,5],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"61440":237478966,"onOff":0}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":1,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020","zclVersion":0,"interviewCompleted":true,"meta":{},"lastSeen":1640285631405}'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
placeholder: Some additional notes...
|
||||
validations:
|
||||
required: false
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node: [14, 16, 18, 19]
|
||||
node: [16, 18, 20]
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: true
|
||||
steps:
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write # for actions/stale to close stale PRs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v7
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days'
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
node-version: 18
|
||||
- run: npx npm-check-updates -u -x connect-gzip-static # connect-gzip-static only supports node 16
|
||||
- run: npm install
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
commit-message: Update dependencies
|
||||
branch: "update_deps"
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm install zigbee2mqtt-frontend@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
id: cpr
|
||||
with:
|
||||
commit-message: Update zigbee2mqtt-frontend to ${{ github.event.client_payload.version }}
|
||||
branch: "update_frontend"
|
||||
title: Update zigbee-frontend to ${{ github.event.client_payload.version }}
|
||||
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
|
||||
- uses: peter-evans/enable-pull-request-automerge@v2
|
||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.CR_PAT }}
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm install zigbee-herdsman@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
id: cpr
|
||||
with:
|
||||
commit-message: Update zigbee-herdsman to ${{ github.event.client_payload.version }}
|
||||
branch: "update_zh"
|
||||
title: Update zigbee-herdsman to ${{ github.event.client_payload.version }}
|
||||
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
|
||||
- uses: peter-evans/enable-pull-request-automerge@v2
|
||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.CR_PAT }}
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
with:
|
||||
node-version: 18
|
||||
- run: npm install zigbee-herdsman-converters@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
id: cpr
|
||||
with:
|
||||
commit-message: Update zigbee-herdsman-converters to ${{ github.event.client_payload.version }}
|
||||
branch: "update_zhc"
|
||||
title: Update zigbee-herdsman-converters to ${{ github.event.client_payload.version }}
|
||||
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
|
||||
- uses: peter-evans/enable-pull-request-automerge@v2
|
||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.CR_PAT }}
|
||||
|
||||
@@ -53,8 +53,8 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because
|
||||
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/7738048/40914297-49e6e560-6800-11e8-8904-36cce896e5a8.png">
|
||||
|
||||
### [Home Assistant](https://www.home-assistant.io/)
|
||||
- [Home Assistant](https://www.home-assistant.io/hassio/): Using [the official addon](https://github.com/zigbee2mqtt/hassio-zigbee2mqtt)
|
||||
- Generic install: using instructions [here](https://www.zigbee2mqtt.io/guide/usage/integrations/home_assistant.html)
|
||||
- [Home Assistant OS](https://www.home-assistant.io/installation/): Using [the official addon](https://github.com/zigbee2mqtt/hassio-zigbee2mqtt)
|
||||
- Other installation: using instructions [here](https://www.zigbee2mqtt.io/guide/usage/integrations/home_assistant.html)
|
||||
|
||||
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/2734836/47615848-b8dd8700-dabd-11e8-9d77-175002dd8987.png">
|
||||
|
||||
@@ -73,7 +73,7 @@ Zigbee2MQTT is made up of three modules, each developed in its own Github projec
|
||||
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/supported-devices) to check whether your device is supported. There is quite an extensive list, including devices from vendors like Xiaomi, Ikea, Philips, OSRAM and more.
|
||||
See [Supported devices](https://www.zigbee2mqtt.io/supported-devices) to check whether your device is supported. There is quite an extensive list, including devices from vendors like [Xiaomi](https://www.zigbee2mqtt.io/supported-devices/#v=Xiaomi), [Ikea](https://www.zigbee2mqtt.io/supported-devices/#v=IKEA), [Philips](https://www.zigbee2mqtt.io/supported-devices/#v=Philips), [OSRAM](https://www.zigbee2mqtt.io/supported-devices/#v=OSRAM) and more.
|
||||
|
||||
If it's not listed in [Supported devices](https://www.zigbee2mqtt.io/supported-devices), support can be added (fairly) easily, see [How to support new devices](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.17.2 as base
|
||||
FROM alpine:3.17.3 as base
|
||||
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache tzdata eudev tini nodejs
|
||||
|
||||
@@ -4,7 +4,7 @@ const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const {exec} = require('child_process');
|
||||
const rimraf = require('rimraf');
|
||||
const {rimrafSync} = require('rimraf');
|
||||
require('source-map-support').install();
|
||||
|
||||
let controller;
|
||||
@@ -41,7 +41,7 @@ async function writeHash() {
|
||||
async function build(reason) {
|
||||
return new Promise((resolve, reject) => {
|
||||
process.stdout.write(`Building Zigbee2MQTT... (${reason})`);
|
||||
rimraf.sync('dist');
|
||||
rimrafSync('dist');
|
||||
const env = {...process.env};
|
||||
const _600mb = 629145600;
|
||||
if (_600mb > os.totalmem() && !env.NODE_OPTIONS) {
|
||||
|
||||
@@ -47,7 +47,7 @@ export default class Availability extends Extension {
|
||||
private resetTimer(device: Device): void {
|
||||
clearTimeout(this.timers[device.ieeeAddr]);
|
||||
|
||||
// If the timer triggers, the device is not avaiable anymore otherwise resetTimer already have been called
|
||||
// If the timer triggers, the device is not available anymore otherwise resetTimer already have been called
|
||||
if (this.isActiveDevice(device)) {
|
||||
// If device did not check in, ping it, if that fails it will be marked as offline
|
||||
this.timers[device.ieeeAddr] = setTimeout(
|
||||
@@ -82,7 +82,7 @@ export default class Availability extends Extension {
|
||||
const disableRecovery = !(i == 1 && available);
|
||||
await device.zh.ping(disableRecovery);
|
||||
pingedSuccessfully = true;
|
||||
logger.debug(`Succesfully pinged '${device.name}' (attempt ${i + 1}/${attempts})`);
|
||||
logger.debug(`Successfully pinged '${device.name}' (attempt ${i + 1}/${attempts})`);
|
||||
break;
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to ping '${device.name}' (attempt ${i + 1}/${attempts}, ${error.message})`);
|
||||
|
||||
+20
-20
@@ -4,7 +4,7 @@ import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import debounce from 'debounce';
|
||||
import * as zigbeeHersdman from 'zigbee-herdsman/dist';
|
||||
import * as zigbeeHerdsman from 'zigbee-herdsman/dist';
|
||||
import bind from 'bind-decorator';
|
||||
import Device from '../model/device';
|
||||
import Group from '../model/group';
|
||||
@@ -97,12 +97,12 @@ const pollOnMessage: PollOnMessage = [
|
||||
read: {cluster: 'genLevelCtrl', attributes: ['currentLevel']},
|
||||
// When the bound devices/members of group have the following manufacturerIDs
|
||||
manufacturerIDs: [
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.TELINK,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.BUSCH_JAEGER,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.TELINK,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.BUSCH_JAEGER,
|
||||
],
|
||||
manufacturerNames: [
|
||||
'GLEDOPTO',
|
||||
@@ -133,12 +133,12 @@ const pollOnMessage: PollOnMessage = [
|
||||
},
|
||||
read: {cluster: 'genOnOff', attributes: ['onOff']},
|
||||
manufacturerIDs: [
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.TELINK,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.BUSCH_JAEGER,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.TELINK,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.BUSCH_JAEGER,
|
||||
],
|
||||
manufacturerNames: [
|
||||
'GLEDOPTO',
|
||||
@@ -165,11 +165,11 @@ const pollOnMessage: PollOnMessage = [
|
||||
},
|
||||
},
|
||||
manufacturerIDs: [
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.TELINK,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.TELINK,
|
||||
// Note: ManufacturerCode.BUSCH_JAEGER is left out intentionally here as their devices don't support colors
|
||||
],
|
||||
manufacturerNames: [
|
||||
@@ -389,7 +389,7 @@ export default class Bind extends Extension {
|
||||
}
|
||||
|
||||
await endpoint.configureReporting(bind.cluster.name, items);
|
||||
logger.info(`Succesfully setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
|
||||
logger.info(`Successfully setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
|
||||
}
|
||||
@@ -431,7 +431,7 @@ export default class Bind extends Extension {
|
||||
}
|
||||
|
||||
await endpoint.configureReporting(cluster, items);
|
||||
logger.info(`Succesfully disabled reporting for '${entity}' cluster '${cluster}'`);
|
||||
logger.info(`Successfully disabled reporting for '${entity}' cluster '${cluster}'`);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to disable reporting for '${entity}' cluster '${cluster}'`);
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ export default class Bridge extends Extension {
|
||||
logger.setLevel(newSettings.advanced.log_level);
|
||||
}
|
||||
|
||||
logger.info('Succesfully changed options');
|
||||
logger.info('Successfully changed options');
|
||||
this.publishInfo();
|
||||
return utils.getResponse(message, {restart_required: this.restartRequired}, null);
|
||||
}
|
||||
@@ -470,7 +470,7 @@ export default class Bridge extends Extension {
|
||||
this.publishInfo();
|
||||
}
|
||||
|
||||
// Repulish entity state
|
||||
// Republish entity state
|
||||
this.publishEntityState(entity, {});
|
||||
|
||||
return utils.getResponse(
|
||||
|
||||
@@ -85,7 +85,7 @@ export default class Configure extends Extension {
|
||||
}
|
||||
|
||||
private async configure(device: Device, event: 'started' | 'zigbee_event' | 'reporting_disabled' | 'mqtt_message',
|
||||
force=false, thowError=false): Promise<void> {
|
||||
force=false, throwError=false): Promise<void> {
|
||||
if (!force) {
|
||||
if (device.options.disabled || !device.definition?.configure || !device.zh.interviewCompleted) {
|
||||
return;
|
||||
@@ -126,7 +126,7 @@ export default class Configure extends Extension {
|
||||
const msg = `Failed to configure '${device.name}', attempt ${attempt} (${error.stack})`;
|
||||
logger.error(msg);
|
||||
|
||||
if (thowError) {
|
||||
if (throwError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ export default class ExternalExtension extends Extension {
|
||||
const basePath = this.getExtensionsBasePath();
|
||||
if (fs.existsSync(basePath)) {
|
||||
return fs.readdirSync(basePath).filter((f) => f.endsWith('.js')).map((fileName) => {
|
||||
const extensonFilePath = path.join(basePath, fileName);
|
||||
return {'name': fileName, 'code': fs.readFileSync(extensonFilePath, 'utf-8')};
|
||||
const extensionFilePath = path.join(basePath, fileName);
|
||||
return {'name': fileName, 'code': fs.readFileSync(extensionFilePath, 'utf-8')};
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
@@ -63,8 +63,8 @@ export default class ExternalExtension extends Extension {
|
||||
if (!fs.existsSync(basePath)) {
|
||||
fs.mkdirSync(basePath);
|
||||
}
|
||||
const extensonFilePath = path.join(basePath, path.basename(name));
|
||||
fs.writeFileSync(extensonFilePath, code);
|
||||
const extensionFilePath = path.join(basePath, path.basename(name));
|
||||
fs.writeFileSync(extensionFilePath, code);
|
||||
this.publishExtensions();
|
||||
logger.info(`Extension ${name} loaded`);
|
||||
return utils.getResponse(message, {}, null);
|
||||
|
||||
@@ -74,8 +74,13 @@ export default class Frontend extends Extension {
|
||||
this.wss = new WebSocket.Server({noServer: true});
|
||||
this.wss.on('connection', this.onWebSocketConnection);
|
||||
|
||||
this.server.listen(this.port, this.host);
|
||||
logger.info(`Started frontend on port ${this.host}:${this.port}`);
|
||||
if (this.host.startsWith('/')) {
|
||||
this.server.listen(this.host);
|
||||
logger.info(`Started frontend on socket ${this.host}`);
|
||||
} else {
|
||||
this.server.listen(this.port, this.host);
|
||||
logger.info(`Started frontend on port ${this.host}:${this.port}`);
|
||||
}
|
||||
}
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
@@ -103,8 +108,8 @@ export default class Frontend extends Extension {
|
||||
|
||||
@bind private onUpgrade(request: http.IncomingMessage, socket: net.Socket, head: Buffer): void {
|
||||
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
this.authenticate(request, (isAuthentificated) => {
|
||||
if (isAuthentificated) {
|
||||
this.authenticate(request, (isAuthenticated) => {
|
||||
if (isAuthenticated) {
|
||||
this.wss.emit('connection', ws, request);
|
||||
} else {
|
||||
ws.close(4401, 'Unauthorized');
|
||||
|
||||
@@ -237,7 +237,7 @@ export default class HomeAssistant extends Extension {
|
||||
const mode = firstExpose.features.find((f) => f.name === 'system_mode');
|
||||
if (mode) {
|
||||
if (mode.values.includes('sleep')) {
|
||||
// 'sleep' is not supported by homeassistent, but is valid according to ZCL
|
||||
// 'sleep' is not supported by Home Assistant, but is valid according to ZCL
|
||||
// TRV that support sleep (e.g. Viessmann) will have it removed from here,
|
||||
// this allows other expose consumers to still use it, e.g. the frontend.
|
||||
mode.values.splice(mode.values.indexOf('sleep'), 1);
|
||||
@@ -535,7 +535,7 @@ export default class HomeAssistant extends Extension {
|
||||
battery_low: {entity_category: 'diagnostic', device_class: 'battery'},
|
||||
button_lock: {entity_category: 'config', icon: 'mdi:lock'},
|
||||
calibration: {entity_category: 'config', icon: 'mdi:progress-wrench'},
|
||||
carbon_monoxide: {device_class: 'safety'},
|
||||
carbon_monoxide: {device_class: 'carbon_monoxide'},
|
||||
card: {entity_category: 'config', icon: 'mdi:clipboard-check'},
|
||||
child_lock: {entity_category: 'config', icon: 'mdi:account-lock'},
|
||||
color_sync: {entity_category: 'config', icon: 'mdi:sync-circle'},
|
||||
@@ -711,6 +711,7 @@ export default class HomeAssistant extends Extension {
|
||||
temperature_min: {entity_category: 'config', icon: 'mdi:thermometer-minus'},
|
||||
transition: {entity_category: 'config', icon: 'mdi:transition'},
|
||||
voc: {device_class: 'volatile_organic_compounds', state_class: 'measurement'},
|
||||
voc_index: {state_class: 'measurement'},
|
||||
vibration_timeout: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
voltage: {
|
||||
device_class: 'voltage',
|
||||
@@ -764,6 +765,12 @@ export default class HomeAssistant extends Extension {
|
||||
delete discoveryEntry.discovery_payload.device_class;
|
||||
}
|
||||
|
||||
// Home Assistant only supports µg/m³, not other units like ppb.
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/16057
|
||||
if (firstExpose.name === 'voc' && discoveryEntry.discovery_payload.unit_of_measurement !== 'µg/m³') {
|
||||
delete discoveryEntry.discovery_payload.device_class;
|
||||
}
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
|
||||
/**
|
||||
@@ -961,7 +968,7 @@ export default class HomeAssistant extends Extension {
|
||||
* Implements the MQTT device trigger (https://www.home-assistant.io/integrations/device_trigger.mqtt/)
|
||||
* The MQTT device trigger does not support JSON parsing, so it cannot listen to zigbee2mqtt/my_device
|
||||
* Whenever a device publish an {action: *} we discover an MQTT device trigger sensor
|
||||
* and republish it to zigbee2mqtt/my_devic/action
|
||||
* and republish it to zigbee2mqtt/my_device/action
|
||||
*/
|
||||
if (entity.isDevice() && entity.definition) {
|
||||
const keys = ['action', 'click'].filter((k) => data.message[k]);
|
||||
@@ -1435,7 +1442,7 @@ export default class HomeAssistant extends Extension {
|
||||
const identifierPostfix = entity.isGroup() ?
|
||||
`zigbee2mqtt_${this.getEncodedBaseTopic()}` : 'zigbee2mqtt';
|
||||
|
||||
// Allow device name to be overriden by homeassistant config
|
||||
// Allow device name to be overridden by homeassistant config
|
||||
let deviceName = entity.name;
|
||||
if (typeof entity.options.homeassistant?.name === 'string') {
|
||||
deviceName = entity.options.homeassistant.name;
|
||||
@@ -1470,7 +1477,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
});
|
||||
|
||||
// Copy hue -> h, saturation -> s to make homeassitant happy
|
||||
// Copy hue -> h, saturation -> s to make homeassistant happy
|
||||
if (message.hasOwnProperty('color')) {
|
||||
if (message.color.hasOwnProperty('hue')) {
|
||||
message.color.h = message.color.hue;
|
||||
|
||||
@@ -90,7 +90,7 @@ export default class Publish extends Extension {
|
||||
// When reporting is requested for a device (report: true in device-specific settings) we won't
|
||||
// ever issue a read here, as we assume the device will properly report changes.
|
||||
// Only do this when the retrieve_state option is enabled for this device.
|
||||
// retrieve_state == decprecated
|
||||
// retrieve_state == deprecated
|
||||
if (re instanceof Device && result && result.hasOwnProperty('readAfterWriteTime') &&
|
||||
re.options.retrieve_state
|
||||
) {
|
||||
@@ -102,7 +102,7 @@ export default class Publish extends Extension {
|
||||
/**
|
||||
* Home Assistant always publishes 'state', even when e.g. only setting
|
||||
* the color temperature. This would lead to 2 zigbee publishes, where the first one
|
||||
* (state) is probably unecessary.
|
||||
* (state) is probably unnecessary.
|
||||
*/
|
||||
if (settings.get().homeassistant) {
|
||||
const hasColorTemp = message.hasOwnProperty('color_temp');
|
||||
@@ -166,7 +166,7 @@ export default class Publish extends Extension {
|
||||
* Order state & brightness based on current bulb state
|
||||
*
|
||||
* Not all bulbs support setting the color/color_temp while it is off
|
||||
* this results in inconsistant behavior between different vendors.
|
||||
* this results in inconsistent behavior between different vendors.
|
||||
*
|
||||
* bulb on => move state & brightness to the back
|
||||
* bulb off => move state & brightness to the front
|
||||
@@ -220,7 +220,7 @@ export default class Publish extends Extension {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the endpoint_name name is a nubmer, try to map it to a friendlyName
|
||||
// If the endpoint_name name is a number, try to map it to a friendlyName
|
||||
if (!isNaN(Number(endpointName)) && re.isDevice() && utils.isEndpoint(localTarget) &&
|
||||
re.endpointName(localTarget)) {
|
||||
endpointName = re.endpointName(localTarget);
|
||||
|
||||
@@ -49,6 +49,13 @@ export default class Receive extends Extension {
|
||||
|
||||
// extend debounced payload with current
|
||||
this.debouncers[device.ieeeAddr].payload = {...this.debouncers[device.ieeeAddr].payload, ...payload};
|
||||
|
||||
// Update state cache right away. This makes sure that during debouncing cached state is always up to date.
|
||||
// ( Update right away as "lastSeenChanged" event might occur while debouncer is still active.
|
||||
// And if that happens it would cause old message to be published from cache.
|
||||
// By updating cache we make sure that state cache is always up-to-date.
|
||||
this.state.set(device, this.debouncers[device.ieeeAddr].payload);
|
||||
|
||||
this.debouncers[device.ieeeAddr].publish();
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@ import * as settings from './settings';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import fx from 'mkdir-recursive';
|
||||
import rimraf from 'rimraf';
|
||||
import {rimrafSync} from 'rimraf';
|
||||
import assert from 'assert';
|
||||
|
||||
const colorizer = winston.format.colorize();
|
||||
@@ -38,7 +38,7 @@ function init(): void {
|
||||
directory = settings.get().advanced.log_directory.replace('%TIMESTAMP%', timestamp);
|
||||
logFilename = settings.get().advanced.log_file.replace('%TIMESTAMP%', timestamp);
|
||||
|
||||
// Make sure that log directoy exsists when not logging to stdout only
|
||||
// Make sure that log directory exists when not logging to stdout only
|
||||
if (output.includes('file')) {
|
||||
fx.mkdirSync(directory);
|
||||
|
||||
@@ -144,7 +144,7 @@ function cleanup(): void {
|
||||
directories = directories.slice(10, directories.length);
|
||||
directories.forEach((dir) => {
|
||||
logger.debug(`Removing old log directory '${dir.path}'`);
|
||||
rimraf.sync(dir.path);
|
||||
rimrafSync(dir.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ function setLevel(level: Z2MLogLevel): void {
|
||||
}
|
||||
|
||||
function warn(message: string): void {
|
||||
// winston.config.syslog.levels doesnt have warn, but is required for syslog.
|
||||
// winston.config.syslog.levels doesn't have warn, but is required for syslog.
|
||||
logger.warning(message);
|
||||
}
|
||||
|
||||
|
||||
@@ -366,14 +366,15 @@
|
||||
"port": {
|
||||
"type": "number",
|
||||
"title": "Port",
|
||||
"description": "Frontend binding port",
|
||||
"description": "Frontend binding port. Ignored when using a unix domain socket",
|
||||
"default": 8080,
|
||||
"requiresRestart": true
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"title": "Bind host",
|
||||
"description": "Frontend binding host",
|
||||
"description": "Frontend binding host. Binds to a unix socket when an absolute path is given instead.",
|
||||
"examples": ["127.0.0.1", "/run/zigbee2mqtt/zigbee2mqtt.sock"],
|
||||
"default": "0.0.0.0",
|
||||
"requiresRestart": true
|
||||
},
|
||||
|
||||
@@ -211,7 +211,7 @@ function write(): void {
|
||||
// Read settings to check if we have to split devices/groups into separate file.
|
||||
const actual = yaml.read(file);
|
||||
|
||||
// In case the setting is defined in a separte file (e.g. !secret network_key) update it there.
|
||||
// In case the setting is defined in a separate file (e.g. !secret network_key) update it there.
|
||||
for (const path of [
|
||||
['mqtt', 'server'],
|
||||
['mqtt', 'user'],
|
||||
@@ -330,7 +330,7 @@ function read(): Settings {
|
||||
|
||||
// Read !secret MQTT username and password if set
|
||||
// eslint-disable-next-line
|
||||
const interpetValue = (value: any): any => {
|
||||
const interpretValue = (value: any): any => {
|
||||
const ref = parseValueRef(value);
|
||||
if (ref) {
|
||||
return yaml.read(data.joinPath(ref.filename))[ref.key];
|
||||
@@ -340,23 +340,23 @@ function read(): Settings {
|
||||
};
|
||||
|
||||
if (s.mqtt?.user) {
|
||||
s.mqtt.user = interpetValue(s.mqtt.user);
|
||||
s.mqtt.user = interpretValue(s.mqtt.user);
|
||||
}
|
||||
|
||||
if (s.mqtt?.password) {
|
||||
s.mqtt.password = interpetValue(s.mqtt.password);
|
||||
s.mqtt.password = interpretValue(s.mqtt.password);
|
||||
}
|
||||
|
||||
if (s.mqtt?.server) {
|
||||
s.mqtt.server = interpetValue(s.mqtt.server);
|
||||
s.mqtt.server = interpretValue(s.mqtt.server);
|
||||
}
|
||||
|
||||
if (s.advanced?.network_key) {
|
||||
s.advanced.network_key = interpetValue(s.advanced.network_key);
|
||||
s.advanced.network_key = interpretValue(s.advanced.network_key);
|
||||
}
|
||||
|
||||
if (s.frontend?.auth_token) {
|
||||
s.frontend.auth_token = interpetValue(s.frontend.auth_token);
|
||||
s.frontend.auth_token = interpretValue(s.frontend.auth_token);
|
||||
}
|
||||
|
||||
// Read devices/groups configuration from separate file if specified.
|
||||
@@ -472,7 +472,7 @@ export function apply(newSettings: Record<string, unknown>): boolean {
|
||||
throw new Error(`${error.instancePath.substring(1)} ${error.message}`);
|
||||
}
|
||||
|
||||
getInternalSettings(); // Ensure _settings is intialized.
|
||||
getInternalSettings(); // Ensure _settings is initialized.
|
||||
/* eslint-disable-line */ // @ts-ignore
|
||||
_settings = objectAssignDeep.noMutate(_settings, newSettings);
|
||||
write();
|
||||
|
||||
Generated
+1518
-1360
File diff suppressed because it is too large
Load Diff
+18
-18
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "1.30.2",
|
||||
"version": "1.30.4",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -8,7 +8,7 @@
|
||||
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14 || ^16 || ^18 || ^19"
|
||||
"node": "^14 || ^16 || ^18 || ^19 || ^20"
|
||||
},
|
||||
"keywords": [
|
||||
"xiaomi",
|
||||
@@ -51,40 +51,40 @@
|
||||
"moment": "^2.29.4",
|
||||
"mqtt": "4.3.7",
|
||||
"object-assign-deep": "^0.4.0",
|
||||
"rimraf": "^4.1.2",
|
||||
"semver": "^7.3.8",
|
||||
"rimraf": "^5.0.0",
|
||||
"semver": "^7.5.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"uri-js": "^4.4.1",
|
||||
"winston": "^3.8.2",
|
||||
"winston-syslog": "^2.7.0",
|
||||
"winston-transport": "^4.5.0",
|
||||
"ws": "^8.12.1",
|
||||
"zigbee-herdsman": "0.14.96",
|
||||
"zigbee-herdsman-converters": "15.0.58",
|
||||
"zigbee2mqtt-frontend": "0.6.125"
|
||||
"ws": "^8.13.0",
|
||||
"zigbee-herdsman": "0.14.111",
|
||||
"zigbee-herdsman-converters": "15.0.100",
|
||||
"zigbee2mqtt-frontend": "0.6.127"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.0",
|
||||
"@babel/core": "^7.21.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-typescript": "^7.21.0",
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"@babel/preset-typescript": "^7.21.5",
|
||||
"@types/debounce": "^1.2.1",
|
||||
"@types/finalhandler": "^1.2.0",
|
||||
"@types/humanize-duration": "^3.27.1",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/jest": "^29.5.1",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/object-assign-deep": "^0.4.0",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||
"@typescript-eslint/parser": "^5.53.0",
|
||||
"babel-jest": "^29.4.3",
|
||||
"eslint": "^8.34.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@typescript-eslint/parser": "^5.59.1",
|
||||
"babel-jest": "^29.5.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"jest": "^29.4.3",
|
||||
"jest": "^29.5.0",
|
||||
"tmp": "^0.2.1",
|
||||
"typescript": "^4.9.5"
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"jest": {
|
||||
"coverageThreshold": {
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@ const logger = require('./stub/logger');
|
||||
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const path = require('path');
|
||||
const rimraf = require('rimraf');
|
||||
const {rimrafSync} = require('rimraf');
|
||||
const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
const stringify = require('json-stable-stringify-without-jsonify');
|
||||
@@ -45,7 +45,7 @@ describe('User extensions', () => {
|
||||
});
|
||||
afterEach(() => {
|
||||
const extensionPath = path.join(data.mockDir, 'extension');
|
||||
rimraf.sync(extensionPath);
|
||||
rimrafSync(extensionPath);
|
||||
})
|
||||
|
||||
it('Load user extension', async () => {
|
||||
|
||||
@@ -128,6 +128,32 @@ describe('Frontend', () => {
|
||||
expect(mockWSClient.implementation.terminate).toHaveBeenCalledTimes(1);
|
||||
expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1);
|
||||
expect(mockWS.implementation.close).toHaveBeenCalledTimes(1);
|
||||
mockWS.implementation.close.mockClear();
|
||||
mockHTTP.implementation.close.mockClear();
|
||||
mockHTTP.implementation.listen.mockClear();
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
});
|
||||
|
||||
it('Start/stop unix socket', async () => {
|
||||
settings.set(['frontend'], {host: "/tmp/zigbee2mqtt.sock"});
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockNodeStatic.variables.path).toBe("my/dummy/path");
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith("/tmp/zigbee2mqtt.sock");
|
||||
const mockWSClient = {
|
||||
implementation: {
|
||||
terminate: jest.fn(),
|
||||
send: jest.fn(),
|
||||
},
|
||||
events: {},
|
||||
};
|
||||
mockWS.implementation.clients.push(mockWSClient.implementation);
|
||||
await controller.stop();
|
||||
expect(mockWSClient.implementation.terminate).toHaveBeenCalledTimes(1);
|
||||
expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1);
|
||||
expect(mockWS.implementation.close).toHaveBeenCalledTimes(1);
|
||||
mockWS.implementation.close.mockClear();
|
||||
mockHTTP.implementation.close.mockClear();
|
||||
mockHTTP.implementation.listen.mockClear();
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
});
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const data = require('./stub/data');
|
||||
let stdOutWriteOriginal;
|
||||
const rimraf = require('rimraf');
|
||||
const {rimrafSync} = require('rimraf');
|
||||
const Transport = require('winston-transport');
|
||||
|
||||
describe('Logger', () => {
|
||||
@@ -37,7 +37,7 @@ describe('Logger', () => {
|
||||
logger.logOutput();
|
||||
|
||||
for (const d of fs.readdirSync(dir.name)) {
|
||||
rimraf.sync(path.join(dir.name, d));
|
||||
rimrafSync(path.join(dir.name, d));
|
||||
}
|
||||
|
||||
for (let i = 0; i < 21; i++) {
|
||||
|
||||
@@ -178,6 +178,47 @@ describe('Receive', () => {
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03});
|
||||
});
|
||||
|
||||
it('Should NOT publish old messages from State cache during debouncing', async () => {
|
||||
|
||||
// Summary:
|
||||
// First send multiple measurements to device that is debouncing. Make sure only one message is sent out to MQTT. This also ensures first message is cached to "State".
|
||||
// Then send another measurement to that same device and trigger asyncronous event to push data from Cache. Newest value should be sent out.
|
||||
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1);
|
||||
await zigbeeHerdsman.events.message({data: {measuredValue: 8}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await zigbeeHerdsman.events.message( {data: {measuredValue: 1}, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10} );
|
||||
await zigbeeHerdsman.events.message( {data: {measuredValue: 2}, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10} );
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(50);
|
||||
// Test that measurements are combined(=debounced)
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
// Test that only one MQTT is sent out and test its values.
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
|
||||
// Send another Zigbee messages...
|
||||
await zigbeeHerdsman.events.message({data: {measuredValue: 9}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
const realDevice = controller.zigbee.resolveEntity(device);
|
||||
|
||||
// Trigger asyncronous event while device is "debouncing" to trigger Message to be sent out from State cache.
|
||||
await controller.publishEntityState( realDevice, {} );
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
// Total of 3 messages should have triggered.
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Test that message pushed by asyncronous message contains NEW measurement and not old.
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
// Test that messages after debouncing contains NEW measurement and not old.
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
});
|
||||
|
||||
it('Shouldnt republish old state', async () => {
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/3572
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
|
||||
Reference in New Issue
Block a user