feat!: The big cleanup (#24200)

* Remove `deviceGroupMembership`

* Fix controller.ts

* Remove `legacy_api` logic from non-legacy extensions.

* Fix network map coverage.

* Remove all `legacy` extensions.

* Remove `legacy_availability_payload`, `legacy_api`. Remove legacy from configure.

* Fix prettier.

* Remove some leftovers

* Renamed `updateAvailable` to `update_available`

* Remove emitPublishAvailability

* Remove `configuration.yaml`

* Switch to pnpm

* Remove direct mqtt-packet dependency.

* fix pretty

* fix `getDependencyVersion`

* fix pnpm publish

* fix(ignore): fix pnpm publish

* Remove deprecated bridge `config/*`

* Improve update script

* Improve update.sh

---------

Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
This commit is contained in:
Koen Kanters
2024-12-01 20:06:49 +01:00
co-authored by Nerivec
parent 3220b27c8a
commit b6186623f0
50 changed files with 6459 additions and 12214 deletions
+18 -13
View File
@@ -17,21 +17,24 @@ jobs:
token: ${{secrets.GH_TOKEN}}
- uses: actions/checkout@v4
if: ((github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push') == false
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org/
cache: npm
cache: pnpm
- name: Install dependencies
run: npm ci
run: pnpm i --frozen-lockfile
- name: Build
run: npm run build
run: pnpm run build
- name: Lint
run: |
npm run pretty:check
npm run eslint
pnpm run pretty:check
pnpm run eslint
- name: Test
run: npm run test-with-coverage
run: pnpm run test-with-coverage
- name: Docker login
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
run: echo ${{ secrets.DOCKER_KEY }} | docker login -u koenkk --password-stdin
@@ -74,7 +77,7 @@ jobs:
.
- name: 'release: Publish to npm'
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
run: npm publish
run: pnpm publish --no-git-checks
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN }}
- name: 'dev: Trigger zigbee2mqtt/hassio-zigbee2mqtt build'
@@ -139,15 +142,17 @@ jobs:
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
registry-url: https://registry.npmjs.org/
cache: 'npm'
node-version: 20
cache: pnpm
- name: Install dependencies
# --ignore-scripts prevents the serialport build which often fails on Windows
run: npm ci --ignore-scripts
run: pnpm i --frozen-lockfile --ignore-scripts
- name: Build
run: npm run build
run: pnpm run build
- name: Test
run: npm run test-with-coverage
run: pnpm run test-with-coverage
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
MASTER_FRONTEND_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee2mqtt-frontend"')
wget -q -O - https://raw.githubusercontent.com/Koenkk/zigbee2mqtt/release-please--branches--dev--components--zigbee2mqtt/CHANGELOG.md > z2m/CHANGELOG.md
cd z2m
npm ci
pnpm i --frozen-lockfile
node scripts/generateChangelog.js $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION $MASTER_FRONTEND_VERSION >> ../changelog.md
env:
GH_TOKEN: ${{secrets.GH_TOKEN}}
+5 -2
View File
@@ -17,11 +17,14 @@ jobs:
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }} --save-exact
cache: pnpm
- run: pnpm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }} --save-exact
- uses: peter-evans/create-pull-request@v7
id: cpr
with:
+8 -5
View File
@@ -18,14 +18,17 @@ jobs:
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
# connect-gzip-static@4.0.0 requires Node 20 >=
- run: npx npm-check-updates -u -x connect-gzip-static
- run: rm -f package-lock.json
- run: npm install
cache: pnpm
- run: |
pnpm up --latest
# connect-gzip-static@4.0.0 requires Node 20 >=
pnpm i connect-gzip-static@3.0.1
- uses: peter-evans/create-pull-request@v7
with:
commit-message: 'fix(ignore): update dependencies'
+1 -1
View File
@@ -1,2 +1,2 @@
package-lock.json
pnpm-lock.yaml
CHANGELOG.md
+1 -1
View File
@@ -4,5 +4,5 @@ Everybody is invited and welcome to contribute to Zigbee2MQTT. Zigbee2MQTT is wr
- Pull requests are always created against the [**dev**](https://github.com/Koenkk/zigbee2mqtt/tree/dev) branch.
- Easiest way to start developing Zigbee2MQTT is by setting up a development environment (aka bare-metal installation). You can follow this [guide](https://www.zigbee2mqtt.io/guide/installation/01_linux.html) to do this.
- You can run the tests locally by executing `npm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `npm run test-with-coverage`. The coverage report can be found under `coverage/lcov-report/index.html`. Linting is also enforced and can be run with `npm run eslint`.
- You can run the tests locally by executing `pnpm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `pnpm run test-with-coverage`. The coverage report can be found under `coverage/lcov-report/index.html`. Linting is also enforced and can be run with `pnpm run eslint`.
- When you want to add support for a new device no changes to Zigbee2MQTT have to be made, only to zigbee-herdsman-converters. You can find a guide for it [here](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
+3 -3
View File
@@ -104,9 +104,9 @@ Zigbee2MQTT is made up of three modules, each developed in its own Github projec
### 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.
In first time before building you need to run `npm install --include=dev`
Before submitting changes run `npm run test-with-coverage`, `npm run pretty:check` and `npm run eslint`
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 `pnpm run build`. For faster development instead of running `pnpm run build` you can run `pnpm run build-watch` in another terminal session, this will recompile as you change files.
In first time before building you need to run `pnpm install --include=dev`
Before submitting changes run `pnpm run test-with-coverage`, `pnpm run pretty:check` and `pnpm run eslint`
## Supported devices
-20
View File
@@ -1,20 +0,0 @@
# Home Assistant integration (MQTT discovery)
homeassistant: false
# allow new devices to join
permit_join: true
# MQTT settings
mqtt:
# MQTT base topic for zigbee2mqtt MQTT messages
base_topic: zigbee2mqtt
# MQTT server URL
server: 'mqtt://localhost'
# MQTT server authentication, uncomment if required:
# user: my_user
# password: my_password
# Serial settings
serial:
# Location of CC2531 USB sniffer
port: /dev/ttyACM0
+4 -4
View File
@@ -9,11 +9,11 @@ FROM base as dependencies_and_build
COPY package*.json tsconfig.json index.js ./
COPY lib ./lib
RUN apk add --no-cache --virtual .buildtools make gcc g++ python3 linux-headers git npm && \
npm ci --production --no-audit --no-optional --no-update-notifier && \
RUN apk add make gcc g++ python3 linux-headers git npm && \
npm install -g pnpm && \
pnpm install --frozen-lockfile --no-optional && \
# Serialport needs to be rebuild for Alpine https://serialport.io/docs/9.x.x/guide-installation#alpine-linux
npm rebuild --build-from-source && \
apk del .buildtools
pnpm rebuild
# Release
FROM base as release
+1 -1
View File
@@ -85,7 +85,7 @@ async function build(reason) {
env.NODE_OPTIONS = '--max_old_space_size=256';
}
exec('npm run build', {env, cwd: __dirname}, async (err, stdout, stderr) => {
exec('pnpm run build', {env, cwd: __dirname}, async (err, stdout, stderr) => {
if (err) {
process.stdout.write(', failed\n');
+2 -31
View File
@@ -19,10 +19,6 @@ import ExtensionExternalExtension from './extension/externalExtension';
import ExtensionFrontend from './extension/frontend';
import ExtensionGroups from './extension/groups';
import ExtensionHomeAssistant from './extension/homeassistant';
import ExtensionBridgeLegacy from './extension/legacy/bridgeLegacy';
import ExtensionDeviceGroupMembership from './extension/legacy/deviceGroupMembership';
import ExtensionReport from './extension/legacy/report';
import ExtensionSoftReset from './extension/legacy/softReset';
import ExtensionNetworkMap from './extension/networkMap';
import ExtensionOnEvent from './extension/onEvent';
import ExtensionOTAUpdate from './extension/otaUpdate';
@@ -41,15 +37,11 @@ const AllExtensions = [
ExtensionPublish,
ExtensionReceive,
ExtensionNetworkMap,
ExtensionSoftReset,
ExtensionHomeAssistant,
ExtensionConfigure,
ExtensionDeviceGroupMembership,
ExtensionBridgeLegacy,
ExtensionBridge,
ExtensionGroups,
ExtensionBind,
ExtensionReport,
ExtensionOnEvent,
ExtensionOTAUpdate,
ExtensionExternalConverters,
@@ -108,13 +100,11 @@ export class Controller {
new ExtensionBridge(...this.extensionArgs),
new ExtensionPublish(...this.extensionArgs),
new ExtensionReceive(...this.extensionArgs),
new ExtensionDeviceGroupMembership(...this.extensionArgs),
new ExtensionConfigure(...this.extensionArgs),
new ExtensionNetworkMap(...this.extensionArgs),
new ExtensionGroups(...this.extensionArgs),
new ExtensionBind(...this.extensionArgs),
new ExtensionOTAUpdate(...this.extensionArgs),
new ExtensionReport(...this.extensionArgs),
new ExtensionExternalExtension(...this.extensionArgs),
new ExtensionAvailability(...this.extensionArgs),
];
@@ -123,10 +113,6 @@ export class Controller {
this.extensions.push(new ExtensionFrontend(...this.extensionArgs));
}
if (settings.get().advanced.legacy_api) {
this.extensions.push(new ExtensionBridgeLegacy(...this.extensionArgs));
}
if (settings.get().external_converters.length) {
this.extensions.push(new ExtensionExternalConverters(...this.extensionArgs));
}
@@ -134,11 +120,6 @@ export class Controller {
if (settings.get().homeassistant) {
this.extensions.push(new ExtensionHomeAssistant(...this.extensionArgs));
}
/* istanbul ignore next */
if (settings.get().advanced.soft_reset_timeout !== 0) {
this.extensions.push(new ExtensionSoftReset(...this.extensionArgs));
}
}
async start(): Promise<void> {
@@ -156,9 +137,8 @@ export class Controller {
}
// Start zigbee
let startResult;
try {
startResult = await this.zigbee.start();
await this.zigbee.start();
this.eventBus.onAdapterDisconnected(this, this.onZigbeeAdapterDisconnected);
} catch (error) {
logger.error('Failed to start zigbee-herdsman');
@@ -172,15 +152,6 @@ export class Controller {
return await this.exit(1);
}
// Disable some legacy options on new network creation
if (startResult === 'reset') {
settings.set(['advanced', 'homeassistant_legacy_entity_attributes'], false);
settings.set(['advanced', 'legacy_api'], false);
settings.set(['advanced', 'legacy_availability_payload'], false);
settings.set(['device_options', 'legacy'], false);
await this.enableDisableExtension(false, 'BridgeLegacy');
}
// Log zigbee clients on startup
let deviceCount = 0;
@@ -356,7 +327,7 @@ export class Controller {
// Filter mqtt message attributes
utils.filterProperties(entity.options.filtered_attributes, message);
if (Object.entries(message).length) {
if (!utils.objectIsEmpty(message)) {
const output = settings.get().advanced.output;
if (output === 'attribute_and_json' || output === 'json') {
await this.mqtt.publish(entity.name, stringify(message), options);
-7
View File
@@ -56,13 +56,6 @@ export default class EventBus {
this.on('permitJoinChanged', callback, key);
}
public emitPublishAvailability(): void {
this.emitter.emit('publishAvailability');
}
public onPublishAvailability(key: ListenerKey, callback: () => void): void {
this.on('publishAvailability', callback, key);
}
public emitEntityRenamed(data: eventdata.EntityRenamed): void {
this.emitter.emit('deviceRenamed', data);
}
+1 -2
View File
@@ -140,7 +140,6 @@ export default class Availability extends Extension {
this.eventBus.onDeviceLeave(this, (data) => clearTimeout(this.timers[data.ieeeAddr]));
this.eventBus.onDeviceAnnounce(this, (data) => this.retrieveState(data.device));
this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged);
this.eventBus.onPublishAvailability(this, this.publishAvailabilityForAllEntities);
this.eventBus.onGroupMembersChanged(this, (data) => this.publishAvailability(data.group, false));
// Publish initial availability
await this.publishAvailabilityForAllEntities();
@@ -189,7 +188,7 @@ export default class Availability extends Extension {
}
const topic = `${entity.name}/availability`;
const payload = utils.availabilityPayload(available ? 'online' : 'offline', settings.get());
const payload = JSON.stringify({state: available ? 'online' : 'offline'});
this.availabilityCache[entity.ID] = available;
await this.mqtt.publish(topic, payload, {retain: true, qos: 1});
+3 -35
View File
@@ -14,8 +14,6 @@ import * as settings from '../util/settings';
import utils from '../util/utils';
import Extension from './extension';
const LEGACY_API = settings.get().advanced.legacy_api;
const LEGACY_TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`);
const TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`);
const ALL_CLUSTER_CANDIDATES: readonly ClusterName[] = [
'genScenes',
@@ -226,12 +224,7 @@ export default class Bind extends Extension {
let clusters: ParsedMQTTMessage['clusters'] | undefined;
let skipDisableReporting: ParsedMQTTMessage['skipDisableReporting'] = false;
if (LEGACY_API && data.topic.match(LEGACY_TOPIC_REGEX)) {
const topic = data.topic.replace(`${settings.get().mqtt.base_topic}/bridge/`, '');
type = topic.split('/')[0] as ParsedMQTTMessage['type'];
sourceKey = topic.replace(`${type}/`, '');
targetKey = data.message;
} else if (data.topic.match(TOPIC_REGEX)) {
if (data.topic.match(TOPIC_REGEX)) {
type = data.topic.endsWith('unbind') ? 'unbind' : 'bind';
const message: DataMessage = JSON.parse(data.message);
sourceKey = message.from;
@@ -312,25 +305,9 @@ export default class Bind extends Extension {
logger.info(
`Successfully ${type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from '${source.name}' to '${target.name}'`,
);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
await this.mqtt.publish(
'bridge/log',
stringify({type: `device_${type}`, message: {from: source.name, to: target.name, cluster}}),
);
}
} catch (error) {
failedClusters.push(cluster);
logger.error(`Failed to ${type} cluster '${cluster}' from '${source.name}' to '${target.name}' (${error})`);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
await this.mqtt.publish(
'bridge/log',
stringify({type: `device_${type}_failed`, message: {from: source.name, to: target.name, cluster}}),
);
}
}
}
}
@@ -338,11 +315,6 @@ export default class Bind extends Extension {
if (attemptedClusters.length === 0) {
logger.error(`Nothing to ${type} from '${source.name}' to '${target.name}'`);
error = `Nothing to ${type}`;
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
await this.mqtt.publish('bridge/log', stringify({type: `device_${type}_failed`, message: {from: source.name, to: target.name}}));
}
} else if (failedClusters.length === attemptedClusters.length) {
error = `Failed to ${type}`;
}
@@ -359,13 +331,9 @@ export default class Bind extends Extension {
}
}
const triggeredViaLegacyApi = data.topic.match(LEGACY_TOPIC_REGEX);
const response = utils.getResponse(message, responseData, error);
if (!triggeredViaLegacyApi) {
const response = utils.getResponse(message, responseData, error);
await this.mqtt.publish(`bridge/response/device/${type}`, stringify(response));
}
await this.mqtt.publish(`bridge/response/device/${type}`, stringify(response));
if (error) {
logger.error(error);
-69
View File
@@ -70,11 +70,6 @@ export default class Bridge extends Extension {
health_check: this.healthCheck,
coordinator_check: this.coordinatorCheck,
options: this.bridgeOptions,
// Below are deprecated
'config/last_seen': this.configLastSeen,
'config/homeassistant': this.configHomeAssistant,
'config/elapsed': this.configElapsed,
'config/log_level': this.configLogLevel,
};
const debugToMQTTFrontend = settings.get().advanced.log_debug_to_mqtt_frontend;
@@ -374,58 +369,6 @@ export default class Bridge extends Extension {
return utils.getResponse(message, response);
}
// Deprecated
@bind async configLastSeen(message: KeyValue | string): Promise<MQTTResponse> {
const allowed = ['disable', 'ISO_8601', 'epoch', 'ISO_8601_local'];
const value = this.getValue(message);
if (typeof value !== 'string' || !allowed.includes(value)) {
throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`);
}
settings.set(['advanced', 'last_seen'], value);
await this.publishInfo();
return utils.getResponse(message, {value});
}
// Deprecated
@bind async configHomeAssistant(message: string | KeyValue): Promise<MQTTResponse> {
const allowed = [true, false];
const value = this.getValue(message);
if (typeof value !== 'boolean' || !allowed.includes(value)) {
throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`);
}
settings.set(['homeassistant'], value);
await this.enableDisableExtension(value, 'HomeAssistant');
await this.publishInfo();
return utils.getResponse(message, {value});
}
// Deprecated
@bind async configElapsed(message: KeyValue | string): Promise<MQTTResponse> {
const allowed = [true, false];
const value = this.getValue(message);
if (typeof value !== 'boolean' || !allowed.includes(value)) {
throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`);
}
settings.set(['advanced', 'elapsed'], value);
await this.publishInfo();
return utils.getResponse(message, {value});
}
// Deprecated
@bind async configLogLevel(message: KeyValue | string): Promise<MQTTResponse> {
const value = this.getValue(message) as settings.LogLevel;
if (typeof value !== 'string' || !settings.LOG_LEVELS.includes(value)) {
throw new Error(`'${value}' is not an allowed value, allowed: ${settings.LOG_LEVELS}`);
}
logger.setLevel(value);
await this.publishInfo();
return utils.getResponse(message, {value});
}
@bind async touchlinkIdentify(message: KeyValue | string): Promise<MQTTResponse> {
if (typeof message !== 'object' || message.ieee_address === undefined || message.channel === undefined) {
throw new Error('Invalid payload');
@@ -472,18 +415,6 @@ export default class Bridge extends Extension {
* Utils
*/
getValue(message: KeyValue | string): string | boolean | number {
if (typeof message === 'object') {
if (message.value === undefined) {
throw new Error('No value given');
}
return message.value;
} else {
return message;
}
}
async changeEntityOptions(entityType: 'device' | 'group', message: KeyValue | string): Promise<MQTTResponse> {
if (typeof message !== 'object' || message.id === undefined || message.options === undefined) {
throw new Error(`Invalid payload`);
+1 -15
View File
@@ -16,7 +16,6 @@ export default class Configure extends Extension {
private configuring = new Set();
private attempts: {[s: string]: number} = {};
private topic = `${settings.get().mqtt.base_topic}/bridge/request/device/configure`;
private legacyTopic = `${settings.get().mqtt.base_topic}/bridge/configure`;
@bind private async onReconfigure(data: eventdata.Reconfigure): Promise<void> {
// Disabling reporting unbinds some cluster which could be bound by configure, re-setup.
@@ -29,20 +28,7 @@ export default class Configure extends Extension {
}
@bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
if (data.topic === this.legacyTopic) {
const device = this.zigbee.resolveEntity(data.message);
if (!device || !(device instanceof Device)) {
logger.error(`Device '${data.message}' does not exist`);
return;
}
if (!device.definition || !device.definition.configure) {
logger.warning(`Skipping configure of '${device.name}', device does not require this.`);
return;
}
await this.configure(device, 'mqtt_message', true);
} else if (data.topic === this.topic) {
if (data.topic === this.topic) {
const message = utils.parseJSON(data.message, data.message);
const ID = typeof message === 'object' && message.id !== undefined ? message.id : message;
let error: string | undefined;
+8 -93
View File
@@ -14,8 +14,6 @@ import utils, {isLightExpose} from '../util/utils';
import Extension from './extension';
const TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/group/members/(remove|add|remove_all)$`);
const LEGACY_TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(.+)/(remove|add|remove_all)$`);
const LEGACY_TOPIC_REGEX_REMOVE_ALL = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`);
const STATE_PROPERTIES: Readonly<Record<string, (value: string, exposes: zhc.Expose[]) => boolean>> = {
state: () => true,
@@ -37,13 +35,11 @@ interface ParsedMQTTMessage {
error?: string;
groupKey?: string;
deviceKey?: string;
triggeredViaLegacyApi: boolean;
skipDisableReporting: boolean;
resolvedEntityEndpoint?: zh.Endpoint;
}
export default class Groups extends Extension {
private legacyApi = settings.get().advanced.legacy_api;
private lastOptimisticState: {[s: string]: KeyValue} = {};
override async start(): Promise<void> {
@@ -260,60 +256,12 @@ export default class Groups extends Extension {
let error: ParsedMQTTMessage['error'] | undefined;
let groupKey: ParsedMQTTMessage['groupKey'] | undefined;
let deviceKey: ParsedMQTTMessage['deviceKey'] | undefined;
let triggeredViaLegacyApi: ParsedMQTTMessage['triggeredViaLegacyApi'] = false;
let skipDisableReporting: ParsedMQTTMessage['skipDisableReporting'] = false;
/* istanbul ignore else */
const topicRegexMatch = data.topic.match(TOPIC_REGEX);
const legacyTopicRegexRemoveAllMatch = data.topic.match(LEGACY_TOPIC_REGEX_REMOVE_ALL);
const legacyTopicRegexMatch = data.topic.match(LEGACY_TOPIC_REGEX);
if (this.legacyApi && (legacyTopicRegexMatch || legacyTopicRegexRemoveAllMatch)) {
triggeredViaLegacyApi = true;
if (legacyTopicRegexMatch) {
resolvedEntityGroup = this.zigbee.resolveEntity(legacyTopicRegexMatch[1]) as Group;
type = legacyTopicRegexMatch[2] as ParsedMQTTMessage['type'];
if (!resolvedEntityGroup || !(resolvedEntityGroup instanceof Group)) {
logger.error(`Group '${legacyTopicRegexMatch[1]}' does not exist`);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const message = {friendly_name: data.message, group: legacyTopicRegexMatch[1], error: `group doesn't exists`};
await this.mqtt.publish('bridge/log', stringify({type: `device_group_${type}_failed`, message}));
}
return undefined;
}
} else {
type = 'remove_all';
}
const parsedEntity = this.zigbee.resolveEntityAndEndpoint(data.message);
resolvedEntityDevice = parsedEntity.entity as Device;
if (!resolvedEntityDevice || !(resolvedEntityDevice instanceof Device)) {
logger.error(`Device '${data.message}' does not exist`);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const message = {friendly_name: data.message, group: legacyTopicRegexMatch![1], error: "entity doesn't exists"};
await this.mqtt.publish('bridge/log', stringify({type: `device_group_${type}_failed`, message}));
}
return undefined;
}
resolvedEntityEndpoint = parsedEntity.endpoint;
if (parsedEntity.endpointID && !resolvedEntityEndpoint) {
logger.error(`Device '${parsedEntity.ID}' does not have endpoint '${parsedEntity.endpointID}'`);
return undefined;
}
} else if (topicRegexMatch) {
if (topicRegexMatch) {
type = topicRegexMatch[1] as 'remove' | 'add' | 'remove_all';
const message = JSON.parse(data.message);
deviceKey = message.device;
@@ -353,7 +301,6 @@ export default class Groups extends Extension {
error,
groupKey,
deviceKey,
triggeredViaLegacyApi,
skipDisableReporting,
resolvedEntityEndpoint,
};
@@ -366,16 +313,7 @@ export default class Groups extends Extension {
return;
}
const {
resolvedEntityGroup,
resolvedEntityDevice,
type,
triggeredViaLegacyApi,
groupKey,
deviceKey,
skipDisableReporting,
resolvedEntityEndpoint,
} = parsed;
const {resolvedEntityGroup, resolvedEntityDevice, type, groupKey, deviceKey, skipDisableReporting, resolvedEntityEndpoint} = parsed;
let error = parsed.error;
const changedGroups: Group[] = [];
@@ -404,26 +342,12 @@ export default class Groups extends Extension {
await resolvedEntityEndpoint.addToGroup(resolvedEntityGroup.zh);
settings.addDeviceToGroup(resolvedEntityGroup.ID.toString(), keys);
changedGroups.push(resolvedEntityGroup);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const message = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name};
await this.mqtt.publish('bridge/log', stringify({type: `device_group_add`, message}));
}
} else if (type === 'remove') {
assert(resolvedEntityGroup, '`resolvedEntityGroup` is missing');
logger.info(`Removing '${resolvedEntityDevice.name}' from '${resolvedEntityGroup.name}'`);
await resolvedEntityEndpoint.removeFromGroup(resolvedEntityGroup.zh);
settings.removeDeviceFromGroup(resolvedEntityGroup.ID.toString(), keys);
changedGroups.push(resolvedEntityGroup);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const message = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name};
await this.mqtt.publish('bridge/log', stringify({type: `device_group_remove`, message}));
}
} else {
// remove_all
logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`);
@@ -436,13 +360,6 @@ export default class Groups extends Extension {
for (const settingsGroup of settings.getGroups()) {
settings.removeDeviceFromGroup(settingsGroup.ID.toString(), keys);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const message = {friendly_name: resolvedEntityDevice.name};
await this.mqtt.publish('bridge/log', stringify({type: `device_group_remove_all`, message}));
}
}
}
} catch (e) {
@@ -451,17 +368,15 @@ export default class Groups extends Extension {
}
}
if (!triggeredViaLegacyApi) {
const message = utils.parseJSON(data.message, data.message);
const responseData: KeyValue = {device: deviceKey};
const message = utils.parseJSON(data.message, data.message);
const responseData: KeyValue = {device: deviceKey};
if (groupKey) {
responseData.group = groupKey;
}
await this.mqtt.publish(`bridge/response/group/members/${type}`, stringify(utils.getResponse(message, responseData, error)));
if (groupKey) {
responseData.group = groupKey;
}
await this.mqtt.publish(`bridge/response/group/members/${type}`, stringify(utils.getResponse(message, responseData, error)));
if (error) {
logger.error(error);
} else {
+2 -6
View File
@@ -542,9 +542,6 @@ export default class HomeAssistant extends Extension {
await this.discover(e);
}
}, utils.seconds(discoverWait));
// Send availability messages, this is required if the legacy_availability_payload option has been changed.
this.eventBus.emitPublishAvailability();
}
private getDiscovered(entity: Device | Group | Bridge | string | number): Discovered {
@@ -1693,7 +1690,7 @@ export default class HomeAssistant extends Extension {
if (isDevice && entity.options.disabled) {
// Mark disabled device always as unavailable
payload.availability.forEach((a: KeyValue) => (a.value_template = '{{ "offline" }}'));
} else if (!settings.get().advanced.legacy_availability_payload) {
} else {
payload.availability.forEach((a: KeyValue) => (a.value_template = '{{ value_json.state }}'));
}
} else {
@@ -2069,7 +2066,6 @@ export default class HomeAssistant extends Extension {
const discovery: DiscoveryEntry[] = [];
const bridge = new Bridge(coordinatorIeeeAddress, coordinatorVersion, discovery);
const baseTopic = `${settings.get().mqtt.base_topic}/${bridge.name}`;
const legacyAvailability = settings.get().advanced.legacy_availability_payload;
discovery.push(
// Binary sensors.
@@ -2083,7 +2079,7 @@ export default class HomeAssistant extends Extension {
entity_category: 'diagnostic',
state_topic: true,
state_topic_postfix: 'state',
value_template: !legacyAvailability ? '{{ value_json.state }}' : '{{ value }}',
value_template: '{{ value_json.state }}',
payload_on: 'online',
payload_off: 'offline',
availability: false,
-424
View File
@@ -1,424 +0,0 @@
import assert from 'assert';
import bind from 'bind-decorator';
import stringify from 'json-stable-stringify-without-jsonify';
import logger from '../../util/logger';
import * as settings from '../../util/settings';
import utils from '../../util/utils';
import Extension from '../extension';
const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+/factory_reset)|(?:\\w+))`);
export default class BridgeLegacy extends Extension {
private lastJoinedDeviceName?: string;
// @ts-expect-error initialized in `start`
private supportedOptions: {[s: string]: (topic: string, message: string) => Promise<void> | void};
override async start(): Promise<void> {
this.supportedOptions = {
permit_join: this.permitJoin,
last_seen: this.lastSeen,
elapsed: this.elapsed,
reset: this.reset,
log_level: this.logLevel,
devices: this.devices,
groups: this.groups,
'devices/get': this.devices,
rename: this.rename,
rename_last: this.renameLast,
remove: this.remove,
force_remove: this.forceRemove,
ban: this.ban,
device_options: this.deviceOptions,
add_group: this.addGroup,
remove_group: this.removeGroup,
force_remove_group: this.removeGroup,
whitelist: this.whitelist,
'touchlink/factory_reset': this.touchlinkFactoryReset,
};
this.eventBus.onDeviceJoined(this, (data) => this.onZigbeeEvent_('deviceJoined', data, data.device));
this.eventBus.onDeviceInterview(this, (data) => this.onZigbeeEvent_('deviceInterview', data, data.device));
this.eventBus.onDeviceAnnounce(this, (data) => this.onZigbeeEvent_('deviceAnnounce', data, data.device));
this.eventBus.onDeviceLeave(this, (data) => this.onZigbeeEvent_('deviceLeave', data, undefined));
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
await this.publish();
}
@bind async whitelist(topic: string, message: string): Promise<void> {
try {
const entity = settings.getDevice(message);
assert(entity, `Entity '${message}' does not exist`);
settings.addDeviceToPasslist(entity.ID.toString());
logger.info(`Whitelisted '${entity.friendly_name}'`);
await this.mqtt.publish('bridge/log', stringify({type: 'device_whitelisted', message: {friendly_name: entity.friendly_name}}));
} catch (error) {
logger.error(`Failed to whitelist '${message}' '${error}'`);
}
}
@bind deviceOptions(topic: string, message: string): void {
let json = null;
try {
json = JSON.parse(message);
} catch {
logger.error('Failed to parse message as JSON');
return;
}
if (json.friendly_name === undefined || json.options === undefined) {
logger.error('Invalid JSON message, should contain "friendly_name" and "options"');
return;
}
const entity = settings.getDevice(json.friendly_name);
assert(entity, `Entity '${json.friendly_name}' does not exist`);
settings.changeEntityOptions(entity.ID.toString(), json.options);
logger.info(`Changed device specific options of '${json.friendly_name}' (${stringify(json.options)})`);
}
@bind async permitJoin(topic: string, message: string): Promise<void> {
await this.zigbee.permitJoin(message.toLowerCase() === 'true');
await this.publish();
}
@bind async reset(): Promise<void> {
try {
await this.zigbee.reset('soft');
logger.info('Soft reset ZNP');
} catch {
logger.error('Soft reset failed');
}
}
@bind lastSeen(topic: string, message: string): void {
const allowed = ['disable', 'ISO_8601', 'epoch', 'ISO_8601_local'];
if (!allowed.includes(message)) {
logger.error(`${message} is not an allowed value, possible: ${allowed}`);
return;
}
settings.set(['advanced', 'last_seen'], message);
logger.info(`Set last_seen to ${message}`);
}
@bind elapsed(topic: string, message: string): void {
const allowed = ['true', 'false'];
if (!allowed.includes(message)) {
logger.error(`${message} is not an allowed value, possible: ${allowed}`);
return;
}
settings.set(['advanced', 'elapsed'], message === 'true');
logger.info(`Set elapsed to ${message}`);
}
@bind async logLevel(topic: string, message: string): Promise<void> {
const level = message.toLowerCase() as settings.LogLevel;
if (settings.LOG_LEVELS.includes(level)) {
logger.info(`Switching log level to '${level}'`);
logger.setLevel(level);
} else {
logger.error(`Could not set log level to '${level}'. Allowed level: '${settings.LOG_LEVELS.join(',')}'`);
}
await this.publish();
}
@bind async devices(topic: string): Promise<void> {
const coordinator = await this.zigbee.getCoordinatorVersion();
const devices: KeyValue[] = [];
for (const device of this.zigbee.devicesIterator()) {
const payload: KeyValue = {
ieeeAddr: device.ieeeAddr,
type: device.zh.type,
networkAddress: device.zh.networkAddress,
};
if (device.zh.type !== 'Coordinator') {
const definition = device.definition;
payload.model = definition ? definition.model : device.zh.modelID;
payload.vendor = definition ? definition.vendor : '-';
payload.description = definition ? definition.description : '-';
payload.friendly_name = device.name;
payload.manufacturerID = device.zh.manufacturerID;
payload.manufacturerName = device.zh.manufacturerName;
payload.powerSource = device.zh.powerSource;
payload.modelID = device.zh.modelID;
payload.hardwareVersion = device.zh.hardwareVersion;
payload.softwareBuildID = device.zh.softwareBuildID;
payload.dateCode = device.zh.dateCode;
payload.lastSeen = device.zh.lastSeen;
} else {
payload.friendly_name = 'Coordinator';
payload.softwareBuildID = coordinator.type;
payload.dateCode = coordinator.meta.revision.toString();
payload.lastSeen = Date.now();
}
devices.push(payload);
}
if (topic.split('/').pop() == 'get') {
await this.mqtt.publish(`bridge/config/devices`, stringify(devices), {}, settings.get().mqtt.base_topic, false, false);
} else {
await this.mqtt.publish('bridge/log', stringify({type: 'devices', message: devices}));
}
}
@bind async groups(): Promise<void> {
const payload = settings.getGroups().map((g) => {
return {...g, ID: Number(g.ID)};
});
await this.mqtt.publish('bridge/log', stringify({type: 'groups', message: payload}));
}
@bind async rename(topic: string, message: string): Promise<void> {
const invalid = `Invalid rename message format expected {"old": "friendly_name", "new": "new_name"} got ${message}`;
let json = null;
try {
json = JSON.parse(message);
} catch {
logger.error(invalid);
return;
}
// Validate message
if (!json.new || !json.old) {
logger.error(invalid);
return;
}
await this._renameInternal(json.old, json.new);
}
@bind async renameLast(topic: string, message: string): Promise<void> {
if (!this.lastJoinedDeviceName) {
logger.error(`Cannot rename last joined device, no device has joined during this session`);
return;
}
await this._renameInternal(this.lastJoinedDeviceName, message);
}
async _renameInternal(from: string, to: string): Promise<void> {
try {
const isGroup = settings.getGroup(from) != undefined;
settings.changeFriendlyName(from, to);
logger.info(`Successfully renamed - ${from} to ${to} `);
const entity = this.zigbee.resolveEntity(to);
if (entity?.isDevice()) {
this.eventBus.emitEntityRenamed({homeAssisantRename: false, from, to, entity});
}
await this.mqtt.publish('bridge/log', stringify({type: `${isGroup ? 'group' : 'device'}_renamed`, message: {from, to}}));
} catch {
logger.error(`Failed to rename - ${from} to ${to}`);
}
}
@bind async addGroup(topic: string, message: string): Promise<void> {
let id = null;
let name = null;
try {
// json payload with id and friendly_name
const json = JSON.parse(message);
if (json.id !== undefined) {
id = json.id;
name = `group_${id}`;
}
if (json.friendly_name !== undefined) {
name = json.friendly_name;
}
} catch {
// just friendly_name
name = message;
}
if (name == null) {
logger.error('Failed to add group, missing friendly_name!');
return;
}
const group = settings.addGroup(name, id);
this.zigbee.createGroup(group.ID);
await this.mqtt.publish('bridge/log', stringify({type: `group_added`, message: name}));
logger.info(`Added group '${name}'`);
}
@bind async removeGroup(topic: string, message: string): Promise<void> {
const name = message;
const entity = this.zigbee.resolveEntity(message) as Group;
assert(entity && entity.isGroup(), `Group '${message}' does not exist`);
if (topic.includes('force')) {
entity.zh.removeFromDatabase();
} else {
await entity.zh.removeFromNetwork();
}
settings.removeGroup(message);
await this.mqtt.publish('bridge/log', stringify({type: `group_removed`, message}));
logger.info(`Removed group '${name}'`);
}
@bind async forceRemove(topic: string, message: string): Promise<void> {
await this.removeForceRemoveOrBan('force_remove', message);
}
@bind async remove(topic: string, message: string): Promise<void> {
await this.removeForceRemoveOrBan('remove', message);
}
@bind async ban(topic: string, message: string): Promise<void> {
await this.removeForceRemoveOrBan('ban', message);
}
@bind async removeForceRemoveOrBan(action: string, message: string): Promise<void> {
const entity = this.zigbee.resolveEntity(message.trim()) as Device;
const lookup: KeyValue = {
ban: ['banned', 'Banning', 'ban'],
force_remove: ['force_removed', 'Force removing', 'force remove'],
remove: ['removed', 'Removing', 'remove'],
};
if (!entity) {
logger.error(`Cannot ${lookup[action][2]}, device '${message}' does not exist`);
await this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}_failed`, message}));
return;
}
const ieeeAddr = entity.ieeeAddr;
const name = entity.name;
const cleanup = async (): Promise<void> => {
// Fire event
this.eventBus.emitEntityRemoved({id: ieeeAddr, name: name, type: 'device'});
// Remove from configuration.yaml
settings.removeDevice(entity.ieeeAddr);
// Remove from state
this.state.remove(ieeeAddr);
logger.info(`Successfully ${lookup[action][0]} ${entity.name}`);
await this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}`, message}));
};
try {
logger.info(`${lookup[action][1]} '${entity.name}'`);
if (action === 'force_remove') {
entity.zh.removeFromDatabase();
} else {
await entity.zh.removeFromNetwork();
}
await cleanup();
} catch (error) {
logger.error(`Failed to ${lookup[action][2]} ${entity.name} (${error})`);
logger.error(`See https://www.zigbee2mqtt.io/guide/usage/mqtt_topics_and_messages.html#zigbee2mqtt-bridge-request for more info`);
await this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}_failed`, message}));
}
if (action === 'ban') {
settings.blockDevice(ieeeAddr);
}
}
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
const {topic, message} = data;
const match = topic.match(configRegex);
if (!match) {
return;
}
const option = match[1];
if (this.supportedOptions[option] === undefined) {
return;
}
await this.supportedOptions[option](topic, message);
return;
}
async publish(): Promise<void> {
const info = await utils.getZigbee2MQTTVersion();
const coordinator = await this.zigbee.getCoordinatorVersion();
const topic = `bridge/config`;
const payload = {
version: info.version,
commit: info.commitHash,
coordinator,
network: await this.zigbee.getNetworkParameters(),
log_level: logger.getLevel(),
permit_join: this.zigbee.getPermitJoin(),
};
await this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0});
}
async onZigbeeEvent_(type: string, data: KeyValue, resolvedEntity: Device | undefined): Promise<void> {
if (resolvedEntity) {
/* istanbul ignore else */
if (type === 'deviceJoined') {
this.lastJoinedDeviceName = resolvedEntity.name;
await this.mqtt.publish('bridge/log', stringify({type: `device_connected`, message: {friendly_name: resolvedEntity.name}}));
} else if (type === 'deviceInterview') {
if (data.status === 'successful') {
if (resolvedEntity.isSupported) {
const {vendor, description, model} = resolvedEntity.definition!; // checked by `isSupported`
const log = {friendly_name: resolvedEntity.name, model, vendor, description, supported: true};
await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_successful', meta: log}));
} else {
const meta = {friendly_name: resolvedEntity.name, supported: false};
await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_successful', meta}));
}
} else if (data.status === 'failed') {
const meta = {friendly_name: resolvedEntity.name};
await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_failed', meta}));
} else {
/* istanbul ignore else */
if (data.status === 'started') {
const meta = {friendly_name: resolvedEntity.name};
await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_started', meta}));
}
}
} else if (type === 'deviceAnnounce') {
const meta = {friendly_name: resolvedEntity.name};
await this.mqtt.publish('bridge/log', stringify({type: `device_announced`, message: 'announce', meta}));
}
} else {
/* istanbul ignore else */
if (type === 'deviceLeave') {
const name = data.ieeeAddr;
const meta = {friendly_name: name};
await this.mqtt.publish('bridge/log', stringify({type: `device_removed`, message: 'left_network', meta}));
}
}
}
@bind async touchlinkFactoryReset(): Promise<void> {
logger.info('Starting touchlink factory reset...');
await this.mqtt.publish('bridge/log', stringify({type: `touchlink`, message: 'reset_started', meta: {status: 'started'}}));
const result = await this.zigbee.touchlinkFactoryResetFirst();
if (result) {
logger.info('Successfully factory reset device through Touchlink');
await this.mqtt.publish('bridge/log', stringify({type: `touchlink`, message: 'reset_success', meta: {status: 'success'}}));
} else {
logger.warning('Failed to factory reset device through Touchlink');
await this.mqtt.publish('bridge/log', stringify({type: `touchlink`, message: 'reset_failed', meta: {status: 'failed'}}));
}
}
}
@@ -1,67 +0,0 @@
/* istanbul ignore file */
import assert from 'assert';
import bind from 'bind-decorator';
import Device from '../../model/device';
import logger from '../../util/logger';
import * as settings from '../../util/settings';
import Extension from '../extension';
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/device/(.+)/get_group_membership$`);
export default class DeviceGroupMembership extends Extension {
override async start(): Promise<void> {
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
}
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
const match = data.topic.match(topicRegex);
if (!match) {
return;
}
const parsed = this.zigbee.resolveEntityAndEndpoint(match[1]);
const device = parsed?.entity as Device;
if (!device || !(device instanceof Device)) {
logger.error(`Device '${match[1]}' does not exist`);
return;
}
const endpoint = parsed.endpoint;
if (parsed.endpointID && !endpoint) {
logger.error(`Device '${parsed.ID}' does not have endpoint '${parsed.endpointID}'`);
return;
}
assert(endpoint !== undefined);
const response = await endpoint.command(`genGroups`, 'getMembership', {groupcount: 0, grouplist: []}, {});
if (!response) {
logger.warning(`Couldn't get group membership of ${device.ieeeAddr}`);
return;
}
let {grouplist} = response;
grouplist = grouplist.map((gid: string) => {
const g = settings.getGroup(gid);
return g ? g.friendly_name : gid;
});
const msgGroupList = `${device.ieeeAddr} is in groups [${grouplist}]`;
let msgCapacity;
if (response.capacity === 254) {
msgCapacity = 'it can be a part of at least 1 more group';
} else {
msgCapacity = `its remaining group capacity is ${response.capacity === 255 ? 'unknown' : response.capacity}`;
}
logger.info(`${msgGroupList} and ${msgCapacity}`);
await this.publishEntityState(device, {group_list: grouplist, group_capacity: response.capacity});
}
}
-204
View File
@@ -1,204 +0,0 @@
import * as zhc from 'zigbee-herdsman-converters';
import logger from '../../util/logger';
import * as settings from '../../util/settings';
import utils from '../../util/utils';
import Extension from '../extension';
const defaultConfiguration = {
minimumReportInterval: 3,
maximumReportInterval: 300,
reportableChange: 1,
};
const ZNLDP12LM = zhc.definitions.find((d) => d.model === 'ZNLDP12LM');
const devicesNotSupportingReporting = [
zhc.definitions.find((d) => d.model === 'CC2530.ROUTER'),
zhc.definitions.find((d) => d.model === 'BASICZBR3'),
zhc.definitions.find((d) => d.model === 'ZM-CSW032-D'),
zhc.definitions.find((d) => d.model === 'TS0001'),
zhc.definitions.find((d) => d.model === 'TS0115'),
];
const reportKey = 1;
const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemperature: boolean; colorXY: boolean}> => {
if (endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') === undefined) {
await endpoint.read('lightingColorCtrl', ['colorCapabilities']);
}
const value = endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') as number;
return {
colorTemperature: (value & (1 << 4)) > 0,
colorXY: (value & (1 << 3)) > 0,
};
};
const clusters: {
[s: string]: {
attribute: string;
minimumReportInterval: number;
maximumReportInterval: number;
reportableChange: number;
condition?: (endpoint: zh.Endpoint) => Promise<boolean>;
}[];
} = {
genOnOff: [{attribute: 'onOff', ...defaultConfiguration, minimumReportInterval: 0, reportableChange: 0}],
genLevelCtrl: [{attribute: 'currentLevel', ...defaultConfiguration}],
lightingColorCtrl: [
{
attribute: 'colorTemperature',
...defaultConfiguration,
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorTemperature,
},
{
attribute: 'currentX',
...defaultConfiguration,
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
},
{
attribute: 'currentY',
...defaultConfiguration,
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
},
],
closuresWindowCovering: [
{attribute: 'currentPositionLiftPercentage', ...defaultConfiguration},
{attribute: 'currentPositionTiltPercentage', ...defaultConfiguration},
],
};
export default class Report extends Extension {
private queue: Set<string> = new Set();
private failed: Set<string> = new Set();
private enabled = settings.get().advanced.report;
shouldIgnoreClusterForDevice(cluster: string, definition?: zhc.Definition): boolean {
if (definition === ZNLDP12LM && cluster === 'closuresWindowCovering') {
// Device announces it but doesn't support it
// https://github.com/Koenkk/zigbee2mqtt/issues/2611
return true;
}
return false;
}
async setupReporting(device: Device): Promise<void> {
if (this.queue.has(device.ieeeAddr) || this.failed.has(device.ieeeAddr)) return;
this.queue.add(device.ieeeAddr);
const term1 = this.enabled ? 'Setup' : 'Disable';
const term2 = this.enabled ? 'setup' : 'disabled';
try {
for (const ep of device.zh.endpoints) {
for (const [cluster, configuration] of Object.entries(clusters)) {
if (ep.supportsInputCluster(cluster) && !this.shouldIgnoreClusterForDevice(cluster, device.definition)) {
logger.debug(`${term1} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`);
const items = [];
for (const entry of configuration) {
if (entry.condition == undefined || (await entry.condition(ep))) {
const toAdd = {...entry};
if (!this.enabled) toAdd.maximumReportInterval = 0xffff;
items.push(toAdd);
delete items[items.length - 1].condition;
}
}
if (this.enabled) {
await ep.bind(cluster, this.zigbee.firstCoordinatorEndpoint());
} else {
await ep.unbind(cluster, this.zigbee.firstCoordinatorEndpoint());
}
await ep.configureReporting(cluster, items);
logger.info(`Successfully ${term2} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`);
}
}
}
if (this.enabled) {
device.zh.meta.reporting = reportKey;
} else {
delete device.zh.meta.reporting;
this.eventBus.emitReconfigure({device});
}
this.eventBus.emitDevicesChanged();
} catch (error) {
logger.error(`Failed to ${term1.toLowerCase()} reporting for '${device.ieeeAddr}' - ${(error as Error).stack}`);
this.failed.add(device.ieeeAddr);
}
device.zh.save();
this.queue.delete(device.ieeeAddr);
}
shouldSetupReporting(device: Device, messageType?: string): boolean {
if (!device || !device.zh || !device.definition) return false;
// Handle messages of type endDeviceAnnce and devIncoming.
// This message is typically send when a device comes online after being powered off
// Ikea TRADFRI tend to forget their reporting after powered off.
// Re-setup reporting.
// Only resetup reporting if configuredReportings was not populated yet,
// else reconfigure is done in zigbee-herdsman-converters ikea.js/bulbOnEvent
// configuredReportings are saved since Zigbee2MQTT 1.17.0
// https://github.com/Koenkk/zigbee2mqtt/issues/966
if (
this.enabled &&
messageType === 'deviceAnnounce' &&
device.isIkeaTradfri() &&
device.zh.endpoints.filter((e) => e.configuredReportings.length === 0).length === device.zh.endpoints.length
) {
return true;
}
// These do not support reporting.
// https://github.com/Koenkk/zigbee-herdsman/issues/110
if (
device.zh.manufacturerName === 'Philips' &&
/* istanbul ignore next */
(device.zh.softwareBuildID === '5.127.1.26581' || device.zh.softwareBuildID === '5.130.1.30000')
) {
return false;
}
if (device.zh.interviewing === true) return false;
if (device.zh.type !== 'Router' || device.zh.powerSource === 'Battery') return false;
// Gledopto devices don't support reporting.
if (devicesNotSupportingReporting.includes(device.definition) || device.definition.vendor === 'Gledopto') return false;
if (this.enabled && device.zh.meta.reporting !== undefined && device.zh.meta.reporting === reportKey) {
return false;
}
if (!this.enabled && device.zh.meta.reporting === undefined) {
return false;
}
return true;
}
override async start(): Promise<void> {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
if (this.shouldSetupReporting(device, undefined)) {
await this.setupReporting(device);
}
}
this.eventBus.onDeviceAnnounce(this, (data) => this.onZigbeeEvent_('deviceAnnounce', data.device));
this.eventBus.onDeviceMessage(this, (data) => this.onZigbeeEvent_('dummy', data.device));
this.eventBus.onDeviceJoined(this, (data) => this.onZigbeeEvent_('dummy', data.device));
this.eventBus.onDeviceNetworkAddressChanged(this, (data) => this.onZigbeeEvent_('dummy', data.device));
}
async onZigbeeEvent_(type: string, device: Device): Promise<void> {
if (this.shouldSetupReporting(device, type)) {
await this.setupReporting(device);
}
}
}
-61
View File
@@ -1,61 +0,0 @@
/* istanbul ignore file */
import logger from '../../util/logger';
// DEPRECATED
import * as settings from '../../util/settings';
import utils from '../../util/utils';
import Extension from '../extension';
/**
* This extensions soft resets the ZNP after a certain timeout.
*/
export default class SoftReset extends Extension {
private timer?: NodeJS.Timeout;
private timeout = utils.seconds(settings.get().advanced.soft_reset_timeout);
override async start(): Promise<void> {
logger.debug(`Soft reset timeout set to ${this.timeout / 1000} seconds`);
this.resetTimer();
this.eventBus.onDeviceMessage(this, () => this.resetTimer());
this.eventBus.onDeviceAnnounce(this, () => this.resetTimer());
this.eventBus.onDeviceNetworkAddressChanged(this, () => this.resetTimer());
this.eventBus.onDeviceJoined(this, () => this.resetTimer());
this.eventBus.onDeviceInterview(this, () => this.resetTimer());
}
private clearTimer(): void {
clearTimeout(this.timer);
this.timer = undefined;
}
private resetTimer(): void {
if (this.timeout === 0) {
return;
}
this.clearTimer();
this.timer = setTimeout(() => this.handleTimeout(), this.timeout);
}
private async handleTimeout(): Promise<void> {
logger.warning('Soft reset timeout triggered');
try {
await this.zigbee.reset('soft');
logger.warning('Soft reset ZNP due to timeout');
} catch (error) {
logger.warning(`Soft reset failed, trying stop/start (${(error as Error).message})`);
await this.zigbee.stop();
logger.warning('Zigbee stopped');
try {
await this.zigbee.start();
} catch (error) {
logger.error(`Failed to restart! (${(error as Error).message})`);
}
}
this.resetTimer();
}
}
+2 -14
View File
@@ -38,9 +38,6 @@ interface Topology {
* This extension creates a network map
*/
export default class NetworkMap extends Extension {
private legacyApi = settings.get().advanced.legacy_api;
private legacyTopic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
private legacyTopicRoutes = `${settings.get().mqtt.base_topic}/bridge/networkmap/routes`;
private topic = `${settings.get().mqtt.base_topic}/bridge/request/networkmap`;
private supportedFormats: {[s: string]: (topology: Topology) => KeyValue | string} = {
raw: this.raw,
@@ -53,17 +50,6 @@ export default class NetworkMap extends Extension {
}
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
/* istanbul ignore else */
if (this.legacyApi) {
if ((data.topic === this.legacyTopic || data.topic === this.legacyTopicRoutes) && this.supportedFormats[data.message] !== undefined) {
const includeRoutes = data.topic === this.legacyTopicRoutes;
const topology = await this.networkScan(includeRoutes);
let converted = this.supportedFormats[data.message](topology);
converted = data.message === 'raw' ? stringify(converted) : converted;
await this.mqtt.publish(`bridge/networkmap/${data.message}`, converted as string, {});
}
}
if (data.topic === this.topic) {
const message = utils.parseJSON(data.message, data.message);
try {
@@ -242,6 +228,7 @@ export default class NetworkMap extends Extension {
logger.debug((error as Error).stack!);
}
/* istanbul ignore else */
if (includeRoutes) {
try {
const result = await requestWithRetry<zh.RoutingTable>(async () => await device.zh.routingTable());
@@ -329,6 +316,7 @@ export default class NetworkMap extends Extension {
const routingTable = routingTables.get(device);
/* istanbul ignore else */
if (routingTable) {
for (const entry of routingTable.table) {
if (entry.status === 'ACTIVE' && entry.nextHop === neighbor.networkAddress) {
+22 -94
View File
@@ -28,8 +28,6 @@ function isValidUrl(url: string): boolean {
type UpdateState = 'updating' | 'idle' | 'available';
interface UpdatePayload {
update_available?: boolean;
update: {
progress?: number;
remaining?: number;
@@ -39,13 +37,11 @@ interface UpdatePayload {
};
}
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`);
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check)`, 'i');
export default class OTAUpdate extends Extension {
private inProgress = new Set();
private lastChecked: {[s: string]: number} = {};
private legacyApi = settings.get().advanced.legacy_api;
override async start(): Promise<void> {
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
@@ -80,8 +76,12 @@ export default class OTAUpdate extends Extension {
}
private removeProgressAndRemainingFromState(device: Device): void {
delete this.state.get(device).update?.progress;
delete this.state.get(device).update?.remaining;
const deviceState = this.state.get(device);
if (deviceState.update) {
delete deviceState.update.progress;
delete deviceState.update.remaining;
}
}
@bind private async onZigbeeEvent(data: eventdata.DeviceMessage): Promise<void> {
@@ -110,18 +110,11 @@ export default class OTAUpdate extends Extension {
logger.debug(`Failed to check if update available for '${data.device.name}' (${error})`);
}
const payload = this.getEntityPublishPayload(data.device, availableResult ?? 'idle');
await this.publishEntityState(data.device, payload);
await this.publishEntityState(data.device, this.getEntityPublishPayload(data.device, availableResult ?? 'idle'));
if (availableResult?.available) {
const message = `Update available for '${data.device.name}'`;
logger.info(message);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: 'available', device: data.device.name};
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message, meta}));
}
}
}
@@ -166,24 +159,19 @@ export default class OTAUpdate extends Extension {
},
};
if (progress != undefined) {
if (progress !== undefined) {
payload.update.progress = progress;
}
if (remaining != undefined) {
if (remaining !== undefined) {
payload.update.remaining = Math.round(remaining);
}
/* istanbul ignore else */
if (this.legacyApi) {
payload.update_available = typeof state === 'string' ? state === 'available' : state.available;
}
return payload;
}
@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
if ((!this.legacyApi || !data.topic.match(legacyTopicRegex)) && !data.topic.match(topicRegex)) {
if (!data.topic.match(topicRegex)) {
return;
}
@@ -191,7 +179,7 @@ export default class OTAUpdate extends Extension {
const ID = (typeof message === 'object' && message['id'] !== undefined ? message.id : message) as string;
const device = this.zigbee.resolveEntity(ID);
const type = data.topic.substring(data.topic.lastIndexOf('/') + 1);
const responseData: {id: string; updateAvailable?: boolean; from?: KeyValue | null; to?: KeyValue | null} = {id: ID};
const responseData: {id: string; update_available?: boolean; from?: KeyValue | null; to?: KeyValue | null} = {id: ID};
let error: string | undefined;
let errorStack: string | undefined;
@@ -199,12 +187,6 @@ export default class OTAUpdate extends Extension {
error = `Device '${ID}' does not exist`;
} else if (!device.definition || !device.definition.ota) {
error = `Device '${device.name}' does not support OTA updates`;
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `not_supported`, device: device.name};
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: error, meta}));
}
} else if (this.inProgress.has(device.ieeeAddr)) {
error = `Update or check for update already in progress for '${device.name}'`;
} else {
@@ -214,53 +196,25 @@ export default class OTAUpdate extends Extension {
const msg = `Checking if update available for '${device.name}'`;
logger.info(msg);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `checking_if_available`, device: device.name};
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta}));
}
try {
const availableResult = await device.definition.ota.isUpdateAvailable(device.zh, undefined);
const msg = `${availableResult.available ? 'Update' : 'No update'} available for '${device.name}'`;
logger.info(msg);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {
status: availableResult.available ? 'available' : 'not_available',
device: device.name,
};
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta}));
}
const payload = this.getEntityPublishPayload(device, availableResult);
await this.publishEntityState(device, payload);
await this.publishEntityState(device, this.getEntityPublishPayload(device, availableResult));
this.lastChecked[device.ieeeAddr] = Date.now();
responseData.updateAvailable = availableResult.available;
responseData.update_available = availableResult.available;
} catch (e) {
error = `Failed to check if update available for '${device.name}' (${(e as Error).message})`;
errorStack = (e as Error).stack;
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `check_failed`, device: device.name};
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: error, meta}));
}
}
} else {
// type === 'update'
const msg = `Updating '${device.name}' to latest firmware`;
logger.info(msg);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `update_in_progress`, device: device.name};
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta}));
}
try {
const onProgress = async (progress: number, remaining: number): Promise<void> => {
const onProgress = async (progress: number, remaining: number | null): Promise<void> => {
let msg = `Update of '${device.name}' at ${progress.toFixed(2)}%`;
if (remaining) {
msg += `, ≈ ${Math.round(remaining / 60)} minutes remaining`;
@@ -268,26 +222,17 @@ export default class OTAUpdate extends Extension {
logger.info(msg);
const payload = this.getEntityPublishPayload(device, 'updating', progress, remaining);
await this.publishEntityState(device, payload);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `update_progress`, device: device.name, progress};
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta}));
}
await this.publishEntityState(device, this.getEntityPublishPayload(device, 'updating', progress, remaining ?? undefined));
};
const from_ = await this.readSoftwareBuildIDAndDateCode(device, 'immediate');
const fileVersion = await device.definition.ota.updateToLatest(device.zh, onProgress);
logger.info(`Finished update of '${device.name}'`);
this.removeProgressAndRemainingFromState(device);
const payload = this.getEntityPublishPayload(device, {
available: false,
currentFileVersion: fileVersion,
otaFileVersion: fileVersion,
});
await this.publishEntityState(device, payload);
await this.publishEntityState(
device,
this.getEntityPublishPayload(device, {available: false, currentFileVersion: fileVersion, otaFileVersion: fileVersion}),
);
const to = await this.readSoftwareBuildIDAndDateCode(device);
const [fromS, toS] = [stringify(from_), stringify(to)];
logger.info(`Device '${device.name}' was updated from '${fromS}' to '${toS}'`);
@@ -299,38 +244,21 @@ export default class OTAUpdate extends Extension {
*/
this.eventBus.emitReconfigure({device});
this.eventBus.emitDevicesChanged();
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `update_succeeded`, device: device.name, from: from_, to};
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message, meta}));
}
} catch (e) {
logger.debug(`Update of '${device.name}' failed (${e})`);
error = `Update of '${device.name}' failed (${(e as Error).message})`;
errorStack = (e as Error).stack;
this.removeProgressAndRemainingFromState(device);
const payload = this.getEntityPublishPayload(device, 'available');
await this.publishEntityState(device, payload);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `update_failed`, device: device.name};
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: error, meta}));
}
await this.publishEntityState(device, this.getEntityPublishPayload(device, 'available'));
}
}
this.inProgress.delete(device.ieeeAddr);
}
const triggeredViaLegacyApi = data.topic.match(legacyTopicRegex);
if (!triggeredViaLegacyApi) {
const response = utils.getResponse(message, responseData, error);
await this.mqtt.publish(`bridge/response/device/ota_update/${type}`, stringify(response));
}
const response = utils.getResponse(message, responseData, error);
await this.mqtt.publish(`bridge/response/device/ota_update/${type}`, stringify(response));
if (error) {
logger.error(error);
+10 -63
View File
@@ -1,11 +1,8 @@
import assert from 'assert';
import type * as zhc from 'zigbee-herdsman-converters';
import bind from 'bind-decorator';
import stringify from 'json-stable-stringify-without-jsonify';
import * as zhc from 'zigbee-herdsman-converters';
import * as philips from 'zigbee-herdsman-converters/lib/philips';
import Device from '../model/device';
import Group from '../model/group';
import logger from '../util/logger';
@@ -23,23 +20,6 @@ loadTopicGetSetRegex();
const STATE_VALUES: ReadonlyArray<string> = ['on', 'off', 'toggle', 'open', 'close', 'stop', 'lock', 'unlock'];
const SCENE_CONVERTER_KEYS: ReadonlyArray<string> = ['scene_store', 'scene_add', 'scene_remove', 'scene_remove_all', 'scene_rename'];
// Legacy: don't provide default converters anymore, this is required by older z2m installs not saving group members
const DEFAULT_GROUP_CONVERTERS: ReadonlyArray<zhc.Tz.Converter> = [
zhc.toZigbee.light_onoff_brightness,
zhc.toZigbee.light_color_colortemp,
philips.tz.effect, // Support Hue effects for groups
zhc.toZigbee.ignore_transition,
zhc.toZigbee.cover_position_tilt,
zhc.toZigbee.thermostat_occupied_heating_setpoint,
zhc.toZigbee.tint_scene,
zhc.toZigbee.light_brightness_move,
zhc.toZigbee.light_brightness_step,
zhc.toZigbee.light_colortemp_step,
zhc.toZigbee.light_colortemp_move,
zhc.toZigbee.light_hue_saturation_move,
zhc.toZigbee.light_hue_saturation_step,
];
interface ParsedTopic {
ID: string;
endpoint: string | undefined;
@@ -96,36 +76,6 @@ export default class Publish extends Extension {
}
}
async legacyLog(payload: KeyValue): Promise<void> {
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
await this.mqtt.publish('bridge/log', stringify(payload));
}
}
legacyRetrieveState(
re: Device | Group,
converter: zhc.Tz.Converter,
result: zhc.Tz.ConvertSetResult,
target: zh.Endpoint | zh.Group,
key: string,
meta: zhc.Tz.Meta,
): void {
// It's possible for devices to get out of sync when writing an attribute that's not reportable.
// So here we re-read the value after a specified timeout, this timeout could for example be the
// transition time of a color change or for forcing a state read for devices that don't
// automatically report a new state when set.
// 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 == deprecated
if (re instanceof Device && result && result.readAfterWriteTime !== undefined && re.options.retrieve_state) {
const convertGet = converter.convertGet;
assert(convertGet !== undefined, 'Converter has `readAfterWriteTime` but no `convertGet`');
setTimeout(() => convertGet(target, key, meta), result.readAfterWriteTime);
}
}
updateMessageHomeAssistant(message: KeyValue, entityState: KeyValue): void {
/**
* Home Assistant always publishes 'state', even when e.g. only setting
@@ -154,7 +104,6 @@ export default class Publish extends Extension {
const re = this.zigbee.resolveEntity(parsedTopic.ID);
if (!re) {
await this.legacyLog({type: `entity_not_found`, message: {friendly_name: parsedTopic.ID}});
logger.error(`Entity '${parsedTopic.ID}' is unknown`);
return;
}
@@ -194,14 +143,7 @@ export default class Publish extends Extension {
re.zh.members.map((e) => [e.getDevice().ieeeAddr, this.state.get(this.zigbee.resolveEntity(e.getDevice().ieeeAddr)!)]),
)
: undefined;
let converters: ReadonlyArray<zhc.Tz.Converter>;
if (Array.isArray(definition)) {
const c = new Set(definition.map((d) => d.toZigbee).flat());
converters = c.size === 0 ? DEFAULT_GROUP_CONVERTERS : Array.from(c);
} else {
converters = definition?.toZigbee;
}
const converters = this.getDefinitionConverters(definition);
this.updateMessageHomeAssistant(message, entityState);
@@ -329,8 +271,6 @@ export default class Publish extends Extension {
addToToPublish(this.zigbee.resolveEntity(ieeeAddr)!, state);
}
}
this.legacyRetrieveState(re, converter, result, localTarget, key, meta);
} else if (parsedTopic.type === 'get' && converter.convertGet) {
logger.debug(`Publishing get '${parsedTopic.type}' '${key}' to '${re.name}'`);
await converter.convertGet(localTarget, key, meta);
@@ -342,7 +282,6 @@ export default class Publish extends Extension {
const message = `Publish '${parsedTopic.type}' '${key}' to '${re.name}' failed: '${error}'`;
logger.error(message);
logger.debug((error as Error).stack!);
await this.legacyLog({type: `zigbee_publish_error`, message, meta: {friendly_name: re.name}});
}
usedConverters[endpointOrGroupID].push(converter);
@@ -362,4 +301,12 @@ export default class Publish extends Extension {
this.eventBus.emitScenesChanged({entity: re});
}
}
private getDefinitionConverters(definition: zhc.Definition | zhc.Definition[]): ReadonlyArray<zhc.Tz.Converter> {
if (Array.isArray(definition)) {
return definition.length ? Array.from(new Set(definition.map((d) => d.toZigbee).flat())) : [];
} else {
return definition?.toZigbee;
}
}
}
-4
View File
@@ -117,10 +117,6 @@ export default class Device {
return names;
}
isIkeaTradfri(): boolean {
return this.zh.manufacturerID === 4476;
}
isDevice(): this is Device {
return true;
}
+3 -6
View File
@@ -1,5 +1,3 @@
import type {QoS} from 'mqtt-packet';
import fs from 'fs';
import bind from 'bind-decorator';
@@ -35,7 +33,7 @@ export default class MQTT {
const options: mqtt.IClientOptions = {
will: {
topic: `${settings.get().mqtt.base_topic}/bridge/state`,
payload: Buffer.from(utils.availabilityPayload('offline', settings.get())),
payload: Buffer.from(JSON.stringify({state: 'offline'})),
retain: settings.get().mqtt.force_disable_retain ? false : true,
qos: 1,
},
@@ -84,7 +82,6 @@ export default class MQTT {
this.client = mqtt.connect(mqttSettings.server, options);
// https://github.com/Koenkk/zigbee2mqtt/issues/9822
this.client.stream.setMaxListeners(0);
this.eventBus.onPublishAvailability(this, this.publishStateOnline);
this.client.on('connect', async () => {
// Set timer at interval to check if connected to MQTT server.
@@ -123,13 +120,13 @@ export default class MQTT {
}
@bind async publishStateOnline(): Promise<void> {
await this.publish('bridge/state', utils.availabilityPayload('online', settings.get()), {retain: true, qos: 0});
await this.publish('bridge/state', JSON.stringify({state: 'online'}), {retain: true, qos: 0});
}
async disconnect(): Promise<void> {
clearTimeout(this.connectionTimer);
clearTimeout(this.republishRetainedTimer);
await this.publish('bridge/state', utils.availabilityPayload('offline', settings.get()), {retain: true, qos: 0});
await this.publish('bridge/state', JSON.stringify({state: 'offline'}), {retain: true, qos: 0});
this.eventBus.removeListeners(this);
logger.info('Disconnecting from MQTT server');
this.client?.end();
+1 -13
View File
@@ -5,7 +5,6 @@ import type TypeGroup from 'lib/model/group';
import type TypeMQTT from 'lib/mqtt';
import type TypeState from 'lib/state';
import type TypeZigbee from 'lib/zigbee';
import type {QoS} from 'mqtt-packet';
import type * as zhc from 'zigbee-herdsman-converters';
import type {
CoordinatorVersion as ZHCoordinatorVersion,
@@ -31,6 +30,7 @@ declare global {
type Device = TypeDevice;
type State = TypeState;
type Extension = TypeExtension;
type QoS = 0 | 1 | 2;
// Types
type ExternalDefinition = zhc.Definition & {homeassistant: unknown};
@@ -189,8 +189,6 @@ declare global {
groups: {[s: string]: OptionalProps<Omit<GroupOptions, 'ID'>, 'devices'>};
device_options: KeyValue;
advanced: {
legacy_api: boolean;
legacy_availability_payload: boolean;
log_rotation: boolean;
log_symlink_current: boolean;
log_output: ('console' | 'file' | 'syslog')[];
@@ -215,14 +213,6 @@ declare global {
timestamp_format: string;
output: 'json' | 'attribute' | 'attribute_and_json';
transmit_power?: number;
// Everything below is deprecated
availability_timeout?: number;
availability_blocklist: string[];
availability_passlist: string[];
availability_blacklist: string[];
availability_whitelist: string[];
soft_reset_timeout: number;
report: boolean;
};
}
@@ -231,7 +221,6 @@ declare global {
retention?: number;
availability?: boolean | {timeout: number};
optimistic?: boolean;
retrieve_state?: boolean;
debounce?: number;
debounce_ignore?: string[];
throttle?: number;
@@ -258,7 +247,6 @@ declare global {
filtered_attributes?: string[];
filtered_cache?: string[];
filtered_optimistic?: string[];
retrieve_state?: boolean;
homeassistant?: KeyValue;
friendly_name: string;
description?: string;
-29
View File
@@ -451,20 +451,6 @@
"type": "object",
"title": "Advanced",
"properties": {
"legacy_api": {
"type": "boolean",
"title": "Legacy API",
"requiresRestart": true,
"description": "Disables the legacy api (false = disable)",
"default": true
},
"legacy_availability_payload": {
"type": "boolean",
"title": "Legacy availability payload",
"requiresRestart": true,
"description": "Payload to be used for device availability and bridge/state topics. true = text, false = JSON",
"default": true
},
"log_rotation": {
"type": "boolean",
"title": "Log rotation",
@@ -765,21 +751,6 @@
"description": "Home Assistant legacy triggers, when enabled Zigbee2mqt will send an empty 'action' or 'click' after one has been send. A 'sensor_action' and 'sensor_click' will be discovered",
"default": true
},
"soft_reset_timeout": {
"type": "number",
"minimum": 0,
"requiresRestart": true,
"title": "Soft reset timeout (deprecated)",
"description": "Soft reset ZNP after timeout",
"readOnly": true
},
"report": {
"type": "boolean",
"title": "Reporting",
"requiresRestart": true,
"readOnly": true,
"description": "Enables report feature (deprecated)"
},
"baudrate": {
"type": "number",
"title": "Baudrate (deprecated)",
+1 -39
View File
@@ -19,8 +19,6 @@ objectAssignDeep(schema, schemaJson);
delete schema.properties.advanced.properties.homeassistant_legacy_entity_attributes;
delete schema.properties.advanced.properties.homeassistant_legacy_triggers;
delete schema.properties.advanced.properties.homeassistant_status_topic;
delete schema.properties.advanced.properties.soft_reset_timeout;
delete schema.properties.advanced.properties.report;
delete schema.properties.advanced.properties.baudrate;
delete schema.properties.advanced.properties.rtscts;
delete schema.properties.advanced.properties.ikea_ota_use_test_url;
@@ -83,8 +81,6 @@ const defaults: RecursivePartial<Settings> = {
},
device_options: {},
advanced: {
legacy_api: true,
legacy_availability_payload: true,
log_rotation: true,
log_symlink_current: false,
log_output: ['console', 'file'],
@@ -108,13 +104,6 @@ const defaults: RecursivePartial<Settings> = {
network_key: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
timestamp_format: 'YYYY-MM-DD HH:mm:ss',
output: 'json',
// Everything below is deprecated
availability_blocklist: [],
availability_passlist: [],
availability_blacklist: [],
availability_whitelist: [],
soft_reset_timeout: 0,
report: false,
},
};
@@ -166,7 +155,7 @@ function loadSettingsWithDefaults(): void {
objectAssignDeep(_settingsWithDefaults.homeassistant, defaults, sLegacy, s);
}
if (_settingsWithDefaults.availability || _settingsWithDefaults.advanced?.availability_timeout) {
if (_settingsWithDefaults.availability) {
const defaults = {};
const s = typeof _settingsWithDefaults.availability === 'object' ? _settingsWithDefaults.availability : {};
// @ts-expect-error ignore typing
@@ -367,19 +356,6 @@ export function validate(): string[] {
}
}
const checkAvailabilityList = (list: string[], type: string): void => {
list.forEach((e) => {
if (!getDevice(e)) {
errors.push(`Non-existing entity '${e}' specified in '${type}'`);
}
});
};
checkAvailabilityList(settingsWithDefaults.advanced.availability_blacklist, 'availability_blacklist');
checkAvailabilityList(settingsWithDefaults.advanced.availability_whitelist, 'availability_whitelist');
checkAvailabilityList(settingsWithDefaults.advanced.availability_blocklist, 'availability_blocklist');
checkAvailabilityList(settingsWithDefaults.advanced.availability_passlist, 'availability_passlist');
return errors;
}
@@ -631,20 +607,6 @@ export function addDevice(ID: string): DeviceOptionsWithId {
return getDevice(ID)!; // valid from creation above
}
export function addDeviceToPasslist(ID: string): void {
const settings = getInternalSettings();
if (!settings.passlist) {
settings.passlist = [];
}
if (settings.passlist.includes(ID)) {
throw new Error(`Device '${ID}' already in passlist`);
}
settings.passlist.push(ID);
write();
}
export function blockDevice(ID: string): void {
const settings = getInternalSettings();
if (!settings.blocklist) {
+3 -23
View File
@@ -77,10 +77,8 @@ async function getZigbee2MQTTVersion(includeCommitHash = true): Promise<{commitH
}
async function getDependencyVersion(depend: string): Promise<{version: string}> {
const modulePath = path.dirname(require.resolve(depend));
const packageJSONPath = path.join(modulePath.slice(0, modulePath.indexOf(depend) + depend.length), 'package.json');
const packageJSON = await import(packageJSONPath);
const version = packageJSON.version;
const packageJsonPath = require.resolve(`${depend}/package.json`);
const version = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).version;
return {version};
}
@@ -306,23 +304,10 @@ function isAvailabilityEnabledForEntity(entity: Device | Group, settings: Settin
return !!entity.options.availability;
}
// availability_timeout = deprecated
if (!(settings.advanced.availability_timeout || settings.availability)) {
if (!settings.availability) {
return false;
}
const passlist = settings.advanced.availability_passlist.concat(settings.advanced.availability_whitelist);
if (passlist.length > 0) {
return passlist.includes(entity.name) || passlist.includes(entity.ieeeAddr);
}
const blocklist = settings.advanced.availability_blacklist.concat(settings.advanced.availability_blocklist);
if (blocklist.length > 0) {
return !blocklist.includes(entity.name) && !blocklist.includes(entity.ieeeAddr);
}
return true;
}
@@ -342,10 +327,6 @@ function isZHGroup(obj: unknown): obj is zh.Group {
return obj?.constructor.name.toLowerCase() === 'group';
}
function availabilityPayload(state: 'online' | 'offline', settings: Settings): string {
return settings.advanced.legacy_availability_payload ? state : JSON.stringify({state});
}
const hours = (hours: number): number => 1000 * 60 * 60 * hours;
const minutes = (minutes: number): number => 1000 * 60 * minutes;
const seconds = (seconds: number): number => 1000 * seconds;
@@ -460,7 +441,6 @@ export default {
sanitizeImageParameter,
isAvailabilityEnabledForEntity,
publishLastSeen,
availabilityPayload,
getAllFiles,
filterProperties,
flatten,
-4
View File
@@ -219,10 +219,6 @@ export default class Zigbee {
return await this.herdsman.getNetworkParameters();
}
async reset(type: 'soft' | 'hard'): Promise<void> {
await this.herdsman.reset(type);
}
async stop(): Promise<void> {
logger.info('Stopping zigbee-herdsman...');
await this.herdsman.stop();
-8610
View File
File diff suppressed because it is too large Load Diff
+5968
View File
File diff suppressed because it is too large Load Diff
-75
View File
@@ -1,75 +0,0 @@
#!/bin/bash
function zigbee2mqtt-show-short-info {
echo "Setup for Zigbee2MQTT bridge."
}
function zigbee2mqtt-show-long-info {
echo "This script installs the Zigbee2MQTT bridge"
}
function zigbee2mqtt-show-copyright-info {
echo "Original concept by Landrash <https://github.com/landrash>."
}
function zigbee2mqtt-install-package {
echo -n "Installing dependencies : "
node=$(which npm)
if [ -z "${node}" ]; then #Installing NodeJS if not already installed.
printf "Downloading and installing NodeJS...\\n"
curl -sL https://deb.nodesource.com/setup_10.x | bash -
apt install -y nodejs
fi
echo "Cloning Zigbee2MQTT git repository"
git clone https://github.com/Koenkk/zigbee2mqtt.git /opt/zigbee2mqtt
chown -R pi:pi /opt/zigbee2mqtt
echo "Running install. This might take a while and can produce some expected errors"
cd /opt/zigbee2mqtt || exit
su pi -c "npm ci"
echo "Creating service file zigbee2mqtt.service"
service_path="/etc/systemd/system/zigbee2mqtt.service"
echo "[Unit]
Description=zigbee2mqtt
After=network.target
[Service]
Type=notify
ExecStart=/usr/bin/node index.js
WorkingDirectory=/opt/zigbee2mqtt
StandardOutput=inherit
StandardError=inherit
WatchdogSec=10s
Restart=always
User=pi
[Install]
WantedBy=multi-user.target" > $service_path
echo "Checking the installation..."
if [ ! -f /opt/zigbee2mqtt/data/configuration.yaml ]; then
validation=""
else
validation="ok"
fi
if [ ! -z "${validation}" ]; then
echo
echo -e "\\e[32mInstallation done..\\e[0m"
echo -e "Update of configuration.yaml is required found at /opt/zigbee2mqtt/data/"
echo -e "Some further configuration is required and details can be found here https://www.zigbee2mqtt.io"
echo
echo -e "Service can be started after configuration but running sudo systemctl start zigbee2mqtt"
echo
else
echo
echo -e "\\e[31mInstallation failed..."
echo
return 1
fi
return 0
}
[[ "$_" == "$0" ]] && zigbee2mqtt-install-package
+64 -46
View File
@@ -68,11 +68,21 @@ describe('Availability', () => {
});
it('Should publish availability on startup for device where it is enabled for', async () => {
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'online', {retain: true, qos: 1}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', 'online', {retain: true, qos: 1}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color/availability',
stringify({state: 'online'}),
{retain: true, qos: 1},
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/remote/availability',
stringify({state: 'online'}),
{retain: true, qos: 1},
expect.any(Function),
);
expect(MQTT.publish).not.toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color_2/availability',
'online',
stringify({state: 'online'}),
{retain: true, qos: 1},
expect.any(Function),
);
@@ -109,7 +119,12 @@ describe('Availability', () => {
await setTimeAndAdvanceTimers(utils.minutes(7));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);
expect(devices.bulb_color.ping).toHaveBeenNthCalledWith(1, true);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'offline', {retain: true, qos: 1}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color/availability',
stringify({state: 'offline'}),
{retain: true, qos: 1},
expect.any(Function),
);
});
it('Shouldnt do anything for a device when availability: false is set for device', async () => {
@@ -123,7 +138,12 @@ describe('Availability', () => {
MQTT.publish.mockClear();
await setTimeAndAdvanceTimers(utils.hours(26));
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', 'offline', {retain: true, qos: 1}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/remote/availability',
stringify({state: 'offline'}),
{retain: true, qos: 1},
expect.any(Function),
);
});
it('Should reset ping timer when device last seen changes for active device', async () => {
@@ -133,7 +153,12 @@ describe('Availability', () => {
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color});
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'offline', {retain: true, qos: 1}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color/availability',
stringify({state: 'offline'}),
{retain: true, qos: 1},
expect.any(Function),
);
await setTimeAndAdvanceTimers(utils.minutes(7));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
@@ -165,7 +190,12 @@ describe('Availability', () => {
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
await zigbeeHerdsman.events.lastSeenChanged({device: devices.remote});
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', 'offline', {retain: true, qos: 1}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/remote/availability',
stringify({state: 'offline'}),
{retain: true, qos: 1},
expect.any(Function),
);
await setTimeAndAdvanceTimers(utils.hours(25));
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
@@ -178,12 +208,22 @@ describe('Availability', () => {
MQTT.publish.mockClear();
await setTimeAndAdvanceTimers(utils.minutes(15));
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'offline', {retain: true, qos: 1}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color/availability',
stringify({state: 'offline'}),
{retain: true, qos: 1},
expect.any(Function),
);
devices.bulb_color.lastSeen = Date.now();
await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'online', {retain: true, qos: 1}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color/availability',
stringify({state: 'online'}),
{retain: true, qos: 1},
expect.any(Function),
);
});
it('Should allow to change availability timeout via device options', async () => {
@@ -299,13 +339,22 @@ describe('Availability', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', '', {retain: true, qos: 1}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name/availability', 'online', {retain: true, qos: 1}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_new_name/availability',
stringify({state: 'online'}),
{retain: true, qos: 1},
expect.any(Function),
);
await setTimeAndAdvanceTimers(utils.hours(12));
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name/availability', 'offline', {retain: true, qos: 1}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_new_name/availability',
stringify({state: 'offline'}),
{retain: true, qos: 1},
expect.any(Function),
);
});
it('Should publish availability payload in JSON format', async () => {
settings.set(['advanced', 'legacy_availability_payload'], false);
await resetExtension();
devices.remote.ping.mockClear();
MQTT.publish.mockClear();
@@ -319,37 +368,6 @@ describe('Availability', () => {
);
});
it('Deprecated - should allow to block via advanced.availability_blocklist', async () => {
settings.set(['advanced', 'availability_blocklist'], [devices.bulb_color.ieeeAddr]);
await resetExtension();
devices.bulb_color.ping.mockClear();
await setTimeAndAdvanceTimers(utils.minutes(12));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
});
it('Deprecated - should allow to pass certain devices via availability_passlist', async () => {
settings.set(['advanced', 'availability_passlist'], [devices.bulb_color_2.ieeeAddr]);
settings.changeEntityOptions(devices.bulb_color_2.ieeeAddr, {availability: null});
await resetExtension();
devices.bulb_color.ping.mockClear();
devices.bulb_color_2.ping.mockClear();
await setTimeAndAdvanceTimers(utils.minutes(12));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(1);
});
it('Deprecated - should allow to enable via availability_timeout', async () => {
settings.set(['availability'], false);
settings.set(['advanced', 'availability_timeout'], 60);
await resetExtension();
devices.bulb_color.ping.mockClear();
await setTimeAndAdvanceTimers(utils.minutes(12));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);
});
it('Should publish availability for groups', async () => {
settings.set(['devices', devices.bulb_color_2.ieeeAddr, 'availability'], true);
await resetExtension();
@@ -357,7 +375,7 @@ describe('Availability', () => {
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/group_tradfri_remote/availability',
'online',
stringify({state: 'online'}),
{retain: true, qos: 1},
expect.any(Function),
);
@@ -365,7 +383,7 @@ describe('Availability', () => {
await setTimeAndAdvanceTimers(utils.minutes(12));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/group_tradfri_remote/availability',
'offline',
stringify({state: 'offline'}),
{retain: true, qos: 1},
expect.any(Function),
);
@@ -375,7 +393,7 @@ describe('Availability', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/group_tradfri_remote/availability',
'online',
stringify({state: 'online'}),
{retain: true, qos: 1},
expect.any(Function),
);
+2 -218
View File
@@ -366,9 +366,9 @@ describe('Bind', () => {
expect.any(Function),
);
// Should configure reproting for device added to group
// Should configure reporting for device added to group
target1Member.configureReporting.mockClear();
await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb');
await MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb'}));
await flushPromises();
expect(target1Member.configureReporting).toHaveBeenCalledTimes(2);
expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [
@@ -648,222 +648,6 @@ describe('Bind', () => {
);
});
it('Legacy api: Should bind', async () => {
const device = zigbeeHerdsman.devices.remote;
const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
const endpoint = device.getEndpoint(1);
mockClear(device);
MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'bulb_color');
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(3);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target);
expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({
type: 'device_bind',
message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'},
});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({
type: 'device_bind',
message: {from: 'remote', to: 'bulb_color', cluster: 'genOnOff'},
});
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({
type: 'device_bind',
message: {from: 'remote', to: 'bulb_color', cluster: 'genLevelCtrl'},
});
});
it('Legacy api: Should log error when there is nothing to bind', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
mockClear(device);
logger.error.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'button');
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(0);
expect(logger.error).toHaveBeenCalledWith(`Nothing to bind from 'remote' to 'button'`);
});
it('Legacy api: Should unbind', async () => {
const device = zigbeeHerdsman.devices.remote;
const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
const endpoint = device.getEndpoint(1);
mockClear(device);
MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', 'bulb_color');
await flushPromises();
expect(endpoint.unbind).toHaveBeenCalledTimes(3);
expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target);
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target);
expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({
type: 'device_unbind',
message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'},
});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({
type: 'device_unbind',
message: {from: 'remote', to: 'bulb_color', cluster: 'genOnOff'},
});
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({
type: 'device_unbind',
message: {from: 'remote', to: 'bulb_color', cluster: 'genLevelCtrl'},
});
});
it('Legacy api: Should unbind coordinator', async () => {
const device = zigbeeHerdsman.devices.remote;
const target = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
const endpoint = device.getEndpoint(1);
mockClear(device);
endpoint.unbind.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', 'Coordinator');
await flushPromises();
expect(endpoint.unbind).toHaveBeenCalledTimes(3);
expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target);
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target);
expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({
type: 'device_unbind',
message: {from: 'remote', to: 'Coordinator', cluster: 'genScenes'},
});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({
type: 'device_unbind',
message: {from: 'remote', to: 'Coordinator', cluster: 'genOnOff'},
});
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({
type: 'device_unbind',
message: {from: 'remote', to: 'Coordinator', cluster: 'genLevelCtrl'},
});
});
it('Legacy api: Should bind to groups', async () => {
const device = zigbeeHerdsman.devices.remote;
const target = zigbeeHerdsman.groups.group_1;
const endpoint = device.getEndpoint(1);
mockClear(device);
MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'group_1');
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(3);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target);
expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({
type: 'device_bind',
message: {from: 'remote', to: 'group_1', cluster: 'genScenes'},
});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({
type: 'device_bind',
message: {from: 'remote', to: 'group_1', cluster: 'genOnOff'},
});
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({
type: 'device_bind',
message: {from: 'remote', to: 'group_1', cluster: 'genLevelCtrl'},
});
});
it('Legacy api: Should bind to group by number', async () => {
const device = zigbeeHerdsman.devices.remote;
const target = zigbeeHerdsman.groups.group_1;
const endpoint = device.getEndpoint(1);
mockClear(device);
MQTT.events.message('zigbee2mqtt/bridge/bind/remote', '1');
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(3);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target);
expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({
type: 'device_bind',
message: {from: 'remote', to: 'group_1', cluster: 'genScenes'},
});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({
type: 'device_bind',
message: {from: 'remote', to: 'group_1', cluster: 'genOnOff'},
});
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({
type: 'device_bind',
message: {from: 'remote', to: 'group_1', cluster: 'genLevelCtrl'},
});
});
it('Legacy api: Should log when bind fails', async () => {
logger.error.mockClear();
const device = zigbeeHerdsman.devices.remote;
const endpoint = device.getEndpoint(1);
mockClear(device);
endpoint.bind.mockImplementationOnce(() => {
throw new Error('failed');
});
MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'bulb_color');
await flushPromises();
expect(logger.error).toHaveBeenCalledWith("Failed to bind cluster 'genScenes' from 'remote' to 'bulb_color' (Error: failed)");
expect(endpoint.bind).toHaveBeenCalledTimes(3);
});
it('Legacy api: Should bind from non default endpoints', async () => {
const device = zigbeeHerdsman.devices.remote;
const target = zigbeeHerdsman.devices.QBKG03LM.getEndpoint(3);
const endpoint = device.getEndpoint(2);
mockClear(device);
MQTT.events.message('zigbee2mqtt/bridge/bind/remote/ep2', 'wall_switch_double/right');
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
});
it('Legacy api: Should bind to default endpoint returned by endpoints()', async () => {
const device = zigbeeHerdsman.devices.remote;
const target = zigbeeHerdsman.devices.QBKG04LM.getEndpoint(2);
const endpoint = device.getEndpoint(2);
mockClear(device);
MQTT.events.message('zigbee2mqtt/bridge/bind/remote/ep2', 'wall_switch');
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
});
it('Legacy api: Should unbind from default_bind_group', async () => {
const device = zigbeeHerdsman.devices.remote;
const target = 'default_bind_group';
const endpoint = device.getEndpoint(1);
mockClear(device);
MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', target);
await flushPromises();
expect(endpoint.unbind).toHaveBeenCalledTimes(3);
expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', 901);
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', 901);
expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', 901);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({
type: 'device_unbind',
message: {from: 'remote', to: 'default_bind_group', cluster: 'genScenes'},
});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({
type: 'device_unbind',
message: {from: 'remote', to: 'default_bind_group', cluster: 'genOnOff'},
});
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log');
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({
type: 'device_unbind',
message: {from: 'remote', to: 'default_bind_group', cluster: 'genLevelCtrl'},
});
});
it('Should poll bounded Hue bulb when receiving message from Hue dimmer', async () => {
const remote = zigbeeHerdsman.devices.remote;
const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1};
+3 -165
View File
@@ -49,7 +49,6 @@ describe('Bridge', () => {
beforeAll(async () => {
jest.useFakeTimers();
mockRestart = jest.fn();
settings.set(['advanced', 'legacy_api'], false);
controller = new Controller(mockRestart, jest.fn());
await controller.start();
await flushPromises();
@@ -60,7 +59,6 @@ describe('Bridge', () => {
MQTT.mock.reconnecting = false;
data.writeDefaultConfiguration();
settings.reRead();
settings.set(['advanced', 'legacy_api'], false);
data.writeDefaultState();
logger.info.mockClear();
logger.warning.mockClear();
@@ -94,10 +92,6 @@ describe('Bridge', () => {
advanced: {
adapter_concurrent: undefined,
adapter_delay: undefined,
availability_blacklist: [],
availability_blocklist: [],
availability_passlist: [],
availability_whitelist: [],
cache_state: true,
cache_state_persistent: true,
cache_state_send_on_startup: true,
@@ -105,8 +99,6 @@ describe('Bridge', () => {
elapsed: false,
ext_pan_id: [221, 221, 221, 221, 221, 221, 221, 221],
last_seen: 'disable',
legacy_api: false,
legacy_availability_payload: true,
log_debug_namespace_ignore: '',
log_debug_to_mqtt_frontend: false,
log_directory: directory,
@@ -119,8 +111,6 @@ describe('Bridge', () => {
log_syslog: {},
output: 'json',
pan_id: 6754,
report: false,
soft_reset_timeout: 0,
timestamp_format: 'YYYY-MM-DD HH:mm:ss',
},
blocklist: [],
@@ -2731,11 +2721,11 @@ describe('Bridge', () => {
it('Should put error in response when format is incorrect', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/config/last_seen', stringify({value_not_good: false}));
MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: false}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/config/last_seen',
stringify({data: {}, status: 'error', error: 'No value given'}),
'zigbee2mqtt/bridge/response/options',
stringify({data: {}, status: 'error', error: 'Invalid payload'}),
{retain: false, qos: 0},
expect.any(Function),
);
@@ -2914,16 +2904,6 @@ describe('Bridge', () => {
expect(settings.get().blocklist).toStrictEqual(['0x123']);
});
it('Should allow to add and remove from availabliltiy blocklist', async () => {
expect(settings.get().blocklist).toStrictEqual([]);
MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {availability_blocklist: ['0x123', '0x1234']}}}));
await flushPromises();
expect(settings.get().advanced.availability_blocklist).toStrictEqual(['0x123', '0x1234']);
MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {availability_blocklist: ['0x123']}}}));
await flushPromises();
expect(settings.get().advanced.availability_blocklist).toStrictEqual(['0x123']);
});
it('Should throw error on removing non-existing device', async () => {
const device = zigbeeHerdsman.devices.bulb;
MQTT.publish.mockClear();
@@ -3426,148 +3406,6 @@ describe('Bridge', () => {
);
});
it('Should allow to enable/disable Home Assistant extension', async () => {
// Test if disabled initially
const device = zigbeeHerdsman.devices.WXKG11LM;
settings.set(['devices', device.ieeeAddr, 'legacy'], false);
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
expect(settings.get().homeassistant).toBeFalsy();
expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function));
// Disable when already disabled should go OK
MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', stringify({value: false}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/config/homeassistant',
stringify({data: {value: false}, status: 'ok'}),
{retain: false, qos: 0},
expect.any(Function),
);
expect(settings.get().homeassistant).toBeFalsy();
// Enable
MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', stringify({value: true}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/config/homeassistant',
stringify({data: {value: true}, status: 'ok'}),
{retain: false, qos: 0},
expect.any(Function),
);
expect(settings.get().homeassistant).toBeTruthy();
MQTT.publish.mockClear();
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function));
// Disable
MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', stringify({value: false}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/config/homeassistant',
stringify({data: {value: false}, status: 'ok'}),
{retain: false, qos: 0},
expect.any(Function),
);
expect(settings.get().homeassistant).toBeFalsy();
MQTT.publish.mockClear();
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function));
});
it('Should fail to set Home Assistant when invalid type', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', 'invalid_one');
await flushPromises();
expect(settings.get().homeassistant).toBeFalsy();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/config/homeassistant',
stringify({data: {}, status: 'error', error: "'invalid_one' is not an allowed value, allowed: true,false"}),
{retain: false, qos: 0},
expect.any(Function),
);
});
it('Should allow to set last_seen', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/config/last_seen', 'ISO_8601');
await flushPromises();
expect(settings.get().advanced.last_seen).toBe('ISO_8601');
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/config/last_seen',
stringify({data: {value: 'ISO_8601'}, status: 'ok'}),
{retain: false, qos: 0},
expect.any(Function),
);
});
it('Should fail to set last_seen when invalid type', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/config/last_seen', 'invalid_one');
await flushPromises();
expect(settings.get().advanced.last_seen).toBe('disable');
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/config/last_seen',
stringify({data: {}, status: 'error', error: "'invalid_one' is not an allowed value, allowed: disable,ISO_8601,epoch,ISO_8601_local"}),
{retain: false, qos: 0},
expect.any(Function),
);
});
it('Should allow to set elapsed', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/config/elapsed', 'true');
await flushPromises();
expect(settings.get().advanced.elapsed).toBe(true);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/config/elapsed',
stringify({data: {value: true}, status: 'ok'}),
{retain: false, qos: 0},
expect.any(Function),
);
});
it('Should fail to set last_seen when invalid type', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/config/elapsed', 'not_valid');
await flushPromises();
expect(settings.get().advanced.elapsed).toBe(false);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/config/elapsed',
stringify({data: {}, status: 'error', error: "'not_valid' is not an allowed value, allowed: true,false"}),
{retain: false, qos: 0},
expect.any(Function),
);
});
it('Should allow to set log level', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/config/log_level', 'debug');
await flushPromises();
expect(logger.getLevel()).toBe('debug');
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), expect.any(Object), expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/config/log_level',
stringify({data: {value: 'debug'}, status: 'ok'}),
{retain: false, qos: 0},
expect.any(Function),
);
});
it('Should fail to set log level when invalid type', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/config/log_level', 'not_valid');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/config/log_level',
stringify({data: {}, status: 'error', error: `'not_valid' is not an allowed value, allowed: ${settings.LOG_LEVELS.join(',')}`}),
{retain: false, qos: 0},
expect.any(Function),
);
});
it('Should allow to touchlink factory reset (succeeds)', async () => {
MQTT.publish.mockClear();
zigbeeHerdsman.touchlinkFactoryResetFirst.mockClear();
-20
View File
@@ -191,26 +191,6 @@ describe('Configure', () => {
);
});
it('Legacy api: Should allow to reconfigure manually', async () => {
mockClear(zigbeeHerdsman.devices.remote);
expectRemoteNotConfigured();
await MQTT.events.message('zigbee2mqtt/bridge/configure', 'remote');
await flushPromises();
expectRemoteConfigured();
});
it('Legacy api: Shouldnt manually reconfigure when device does not exist', async () => {
await MQTT.events.message('zigbee2mqtt/bridge/configure', 'remote_random_non_existing');
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(`Device 'remote_random_non_existing' does not exist`);
});
it('Legacy api: Should skip reconfigure when device does not require this', async () => {
await MQTT.events.message('zigbee2mqtt/bridge/configure', '0x0017882104a44559');
await flushPromises();
expect(logger.warning).toHaveBeenCalledWith(`Skipping configure of 'TS0601_thermostat', device does not require this.`);
});
it('Should not configure when interview not completed', async () => {
const device = zigbeeHerdsman.devices.remote;
delete device.meta.configured;
+46 -51
View File
@@ -94,7 +94,7 @@ describe('Controller', () => {
expect(logger.info).toHaveBeenCalledWith('0x0017880104e45518 (0x0017880104e45518): Not supported (EndDevice)');
expect(MQTT.connect).toHaveBeenCalledTimes(1);
expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', {
will: {payload: Buffer.from('offline'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1},
will: {payload: Buffer.from('{"state":"offline"}'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1},
});
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
@@ -140,7 +140,7 @@ describe('Controller', () => {
await flushPromises();
expect(MQTT.connect).toHaveBeenCalledTimes(1);
const expected = {
will: {payload: Buffer.from('offline'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1},
will: {payload: Buffer.from('{"state":"offline"}'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1},
keepalive: 30,
ca: Buffer.from([99, 97]),
key: Buffer.from([107, 101, 121]),
@@ -403,8 +403,8 @@ describe('Controller', () => {
await zigbeeHerdsman.events.deviceJoined(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_connected', message: {friendly_name: 'bulb'}}),
'zigbee2mqtt/bridge/event',
stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
expect.any(Function),
);
@@ -466,8 +466,8 @@ describe('Controller', () => {
zigbeeHerdsman.events.deviceJoined(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_connected', message: {friendly_name: 'bulb'}}),
'zigbee2mqtt/bridge/event',
stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
expect.any(Function),
);
@@ -480,8 +480,8 @@ describe('Controller', () => {
await zigbeeHerdsman.events.deviceInterview(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'pairing', message: 'interview_started', meta: {friendly_name: 'bulb'}}),
'zigbee2mqtt/bridge/event',
stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'started', ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
expect.any(Function),
);
@@ -494,8 +494,8 @@ describe('Controller', () => {
await zigbeeHerdsman.events.deviceInterview(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'pairing', message: 'interview_failed', meta: {friendly_name: 'bulb'}}),
'zigbee2mqtt/bridge/event',
stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'failed', ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
expect.any(Function),
);
@@ -503,40 +503,46 @@ describe('Controller', () => {
it('On zigbee deviceInterview successful supported', async () => {
await controller.start();
MQTT.publish.mockClear();
const device = zigbeeHerdsman.devices.bulb;
const payload = {device, status: 'successful'};
await zigbeeHerdsman.events.deviceInterview(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({
type: 'pairing',
message: 'interview_successful',
meta: {
friendly_name: 'bulb',
model: 'LED1545G12',
vendor: 'IKEA',
description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm',
supported: true,
},
}),
{retain: false, qos: 0},
expect.any(Function),
);
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event');
const parsedMessage = JSON.parse(MQTT.publish.mock.calls[1][1]);
expect(parsedMessage.type).toStrictEqual('device_interview');
expect(parsedMessage.data.friendly_name).toStrictEqual('bulb');
expect(parsedMessage.data.status).toStrictEqual('successful');
expect(parsedMessage.data.ieee_address).toStrictEqual(device.ieeeAddr);
expect(parsedMessage.data.supported).toStrictEqual(true);
expect(parsedMessage.data.definition.model).toStrictEqual('LED1545G12');
expect(parsedMessage.data.definition.vendor).toStrictEqual('IKEA');
expect(parsedMessage.data.definition.description).toStrictEqual('TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm');
expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array));
expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array));
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0});
});
it('On zigbee deviceInterview successful not supported', async () => {
await controller.start();
MQTT.publish.mockClear();
const device = zigbeeHerdsman.devices.unsupported;
const payload = {device, status: 'successful'};
await zigbeeHerdsman.events.deviceInterview(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'pairing', message: 'interview_successful', meta: {friendly_name: '0x0017880104e45518', supported: false}}),
{retain: false, qos: 0},
expect.any(Function),
);
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event');
const parsedMessage = JSON.parse(MQTT.publish.mock.calls[1][1]);
expect(parsedMessage.type).toStrictEqual('device_interview');
expect(parsedMessage.data.friendly_name).toStrictEqual(device.ieeeAddr);
expect(parsedMessage.data.status).toStrictEqual('successful');
expect(parsedMessage.data.ieee_address).toStrictEqual(device.ieeeAddr);
expect(parsedMessage.data.supported).toStrictEqual(false);
expect(parsedMessage.data.definition.model).toStrictEqual('notSupportedModelID');
expect(parsedMessage.data.definition.vendor).toStrictEqual('notSupportedMfg');
expect(parsedMessage.data.definition.description).toStrictEqual('Automatically generated definition');
expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array));
expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array));
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0});
});
it('On zigbee event device announce', async () => {
@@ -547,8 +553,8 @@ describe('Controller', () => {
await flushPromises();
expect(logger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_announced', message: 'announce', meta: {friendly_name: 'bulb'}}),
'zigbee2mqtt/bridge/event',
stringify({type: 'device_announce', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
expect.any(Function),
);
@@ -564,8 +570,8 @@ describe('Controller', () => {
await zigbeeHerdsman.events.deviceLeave(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_removed', message: 'left_network', meta: {friendly_name: '0x000b57fffec6a5b2'}}),
'zigbee2mqtt/bridge/event',
stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: device.ieeeAddr}}),
{retain: false, qos: 0},
expect.any(Function),
);
@@ -580,8 +586,8 @@ describe('Controller', () => {
await zigbeeHerdsman.events.deviceLeave(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_removed', message: 'left_network', meta: {friendly_name: '0x000b57fffec6a5b2'}}),
'zigbee2mqtt/bridge/event',
stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: 'bulb'}}),
{retain: false, qos: 0},
expect.any(Function),
);
@@ -889,7 +895,7 @@ describe('Controller', () => {
await flushPromises();
expect(MQTT.connect).toHaveBeenCalledTimes(1);
const expected = {
will: {payload: Buffer.from('offline'), retain: false, topic: 'zigbee2mqtt/bridge/state', qos: 1},
will: {payload: Buffer.from('{"state":"offline"}'), retain: false, topic: 'zigbee2mqtt/bridge/state', qos: 1},
};
expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected);
});
@@ -899,7 +905,7 @@ describe('Controller', () => {
MQTT.publish.mockClear();
MQTT.events['connect']();
await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers
expect(MQTT.publish).toHaveBeenCalledTimes(14);
expect(MQTT.publish).toHaveBeenCalledTimes(13);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function));
});
@@ -924,17 +930,6 @@ describe('Controller', () => {
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', {retain: false, qos: 0}, expect.any(Function));
});
it('Should disable legacy options on new network start', async () => {
settings.set(['homeassistant'], true);
settings.reRead();
expect(settings.get().homeassistant.legacy_entity_attributes).toBeTruthy();
expect(settings.get().advanced.legacy_api).toBeTruthy();
zigbeeHerdsman.start.mockReturnValueOnce('reset');
await controller.start();
expect(settings.get().homeassistant.legacy_entity_attributes).toBeFalsy();
expect(settings.get().advanced.legacy_api).toBeFalsy();
});
it('Should publish last seen changes', async () => {
settings.set(['advanced', 'last_seen'], 'epoch');
await controller.start();
+1 -2
View File
@@ -244,8 +244,7 @@ describe('Frontend', () => {
const allTopics = mockWSClient.implementation.send.mock.calls.map((m) => JSON.parse(m).topic);
expect(allTopics).toContain('bridge/devices');
expect(allTopics).toContain('bridge/info');
expect(allTopics).toContain('bridge/config');
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: 'online'}));
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: {state: 'online'}}));
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'remote', payload: {brightness: 255}}));
// Message
-191
View File
@@ -120,197 +120,6 @@ describe('Groups', () => {
]);
});
it('Legacy api: Add to group via MQTT', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = zigbeeHerdsman.groups.group_1;
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}});
expect(group.members.length).toBe(0);
await resetExtension();
MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb_color');
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/1`]);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_group_add', message: {friendly_name: 'bulb_color', group: 'group_1'}}),
{retain: false, qos: 0},
expect.any(Function),
);
});
it('Legacy api: Add to group with slashes via MQTT', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = zigbeeHerdsman.groups['group/with/slashes'];
settings.set(['groups'], {99: {friendly_name: 'group/with/slashes', retain: false, devices: []}});
expect(group.members.length).toBe(0);
await resetExtension();
MQTT.events.message('zigbee2mqtt/bridge/group/group/with/slashes/add', 'bulb_color');
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(settings.getGroup('group/with/slashes').devices).toStrictEqual([`${device.ieeeAddr}/1`]);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_group_add', message: {friendly_name: 'bulb_color', group: 'group/with/slashes'}}),
{retain: false, qos: 0},
expect.any(Function),
);
});
it('Legacy api: Add to group via MQTT with postfix', async () => {
const device = zigbeeHerdsman.devices.QBKG03LM;
const endpoint = device.getEndpoint(3);
const group = zigbeeHerdsman.groups.group_1;
expect(group.members.length).toBe(0);
await resetExtension();
await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'wall_switch_double/right');
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]);
});
it('Legacy api: Add to group via MQTT with postfix shouldnt add it twice', async () => {
const device = zigbeeHerdsman.devices.QBKG03LM;
const endpoint = device.getEndpoint(3);
const group = zigbeeHerdsman.groups.group_1;
expect(group.members.length).toBe(0);
await resetExtension();
await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'wall_switch_double/right');
await flushPromises();
await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', '0x0017880104e45542/3');
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]);
});
it('Legacy api: Remove from group via MQTT', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = zigbeeHerdsman.groups.group_1;
group.members.push(endpoint);
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}});
await resetExtension();
await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'bulb_color');
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(settings.getGroup('group_1').devices).toStrictEqual([]);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_group_remove', message: {friendly_name: 'bulb_color', group: 'group_1'}}),
{retain: false, qos: 0},
expect.any(Function),
);
});
it('Legacy api: Remove from group via MQTT when in zigbee but not in settings', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = zigbeeHerdsman.groups.group_1;
group.members.push(endpoint);
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['dummy']}});
await resetExtension();
await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'bulb_color');
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(settings.getGroup('group_1').devices).toStrictEqual(['dummy']);
});
it('Legacy api: Remove from group via MQTT with postfix variant 1', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = zigbeeHerdsman.groups.group_1;
group.members.push(endpoint);
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/right`]}});
await resetExtension();
await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', '0x0017880104e45542/3');
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(settings.getGroup('group_1').devices).toStrictEqual([]);
});
it('Legacy api: Remove from group via MQTT with postfix variant 2', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = zigbeeHerdsman.groups.group_1;
group.members.push(endpoint);
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`0x0017880104e45542/right`]}});
await resetExtension();
await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'wall_switch_double/3');
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(settings.getGroup('group_1').devices).toStrictEqual([]);
});
it('Legacy api: Remove from group via MQTT with postfix variant 3', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = zigbeeHerdsman.groups.group_1;
group.members.push(endpoint);
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}});
await resetExtension();
await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', '0x0017880104e45542/right');
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(settings.getGroup('group_1').devices).toStrictEqual([]);
});
it('Legacy api: Remove from group all', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = zigbeeHerdsman.groups.group_1;
group.members.push(endpoint);
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}});
await resetExtension();
await MQTT.events.message('zigbee2mqtt/bridge/group/remove_all', '0x0017880104e45542/right');
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(settings.getGroup('group_1').devices).toStrictEqual([]);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_group_remove_all', message: {friendly_name: 'wall_switch_double'}}),
{retain: false, qos: 0},
expect.any(Function),
);
});
it('Remove from group all deprecated', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
const group = zigbeeHerdsman.groups.group_1;
group.members.push(endpoint);
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}});
await resetExtension();
await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove_all', '0x0017880104e45542/right');
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(settings.getGroup('group_1').devices).toStrictEqual([]);
});
it('Legacy api: Log when adding to non-existing group', async () => {
await resetExtension();
logger.error.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/group/group_1_not_existing/add', 'bulb_color');
await flushPromises();
expect(logger.error).toHaveBeenCalledWith("Group 'group_1_not_existing' does not exist");
});
it('Legacy api: Log when adding a non-existing device', async () => {
await resetExtension();
logger.error.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb_color_not_existing');
await flushPromises();
expect(logger.error).toHaveBeenCalledWith("Device 'bulb_color_not_existing' does not exist");
});
it('Legacy api: Log when adding a non-existing endpoint', async () => {
await resetExtension();
logger.error.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb_color/not_existing_endpoint');
await flushPromises();
expect(logger.error).toHaveBeenCalledWith("Device 'bulb_color' does not have endpoint 'not_existing_endpoint'");
});
it('Should publish group state change when a device in it changes state', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
+69 -58
View File
@@ -95,7 +95,7 @@ describe('HomeAssistant extension', () => {
let payload;
payload = {
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
brightness: true,
brightness_scale: 254,
command_topic: 'zigbee2mqtt/ha_discovery_group/set',
@@ -140,7 +140,7 @@ describe('HomeAssistant extension', () => {
);
payload = {
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
command_topic: 'zigbee2mqtt/ha_discovery_group/set',
device: {
identifiers: ['zigbee2mqtt_1221051039810110150109113116116_9'],
@@ -186,7 +186,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
enabled_by_default: true,
};
@@ -216,7 +216,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -245,7 +245,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -275,7 +275,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -306,7 +306,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -317,7 +317,7 @@ describe('HomeAssistant extension', () => {
);
payload = {
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
command_topic: 'zigbee2mqtt/wall_switch_double/left/set',
device: {
identifiers: ['zigbee2mqtt_0x0017880104e45542'],
@@ -346,7 +346,7 @@ describe('HomeAssistant extension', () => {
);
payload = {
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
command_topic: 'zigbee2mqtt/wall_switch_double/right/set',
device: {
identifiers: ['zigbee2mqtt_0x0017880104e45542'],
@@ -375,7 +375,7 @@ describe('HomeAssistant extension', () => {
);
payload = {
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
brightness: true,
brightness_scale: 254,
supported_color_modes: ['color_temp'],
@@ -482,7 +482,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
});
const topic2 = 'homeassistant/device_automation/0x0017880104e45522/action_double/config';
const payload2 = stringify({
@@ -553,7 +553,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -582,7 +582,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -611,7 +611,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -670,7 +670,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'From Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
expire_after: 90,
icon: 'mdi:test',
};
@@ -699,7 +699,7 @@ describe('HomeAssistant extension', () => {
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
origin: origin,
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
expire_after: 30,
icon: 'mdi:test',
object_id: 'weather_sensor_humidity',
@@ -745,7 +745,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
enabled_by_default: true,
};
@@ -775,7 +775,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -807,7 +807,7 @@ describe('HomeAssistant extension', () => {
await flushPromises();
payload = {
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
command_topic: 'zigbee2mqtt/my_switch/set',
device: {
identifiers: ['zigbee2mqtt_0x0017880104e45541'],
@@ -896,7 +896,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Hampton Bay',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -915,6 +915,7 @@ describe('HomeAssistant extension', () => {
availability: [
{
topic: 'zigbee2mqtt/bridge/state',
value_template: '{{ value_json.state }}',
},
],
current_temperature_template: '{{ value_json.local_temperature }}',
@@ -962,7 +963,7 @@ describe('HomeAssistant extension', () => {
action_template:
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
action_topic: 'zigbee2mqtt/bosch_radiator',
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
current_temperature_template: '{{ value_json.local_temperature }}',
current_temperature_topic: 'zigbee2mqtt/bosch_radiator',
device: {
@@ -1028,7 +1029,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Keen Home',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -1039,7 +1040,7 @@ describe('HomeAssistant extension', () => {
);
payload = {
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
command_topic: 'zigbee2mqtt/zigfred_plus/l6/set',
device: {
identifiers: ['zigbee2mqtt_0xf4ce368a38be56a1'],
@@ -1101,7 +1102,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -1280,7 +1281,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -1449,7 +1450,10 @@ describe('HomeAssistant extension', () => {
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability_mode: 'all',
availability: [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}],
availability: [
{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'},
{topic: 'zigbee2mqtt/weather_sensor/availability', value_template: '{{ value_json.state }}'},
],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -1462,7 +1466,7 @@ describe('HomeAssistant extension', () => {
it('Should clear discovery when device is removed', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'weather_sensor');
MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', 'weather_sensor');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -1544,7 +1548,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -1595,7 +1599,7 @@ describe('HomeAssistant extension', () => {
await flushPromises();
const payload = {
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
brightness: true,
brightness_scale: 254,
command_topic: 'zigbee2mqtt/ha_discovery_group_new/set',
@@ -1681,7 +1685,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -1712,7 +1716,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'IKEA',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
device_class: 'update',
entity_category: 'diagnostic',
};
@@ -2037,10 +2041,13 @@ describe('HomeAssistant extension', () => {
it('Shouldnt crash in onPublishEntityState on group publish', async () => {
logger.error.mockClear();
MQTT.publish.mockClear();
const group = zigbeeHerdsman.groups.group_1;
group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1));
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
expect(logger.error).toHaveBeenCalledTimes(0);
group.members.pop();
});
it('Should counter an action payload with an empty payload', async () => {
@@ -2083,7 +2090,7 @@ describe('HomeAssistant extension', () => {
MQTT.publish.mockClear();
await MQTT.events.message(
'homeassistant/light/1221051039810110150109113116116_91231/light/config',
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}),
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}),
);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
@@ -2098,14 +2105,17 @@ describe('HomeAssistant extension', () => {
MQTT.publish.mockClear();
await MQTT.events.message(
'homeassistant/light/1221051039810110150109113116116_9/light/config',
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}),
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}),
);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(0);
// Existing group with old topic structure (1.20.0) -> clear
MQTT.publish.mockClear();
await MQTT.events.message('homeassistant/light/9/light/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}));
await MQTT.events.message(
'homeassistant/light/9/light/config',
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}),
);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/light/9/light/config', '', {qos: 1, retain: true}, expect.any(Function));
@@ -2114,7 +2124,7 @@ describe('HomeAssistant extension', () => {
MQTT.publish.mockClear();
await MQTT.events.message(
'homeassistant/light/1221051039810110150109113116116_9/switch/config',
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}),
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}),
);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
@@ -2127,7 +2137,10 @@ describe('HomeAssistant extension', () => {
// Non-existing device -> clear
MQTT.publish.mockClear();
await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}));
await MQTT.events.message(
'homeassistant/sensor/0x123/temperature/config',
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}),
);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/sensor/0x123/temperature/config', '', {qos: 1, retain: true}, expect.any(Function));
@@ -2136,7 +2149,7 @@ describe('HomeAssistant extension', () => {
MQTT.publish.mockClear();
await MQTT.events.message(
'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config',
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}),
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}),
);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(0);
@@ -2154,7 +2167,7 @@ describe('HomeAssistant extension', () => {
MQTT.publish.mockClear();
await MQTT.events.message(
'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config',
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}),
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}),
);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
@@ -2196,7 +2209,7 @@ describe('HomeAssistant extension', () => {
await MQTT.events.message(
'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config',
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}),
stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}),
);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -2244,7 +2257,7 @@ describe('HomeAssistant extension', () => {
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -2265,7 +2278,7 @@ describe('HomeAssistant extension', () => {
await flushPromises();
const payload = {
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
brightness: true,
brightness_scale: 254,
command_topic: 'zigbee2mqtt/ha_discovery_group/set',
@@ -2311,9 +2324,6 @@ describe('HomeAssistant extension', () => {
});
it('Should discover with json availability payload value_template', async () => {
settings.set(['advanced', 'legacy_availability_payload'], false);
await resetExtension();
const payload = {
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
brightness: true,
@@ -2413,6 +2423,7 @@ describe('HomeAssistant extension', () => {
availability: [
{
topic: 'zigbee2mqtt/bridge/state',
value_template: '{{ value_json.state }}',
},
],
device: {
@@ -2472,7 +2483,7 @@ describe('HomeAssistant extension', () => {
configuration_url: 'http://zigbee.mqtt/#/device/0x0017880104e45522/info',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -2518,7 +2529,7 @@ describe('HomeAssistant extension', () => {
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
origin: origin,
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
`homeassistant/scene/0x000b57fffec6a5b4/scene_1/config`,
@@ -2562,7 +2573,7 @@ describe('HomeAssistant extension', () => {
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
origin: origin,
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
};
expect(MQTT.publish).toHaveBeenCalledWith(
`homeassistant/scene/1221051039810110150109113116116_9/scene_4/config`,
@@ -2593,7 +2604,7 @@ describe('HomeAssistant extension', () => {
hw_version: 'z-Stack 20190425',
sw_version: z2m_version,
},
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
availability_mode: 'all',
};
@@ -2628,7 +2639,7 @@ describe('HomeAssistant extension', () => {
device_class: 'connectivity',
unique_id: 'bridge_0x00124b00120144ae_connection_state_zigbee2mqtt',
state_topic: 'zigbee2mqtt/bridge/state',
value_template: '{{ value }}',
value_template: '{{ value_json.state }}',
payload_on: 'online',
payload_off: 'offline',
origin: origin,
@@ -2654,7 +2665,7 @@ describe('HomeAssistant extension', () => {
payload_off: false,
origin: origin,
device: devicePayload,
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
availability_mode: 'all',
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -2674,7 +2685,7 @@ describe('HomeAssistant extension', () => {
payload_press: '',
origin: origin,
device: devicePayload,
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
availability_mode: 'all',
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -2697,7 +2708,7 @@ describe('HomeAssistant extension', () => {
options: settings.LOG_LEVELS,
origin: origin,
device: devicePayload,
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
availability_mode: 'all',
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -2718,7 +2729,7 @@ describe('HomeAssistant extension', () => {
value_template: '{{ value_json.version }}',
origin: origin,
device: devicePayload,
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
availability_mode: 'all',
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -2739,7 +2750,7 @@ describe('HomeAssistant extension', () => {
value_template: '{{ value_json.coordinator.meta.revision }}',
origin: origin,
device: devicePayload,
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
availability_mode: 'all',
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -2761,7 +2772,7 @@ describe('HomeAssistant extension', () => {
json_attributes_template: '{{ value_json.data.value | tojson }}',
origin: origin,
device: devicePayload,
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
availability_mode: 'all',
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -2782,7 +2793,7 @@ describe('HomeAssistant extension', () => {
value_template: '{{ iif(value_json.permit_join_timeout is defined, value_json.permit_join_timeout, None) }}',
origin: origin,
device: devicePayload,
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
availability_mode: 'all',
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -2806,7 +2817,7 @@ describe('HomeAssistant extension', () => {
payload_off: 'false',
origin: origin,
device: devicePayload,
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
availability_mode: 'all',
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -2842,7 +2853,7 @@ describe('HomeAssistant extension', () => {
await zigbeeHerdsman.events.message(msg);
await flushPromises();
const payload = {
availability: [{topic: 'zigbee2mqtt/bridge/state'}],
availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}],
command_topic: 'zigbee2mqtt/0x18fc26000000cafe/set/device_mode',
device: {
identifiers: ['zigbee2mqtt_0x18fc26000000cafe'],
-538
View File
@@ -1,538 +0,0 @@
const data = require('../stub/data');
const logger = require('../stub/logger');
const zigbeeHerdsman = require('../stub/zigbeeHerdsman');
const MQTT = require('../stub/mqtt');
const stringify = require('json-stable-stringify-without-jsonify');
const path = require('path');
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
const settings = require('../../lib/util/settings');
const Controller = require('../../lib/controller');
const flushPromises = require('../lib/flushPromises');
describe('Bridge legacy', () => {
let controller;
let version;
beforeAll(async () => {
jest.useFakeTimers();
version = await require('../../lib/util/utils').default.getZigbee2MQTTVersion();
controller = new Controller(jest.fn(), jest.fn());
await controller.start();
});
beforeEach(() => {
data.writeDefaultConfiguration();
settings.reRead();
logger.info.mockClear();
logger.warning.mockClear();
});
afterAll(async () => {
jest.useRealTimers();
});
it('Should publish bridge configuration on startup', async () => {
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/config',
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),
);
});
it('Should allow to set elapsed', async () => {
MQTT.events.message('zigbee2mqtt/bridge/config/elapsed', 'true');
await flushPromises();
expect(settings.get().advanced.elapsed).toBe(true);
MQTT.events.message('zigbee2mqtt/bridge/config/elapsed', 'false');
await flushPromises();
expect(settings.get().advanced.elapsed).toBe(false);
MQTT.events.message('zigbee2mqtt/bridge/config/elapsed', 'wrong');
await flushPromises();
expect(settings.get().advanced.elapsed).toBe(false);
});
it('Should allow whitelist', async () => {
const bulb_color = zigbeeHerdsman.devices.bulb_color;
const bulb = zigbeeHerdsman.devices.bulb;
expect(settings.get().passlist).toStrictEqual([]);
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb_color');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_whitelisted', message: {friendly_name: 'bulb_color'}}),
{retain: false, qos: 0},
expect.any(Function),
);
MQTT.publish.mockClear();
expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr]);
MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_whitelisted', message: {friendly_name: 'bulb'}}),
{retain: false, qos: 0},
expect.any(Function),
);
MQTT.publish.mockClear();
expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]);
MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb');
await flushPromises();
expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]);
expect(MQTT.publish).toHaveBeenCalledTimes(0);
});
it('Should allow changing device options', async () => {
const bulb_color = zigbeeHerdsman.devices.bulb_color;
expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false});
MQTT.events.message('zigbee2mqtt/bridge/config/device_options', stringify({friendly_name: 'bulb_color', options: {retain: true}}));
await flushPromises();
expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: true});
MQTT.events.message('zigbee2mqtt/bridge/config/device_options', stringify({friendly_name: 'bulb_color', optionswrong: {retain: true}}));
await flushPromises();
expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: true});
MQTT.events.message('zigbee2mqtt/bridge/config/device_options', "{friendly_name: 'bulb_color'malformed: {retain: true}}");
await flushPromises();
expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: true});
MQTT.events.message('zigbee2mqtt/bridge/config/device_options', stringify({friendly_name: 'bulb_color', options: {random_setting: true}}));
await flushPromises();
expect(settings.getDevice('bulb_color')).toStrictEqual({
ID: '0x000b57fffec6a5b3',
friendly_name: 'bulb_color',
random_setting: true,
retain: true,
});
MQTT.events.message(
'zigbee2mqtt/bridge/config/device_options',
stringify({friendly_name: 'bulb_color', options: {options: {random_1: true}}}),
);
await flushPromises();
expect(settings.getDevice('bulb_color')).toStrictEqual({
ID: '0x000b57fffec6a5b3',
friendly_name: 'bulb_color',
random_setting: true,
retain: true,
options: {random_1: true},
});
MQTT.events.message(
'zigbee2mqtt/bridge/config/device_options',
stringify({friendly_name: 'bulb_color', options: {options: {random_2: false}}}),
);
await flushPromises();
expect(settings.getDevice('bulb_color')).toStrictEqual({
ID: '0x000b57fffec6a5b3',
friendly_name: 'bulb_color',
random_setting: true,
retain: true,
options: {random_1: true, random_2: false},
});
});
it('Should allow permit join', async () => {
zigbeeHerdsman.permitJoin.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/permit_join', 'true');
await flushPromises();
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined);
zigbeeHerdsman.permitJoin.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/permit_join', 'false');
await flushPromises();
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined);
});
it('Should allow to reset', async () => {
zigbeeHerdsman.reset.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/reset', '');
await flushPromises();
expect(zigbeeHerdsman.reset).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.reset).toHaveBeenCalledWith('soft');
zigbeeHerdsman.reset.mockImplementationOnce(() => {
throw new Error('');
});
MQTT.events.message('zigbee2mqtt/bridge/config/reset', '');
await flushPromises();
expect(zigbeeHerdsman.reset).toHaveBeenCalledTimes(2);
expect(zigbeeHerdsman.reset.mock.calls[1][0]).toBe('soft');
});
it('Should allow to set last_seen', async () => {
MQTT.events.message('zigbee2mqtt/bridge/config/last_seen', 'ISO_8601');
await flushPromises();
expect(settings.get().advanced.last_seen).toBe('ISO_8601');
MQTT.events.message('zigbee2mqtt/bridge/config/last_seen', 'disable');
await flushPromises();
expect(settings.get().advanced.last_seen).toBe('disable');
MQTT.events.message('zigbee2mqtt/bridge/config/last_seen', 'notvalid');
await flushPromises();
expect(settings.get().advanced.last_seen).toBe('disable');
});
it('Should allow to set log_level', async () => {
MQTT.events.message('zigbee2mqtt/bridge/config/log_level', 'debug');
await flushPromises();
expect(logger.getLevel()).toBe('debug');
MQTT.events.message('zigbee2mqtt/bridge/config/log_level', 'error');
await flushPromises();
expect(logger.getLevel()).toBe('error');
MQTT.events.message('zigbee2mqtt/bridge/config/log_level', 'notvalid');
await flushPromises();
expect(logger.getLevel()).toBe('error');
});
it('Should allow to get devices', async () => {
const now = Date.now;
Date.now = () => 100;
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/devices/get', '');
await flushPromises();
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/config/devices');
const payload = JSON.parse(MQTT.publish.mock.calls[0][1]);
expect(payload.length).toStrictEqual(Object.values(zigbeeHerdsman.devices).length);
expect(payload[1]).toStrictEqual({
ieeeAddr: '0x00124b00120144ae',
type: 'Coordinator',
dateCode: '20190425',
friendly_name: 'Coordinator',
networkAddress: 0,
softwareBuildID: 'z-Stack',
lastSeen: 100,
});
expect(payload[2]).toStrictEqual({
dateCode: null,
friendly_name: 'bulb',
ieeeAddr: '0x000b57fffec6a5b2',
lastSeen: 1000,
manufacturerID: 4476,
model: 'LED1545G12',
modelID: 'TRADFRI bulb E27 WS opal 980lm',
networkAddress: 40369,
powerSource: 'Mains (single phase)',
softwareBuildID: null,
type: 'Router',
description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm',
vendor: 'IKEA',
});
Date.now = now;
});
it('Should allow to get groups', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/groups', '');
await flushPromises();
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
const payload = JSON.parse(MQTT.publish.mock.calls[0][1]);
expect(payload).toStrictEqual({
message: [
{ID: 1, devices: [], friendly_name: 'group_1', retain: false},
{ID: 2, devices: [], friendly_name: 'group_2', retain: false},
{ID: 9, devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], friendly_name: 'ha_discovery_group'},
{ID: 11, devices: ['bulb_2'], friendly_name: 'group_with_tradfri', retain: false},
{ID: 12, devices: ['TS0601_thermostat'], friendly_name: 'thermostat_group', retain: false},
{ID: 14, devices: ['power_plug', 'bulb_2'], friendly_name: 'switch_group', retain: false},
{ID: 21, devices: ['GLEDOPTO_2ID/cct'], friendly_name: 'gledopto_group'},
{ID: 15071, devices: ['bulb_color_2', 'bulb_2'], friendly_name: 'group_tradfri_remote', retain: false},
],
type: 'groups',
});
});
it('Should allow rename devices', async () => {
const bulb_color2 = {ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color2', retain: false};
MQTT.publish.mockClear();
expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false});
MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'bulb_color', new: 'bulb_color2'}));
await flushPromises();
expect(settings.getDevice('bulb_color')).toBeUndefined();
expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_renamed', message: {from: 'bulb_color', to: 'bulb_color2'}}),
{qos: 0, retain: false},
expect.any(Function),
);
MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'bulb_color2', newmalformed: 'bulb_color3'}));
await flushPromises();
expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2);
MQTT.events.message('zigbee2mqtt/bridge/config/rename', "{old: 'bulb_color2'newmalformed: 'bulb_color3'}");
await flushPromises();
expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2);
MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'bulb_color', new: 'bulb_color3'}));
await flushPromises();
expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2);
});
it('Should allow rename groups', async () => {
MQTT.publish.mockClear();
expect(settings.getGroup(1)).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: false});
MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'group_1', new: 'group_1_renamed'}));
await flushPromises();
expect(settings.getGroup(1)).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1_renamed', retain: false});
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'group_renamed', message: {from: 'group_1', to: 'group_1_renamed'}}),
{qos: 0, retain: false},
expect.any(Function),
);
});
it('Should allow to rename last joined device', async () => {
const device = zigbeeHerdsman.devices.bulb;
const payload = {device};
await zigbeeHerdsman.events.deviceJoined(payload);
await flushPromises();
expect(settings.getDevice('0x000b57fffec6a5b2').friendly_name).toStrictEqual('bulb');
MQTT.events.message('zigbee2mqtt/bridge/config/rename_last', 'bulb_new_name');
await flushPromises();
expect(settings.getDevice('0x000b57fffec6a5b2').friendly_name).toStrictEqual('bulb_new_name');
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_renamed', message: {from: 'bulb', to: 'bulb_new_name'}}),
{qos: 0, retain: false},
expect.any(Function),
);
});
it('Shouldnt rename when no device has been joined', async () => {
controller = new Controller(jest.fn(), jest.fn());
await controller.start();
await flushPromises();
expect(settings.getDevice('0x000b57fffec6a5b2').friendly_name).toStrictEqual('bulb');
MQTT.events.message('zigbee2mqtt/bridge/config/rename_last', 'bulb_new_name');
await flushPromises();
expect(settings.getDevice('0x000b57fffec6a5b2').friendly_name).toStrictEqual('bulb');
});
it('Should allow to add groups', async () => {
zigbeeHerdsman.createGroup.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/add_group', 'new_group');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'group_added', message: 'new_group'}),
{qos: 0, retain: false},
expect.any(Function),
);
expect(settings.getGroup('new_group')).toStrictEqual({ID: 3, friendly_name: 'new_group', devices: []});
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3);
});
it('Should allow to add groups with json', async () => {
zigbeeHerdsman.createGroup.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/add_group', '{"friendly_name": "new_group"}');
await flushPromises();
expect(settings.getGroup('new_group')).toStrictEqual({ID: 3, friendly_name: 'new_group', devices: []});
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'group_added', message: 'new_group'}),
{qos: 0, retain: false},
expect.any(Function),
);
});
it('Should allow to add groups with json specifying id', async () => {
zigbeeHerdsman.createGroup.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/add_group', '{"friendly_name": "new_group", "id": 42}');
await flushPromises();
expect(settings.getGroup('new_group')).toStrictEqual({ID: 42, friendly_name: 'new_group', devices: []});
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(42);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'group_added', message: 'new_group'}),
{qos: 0, retain: false},
expect.any(Function),
);
});
it('Should allow to add groups with json specifying only id', async () => {
zigbeeHerdsman.createGroup.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/add_group', '{"id": 42}');
await flushPromises();
expect(settings.getGroup('group_42')).toStrictEqual({ID: 42, friendly_name: 'group_42', devices: []});
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(42);
});
it('Should allow to remove groups', async () => {
const group = zigbeeHerdsman.groups.group_1;
MQTT.events.message('zigbee2mqtt/bridge/config/remove_group', 'group_1');
await flushPromises();
expect(settings.getGroup('to_be_removed')).toBeUndefined();
expect(group.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'group_removed', message: 'group_1'}),
{qos: 0, retain: false},
expect.any(Function),
);
});
it('Should allow to force remove groups', async () => {
const group = zigbeeHerdsman.groups.group_1;
MQTT.events.message('zigbee2mqtt/bridge/config/force_remove_group', 'group_1');
await flushPromises();
expect(settings.getGroup('to_be_removed')).toBeUndefined();
expect(group.removeFromDatabase).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'group_removed', message: 'group_1'}),
{qos: 0, retain: false},
expect.any(Function),
);
});
it('Shouldnt allow add groups without id or friendly_name in json', async () => {
zigbeeHerdsman.createGroup.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/add_group', '{}');
await flushPromises();
expect(logger.error).toHaveBeenCalledWith('Failed to add group, missing friendly_name!');
});
it('Shouldnt do anything on unsupported topic', async () => {
await flushPromises();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/not_supported', 'to_be_removed');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(0);
});
it('Should allow to remove device', async () => {
controller.state.state = {'0x000b57fffec6a5b3': {brightness: 100}};
const device = zigbeeHerdsman.devices.bulb_color;
device.removeFromNetwork.mockClear();
expect(settings.get().blocklist.length).toBe(0);
await flushPromises();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'bulb_color');
await flushPromises();
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(controller.state[device.ieeeAddr]).toBeUndefined();
expect(settings.getDevice('bulb_color')).toBeUndefined();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_removed', message: 'bulb_color'}),
{qos: 0, retain: false},
expect.any(Function),
);
expect(controller.state.state).toStrictEqual({});
expect(settings.get().blocklist.length).toBe(0);
});
it('Should allow to force remove device', async () => {
controller.state.state = {'0x000b57fffec6a5b3': {brightness: 100}};
const device = zigbeeHerdsman.devices.bulb_color;
device.removeFromDatabase.mockClear();
expect(settings.get().blocklist.length).toBe(0);
await flushPromises();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/force_remove', 'bulb_color');
await flushPromises();
expect(device.removeFromDatabase).toHaveBeenCalledTimes(1);
expect(controller.state[device.ieeeAddr]).toBeUndefined();
expect(settings.getDevice('bulb_color')).toBeUndefined();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_force_removed', message: 'bulb_color'}),
{qos: 0, retain: false},
expect.any(Function),
);
expect(controller.state.state).toStrictEqual({});
expect(settings.get().blocklist.length).toBe(0);
});
it('Should allow to block device', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
device.removeFromNetwork.mockClear();
expect(settings.get().blocklist.length).toBe(0);
await flushPromises();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/ban', 'bulb_color');
await flushPromises();
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(controller.state[device.ieeeAddr]).toBeUndefined();
expect(settings.getDevice('bulb_color')).toBeUndefined();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/log',
stringify({type: 'device_banned', message: 'bulb_color'}),
{qos: 0, retain: false},
expect.any(Function),
);
expect(settings.get().blocklist).toStrictEqual(['0x000b57fffec6a5b3']);
});
it('Shouldnt crash when removing non-existing device', async () => {
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'not_existing_123');
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(`Cannot remove, device 'not_existing_123' does not exist`);
});
it('Should handle when remove fails', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
device.removeFromNetwork.mockClear();
device.removeFromNetwork.mockImplementationOnce(() => {
throw new Error('');
});
await flushPromises();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'bulb_color');
await flushPromises();
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
});
it('Should handle when ban fails', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
device.removeFromNetwork.mockClear();
device.removeFromNetwork.mockImplementationOnce(() => {
throw new Error('');
});
await flushPromises();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/config/ban', 'bulb_color');
await flushPromises();
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
});
it('Should allow to touchlink factory reset (OK)', async () => {
zigbeeHerdsman.touchlinkFactoryResetFirst.mockClear();
zigbeeHerdsman.touchlinkFactoryResetFirst.mockReturnValueOnce(true);
MQTT.events.message('zigbee2mqtt/bridge/config/touchlink/factory_reset', '');
await flushPromises();
expect(zigbeeHerdsman.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1);
expect(logger.info).toHaveBeenCalledWith('Successfully factory reset device through Touchlink');
});
it('Should allow to touchlink factory reset (FAILS)', async () => {
zigbeeHerdsman.touchlinkFactoryResetFirst.mockClear();
zigbeeHerdsman.touchlinkFactoryResetFirst.mockReturnValueOnce(false);
MQTT.events.message('zigbee2mqtt/bridge/config/touchlink/factory_reset', '');
await flushPromises();
expect(zigbeeHerdsman.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1);
expect(logger.warning).toHaveBeenCalledWith('Failed to factory reset device through Touchlink');
});
});
-315
View File
@@ -1,315 +0,0 @@
const data = require('../stub/data');
const logger = require('../stub/logger');
const zigbeeHerdsman = require('../stub/zigbeeHerdsman');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3');
zigbeeHerdsman.returnDevices.push('0x00124b00120144ae');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b2');
zigbeeHerdsman.returnDevices.push('0x0017880104e45553');
zigbeeHerdsman.returnDevices.push('0x0017880104e45559');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b4');
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b7');
zigbeeHerdsman.returnDevices.push('0x0017880104e45524');
zigbeeHerdsman.returnDevices.push('0x90fd9ffffe4b64ax');
const MQTT = require('../stub/mqtt');
const settings = require('../../lib/util/settings');
const Controller = require('../../lib/controller');
const flushPromises = require('../lib/flushPromises');
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const mocksClear = [MQTT.publish, logger.warning, logger.debug];
describe('Report', () => {
let controller;
let extension;
function expectOnOffBrightnessColorReport(endpoint, colorXY) {
const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
const device = endpoint.getDevice();
expect(device.meta.reporting).toBe(1);
expect(endpoint.unbind).toHaveBeenCalledTimes(0);
expect(endpoint.bind).toHaveBeenCalledTimes(3);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint);
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint);
expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(3);
expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [
{attribute: 'onOff', maximumReportInterval: 300, minimumReportInterval: 0, reportableChange: 0},
]);
expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [
{attribute: 'currentLevel', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1},
]);
if (colorXY) {
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [
{attribute: 'colorTemperature', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1},
{attribute: 'currentX', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1},
{attribute: 'currentY', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1},
]);
} else {
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [
{attribute: 'colorTemperature', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1},
]);
}
}
function expectOnOffBrightnessColorReportDisabled(endpoint, colorXY) {
const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
const device = endpoint.getDevice();
expect(device.meta.reporting).toBe(undefined);
expect(endpoint.unbind).toHaveBeenCalledTimes(3);
expect(endpoint.bind).toHaveBeenCalledTimes(0);
expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint);
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint);
expect(endpoint.unbind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(3);
expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [
{attribute: 'onOff', maximumReportInterval: 0xffff, minimumReportInterval: 0, reportableChange: 0},
]);
expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [
{attribute: 'currentLevel', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1},
]);
if (colorXY) {
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [
{attribute: 'colorTemperature', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1},
{attribute: 'currentX', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1},
{attribute: 'currentY', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1},
]);
} else {
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [
{attribute: 'colorTemperature', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1},
]);
}
}
const mockClear = (device) => {
for (const endpoint of device.endpoints) {
endpoint.read.mockClear();
endpoint.write.mockClear();
endpoint.configureReporting.mockClear();
endpoint.bind.mockClear();
endpoint.unbind.mockClear();
}
};
beforeAll(async () => {
jest.useFakeTimers();
settings.set(['advanced', 'report'], true);
controller = new Controller(jest.fn(), jest.fn());
await controller.start();
extension = controller.extensions.find((e) => e.constructor.name === 'Report');
});
beforeEach(async () => {
extension.enabled = true;
data.writeDefaultConfiguration();
settings.reRead();
for (const device of Object.values(zigbeeHerdsman.devices)) {
mockClear(device);
delete device.meta.reporting;
}
mocksClear.forEach((m) => m.mockClear());
extension.queue = new Set();
extension.failed = new Set();
await extension.start();
});
afterAll(async () => {
jest.useRealTimers();
});
it('Should configure reporting on startup', async () => {
await extension.start();
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
expectOnOffBrightnessColorReport(endpoint, true);
});
it('Should not configure reporting on startup when disabled', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
const endpoint = device.getEndpoint(1);
mockClear(device);
delete device.meta.report;
extension.enabled = false;
await extension.start();
expect(device.meta.reporting).toBe(undefined);
expect(endpoint.bind).toHaveBeenCalledTimes(0);
});
it('Should disable reporting on startup when enabled earlier', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
device.meta.reporting = 1;
const endpoint = device.getEndpoint(1);
extension.enabled = false;
mockClear(device);
await extension.start();
expectOnOffBrightnessColorReportDisabled(endpoint, true);
});
it('Should configure reporting when receicing message from device which has not been setup yet', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.getEndpoint(1);
device.save.mockClear();
mockClear(device);
delete device.meta.reporting;
const data = {onOff: 1};
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expectOnOffBrightnessColorReport(endpoint, false);
expect(device.save).toHaveBeenCalledTimes(1);
});
it('Should not configure reporting when still configuring', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.getEndpoint(1);
endpoint.bind.mockImplementationOnce(async () => await wait(1000));
delete device.meta.reporting;
mockClear(device);
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
});
it('Should not mark as configured when reporting setup fails', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.getEndpoint(1);
endpoint.bind.mockImplementationOnce(async () => {
throw new Error('failed');
});
delete device.meta.reporting;
mockClear(device);
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
expect(device.meta.reporting).toBeUndefined();
});
it('Should not configure reporting when interviewing', async () => {
const device = zigbeeHerdsman.devices.bulb_2;
const endpoint = device.getEndpoint(1);
device.interviewing = true;
delete device.meta.reporting;
mockClear(device);
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(0);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(0);
expect(device.meta.reporting).toBeUndefined();
});
it('Should not configure reporting when receicing message from device which has already been setup yet', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.getEndpoint(1);
mockClear(device);
const data = {onOff: 1};
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(0);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(0);
});
it('Should not configure reporting for end devices', async () => {
const device = zigbeeHerdsman.devices.E11_G13;
const endpoint = device.getEndpoint(1);
expect(endpoint.bind).toHaveBeenCalledTimes(0);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(0);
});
it('Should configure reporting when deviceAnnounce message from IKEA device', async () => {
const device = zigbeeHerdsman.devices.bulb_2;
const endpoint = device.getEndpoint(1);
mockClear(device);
const payload = {device};
await zigbeeHerdsman.events.deviceAnnounce(payload);
await flushPromises();
expectOnOffBrightnessColorReport(endpoint, false);
});
it('Should not configure reporting on device leave', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.getEndpoint(1);
delete device.meta.reporting;
mockClear(device);
await zigbeeHerdsman.events.deviceLeave({ieeeAddr: device.ieeeAddr});
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(0);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(0);
});
it('Should not configure reporting for the CC2530 router', async () => {
const device = zigbeeHerdsman.devices.CC2530_ROUTER;
const endpoint = device.getEndpoint(1);
delete device.meta.reporting;
mockClear(device);
await zigbeeHerdsman.events.deviceLeave({ieeeAddr: device.ieeeAddr});
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(0);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(0);
});
it('Should not configure reporting again when it already failed once', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.getEndpoint(1);
endpoint.bind.mockImplementationOnce(async () => {
throw new Error('failed');
});
delete device.meta.reporting;
mockClear(device);
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
});
it('Should not configure reporting for the ZNLDP12LM closuresWindowCovering as it is ignored', async () => {
const device = zigbeeHerdsman.devices.ZNLDP12LM;
const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
const endpoint = device.getEndpoint(1);
delete device.meta.reporting;
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(3);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint);
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint);
expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(3);
});
it('Should not setup colorTemperature reporting when bulb does not support it and should read colorCapabilities when its not there yet ', async () => {
const device = zigbeeHerdsman.devices.bulb;
const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
const endpoint = device.getEndpoint(1);
const configuredReportings = endpoint.configuredReportings;
endpoint.configuredReportings = [];
delete device.meta.reporting;
mockClear(device);
endpoint.getClusterAttributeValue = jest.fn();
let count = 0;
endpoint.getClusterAttributeValue.mockImplementation((d) => {
count++;
if (count === 1) return undefined;
return 17;
});
const payload = {device};
await zigbeeHerdsman.events.deviceAnnounce(payload);
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(3);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint);
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint);
expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint);
expect(endpoint.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']);
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [
{attribute: 'colorTemperature', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1},
]);
expect(endpoint.configureReporting).toHaveBeenCalledTimes(3);
endpoint.configuredReportings = configuredReportings;
});
});
+142 -343
View File
@@ -102,350 +102,8 @@ describe('Networkmap', () => {
unsupported_router.routingTable = jest.fn().mockRejectedValue(new Error('failed'));
}
it('Output raw networkmap legacy api', async () => {
it('Output raw networkmap', async () => {
mock();
MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'raw');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
let call = MQTT.publish.mock.calls[0];
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/raw');
const expected = {
links: [
{
depth: 1,
linkquality: 120,
lqi: 120,
relationship: 2,
routes: [],
source: {ieeeAddr: '0x000b57fffec6a5b3', networkAddress: 40399},
sourceIeeeAddr: '0x000b57fffec6a5b3',
sourceNwkAddr: 40399,
target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0},
targetIeeeAddr: '0x00124b00120144ae',
},
{
depth: 1,
linkquality: 92,
lqi: 92,
relationship: 2,
routes: [{destinationAddress: 6540, nextHop: 40369, status: 'ACTIVE'}],
source: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369},
sourceIeeeAddr: '0x000b57fffec6a5b2',
sourceNwkAddr: 40369,
target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0},
targetIeeeAddr: '0x00124b00120144ae',
},
{
depth: 1,
linkquality: 92,
lqi: 92,
relationship: 2,
routes: [],
source: {ieeeAddr: '0x0017880104e45511', networkAddress: 1114},
sourceIeeeAddr: '0x0017880104e45511',
sourceNwkAddr: 1114,
target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0},
targetIeeeAddr: '0x00124b00120144ae',
},
{
depth: 2,
linkquality: 110,
lqi: 110,
relationship: 1,
routes: [],
source: {ieeeAddr: '0x000b57fffec6a5b3', networkAddress: 40399},
sourceIeeeAddr: '0x000b57fffec6a5b3',
sourceNwkAddr: 40399,
target: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369},
targetIeeeAddr: '0x000b57fffec6a5b2',
},
{
depth: 2,
linkquality: 100,
lqi: 100,
relationship: 1,
routes: [],
source: {ieeeAddr: '0x0017880104e45559', networkAddress: 6540},
sourceIeeeAddr: '0x0017880104e45559',
sourceNwkAddr: 6540,
target: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369},
targetIeeeAddr: '0x000b57fffec6a5b2',
},
{
depth: 2,
linkquality: 130,
lqi: 130,
relationship: 1,
routes: [],
source: {ieeeAddr: '0x0017880104e45521', networkAddress: 6538},
sourceIeeeAddr: '0x0017880104e45521',
sourceNwkAddr: 6538,
target: {ieeeAddr: '0x0017880104e45559', networkAddress: 6540},
targetIeeeAddr: '0x0017880104e45559',
},
],
nodes: [
{
// definition: null,
failed: [],
friendlyName: 'Coordinator',
ieeeAddr: '0x00124b00120144ae',
lastSeen: 1000,
modelID: null,
networkAddress: 0,
type: 'Coordinator',
},
{
definition: {
description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm',
model: 'LED1545G12',
supports:
'light (state, brightness, color_temp, color_temp_startup, level_config), effect, power_on_behavior, color_options, identify, linkquality',
vendor: 'IKEA',
},
failed: [],
friendlyName: 'bulb',
ieeeAddr: '0x000b57fffec6a5b2',
lastSeen: 1000,
modelID: 'TRADFRI bulb E27 WS opal 980lm',
networkAddress: 40369,
type: 'Router',
},
{
definition: {
description: 'Hue Go',
model: '7146060PH',
supports:
'light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality',
vendor: 'Philips',
},
failed: [],
friendlyName: 'bulb_color',
ieeeAddr: '0x000b57fffec6a5b3',
lastSeen: 1000,
modelID: 'LLC020',
networkAddress: 40399,
type: 'Router',
},
{
definition: {
description: 'Wireless remote switch (double rocker), 2016 model',
model: 'WXKG02LM_rev1',
supports: 'battery, voltage, power_outage_count, action, linkquality',
vendor: 'Aqara',
},
friendlyName: 'button_double_key',
ieeeAddr: '0x0017880104e45521',
lastSeen: 1000,
modelID: 'lumi.sensor_86sw2.es1',
networkAddress: 6538,
type: 'EndDevice',
},
{
definition: {
description: 'Automatically generated definition',
model: 'notSupportedModelID',
supports: 'action, linkquality',
vendor: 'Boef',
},
failed: ['lqi', 'routingTable'],
friendlyName: '0x0017880104e45525',
ieeeAddr: '0x0017880104e45525',
lastSeen: 1000,
manufacturerName: 'Boef',
modelID: 'notSupportedModelID',
networkAddress: 6536,
type: 'Router',
},
{
definition: {description: 'CC2530 router', model: 'CC2530.ROUTER', supports: 'led, linkquality', vendor: 'Custom devices (DiY)'},
failed: [],
friendlyName: 'cc2530_router',
ieeeAddr: '0x0017880104e45559',
lastSeen: 1000,
modelID: 'lumi.router',
networkAddress: 6540,
type: 'Router',
},
{
definition: {description: 'external', model: 'external_converter_device', supports: 'linkquality', vendor: 'external'},
friendlyName: '0x0017880104e45511',
ieeeAddr: '0x0017880104e45511',
lastSeen: 1000,
modelID: 'external_converter_device',
networkAddress: 1114,
type: 'EndDevice',
},
],
};
expect(JSON.parse(call[1])).toStrictEqual(expected);
/**
* Check again without routes
*/
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/networkmap', 'raw');
await flushPromises();
call = MQTT.publish.mock.calls[0];
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/raw');
// Remove routing information
expected.nodes.forEach((n) => {
if (n.failed && n.failed.includes('routingTable')) {
n.failed.splice(n.failed.indexOf('routingTable'), 1);
}
});
expected.links.forEach((l) => (l.routes = []));
expect(JSON.parse(call[1])).toStrictEqual(expected);
});
it('Output graphviz networkmap legacy api', async () => {
mock();
const device = zigbeeHerdsman.devices.bulb_color;
device.lastSeen = null;
const endpoint = device.getEndpoint(1);
const data = {modelID: 'test'};
const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'graphviz');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
let call = MQTT.publish.mock.calls[0];
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/graphviz');
const expected = `digraph G {
node[shape=record];
"0x00124b00120144ae" [style="bold, filled", fillcolor="#e04e5d", fontcolor="#ffffff", label="{Coordinator|0x00124b00120144ae (0x0000)|0 seconds ago}"];
"0x000b57fffec6a5b2" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb|0x000b57fffec6a5b2 (0x9db1)|IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)|9 seconds ago}"];
"0x000b57fffec6a5b2" -> "0x00124b00120144ae" [penwidth=2, weight=1, color="#009900", label="92 (routes: 0x198c)"]
"0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (0x9dcf)|Philips Hue Go (7146060PH)|unknown}"];
"0x000b57fffec6a5b3" -> "0x00124b00120144ae" [penwidth=0.5, weight=0, color="#994444", label="120"]
"0x000b57fffec6a5b3" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="110"]
"0x0017880104e45521" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{button_double_key|0x0017880104e45521 (0x198a)|Aqara Wireless remote switch (double rocker), 2016 model (WXKG02LM_rev1)|9 seconds ago}"];
"0x0017880104e45521" -> "0x0017880104e45559" [penwidth=1, weight=0, color="#994444", label="130"]
"0x0017880104e45525" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{0x0017880104e45525|0x0017880104e45525 (0x1988)failed: lqi,routingTable|Boef Automatically generated definition (notSupportedModelID)|9 seconds ago}"];
"0x0017880104e45559" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{cc2530_router|0x0017880104e45559 (0x198c)|Custom devices (DiY) CC2530 router (CC2530.ROUTER)|9 seconds ago}"];
"0x0017880104e45559" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="100"]
"0x0017880104e45511" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{0x0017880104e45511|0x0017880104e45511 (0x045a)|external external (external_converter_device)|9 seconds ago}"];
"0x0017880104e45511" -> "0x00124b00120144ae" [penwidth=1, weight=0, color="#994444", label="92"]
}`;
const expectedLines = expected.split('\n');
const actualLines = call[1].split('\n');
for (let i = 0; i < expectedLines.length; i++) {
expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim());
}
});
it('Output plantuml networkmap legacy api', async () => {
mock();
const device = zigbeeHerdsman.devices.bulb_color;
device.lastSeen = null;
const endpoint = device.getEndpoint(1);
const data = {modelID: 'test'};
const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'plantuml');
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
let call = MQTT.publish.mock.calls[0];
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/plantuml');
const expected = `' paste into: https://www.planttext.com/
@startuml
card 0x0017880104e45511 [
0x0017880104e45511
---
0x0017880104e45511 (0x045a)
---
external external (external_converter_device)
---
9 seconds ago
]
card 0x0017880104e45525 [
0x0017880104e45525
---
0x0017880104e45525 (0x1988) failed: lqi,routingTable
---
Boef Automatically generated definition (notSupportedModelID)
---
9 seconds ago
]
card 0x000b57fffec6a5b2 [
bulb
---
0x000b57fffec6a5b2 (0x9db1)
---
IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)
---
9 seconds ago
]
card 0x000b57fffec6a5b3 [
bulb_color
---
0x000b57fffec6a5b3 (0x9dcf)
---
Philips Hue Go (7146060PH)
---
unknown
]
card 0x0017880104e45521 [
button_double_key
---
0x0017880104e45521 (0x198a)
---
Aqara Wireless remote switch (double rocker), 2016 model (WXKG02LM_rev1)
---
9 seconds ago
]
card 0x0017880104e45559 [
cc2530_router
---
0x0017880104e45559 (0x198c)
---
Custom devices (DiY) CC2530 router (CC2530.ROUTER)
---
9 seconds ago
]
card 0x00124b00120144ae [
Coordinator
---
0x00124b00120144ae (0x0000)
---
0 seconds ago
]
0x000b57fffec6a5b3 --> 0x00124b00120144ae: 120
0x000b57fffec6a5b2 --> 0x00124b00120144ae: 92
0x0017880104e45511 --> 0x00124b00120144ae: 92
0x000b57fffec6a5b3 --> 0x000b57fffec6a5b2: 110
0x0017880104e45559 --> 0x000b57fffec6a5b2: 100
0x0017880104e45521 --> 0x0017880104e45559: 130
@enduml`;
const expectedLines = expected.split('\n');
const actualLines = call[1].split('\n');
for (let i = 0; i < expectedLines.length; i++) {
expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim());
}
});
it('Should output raw networkmap', async () => {
mock();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
@@ -637,6 +295,147 @@ describe('Networkmap', () => {
expect(actual).toStrictEqual(expected);
});
it('Output graphviz networkmap', async () => {
mock();
const device = zigbeeHerdsman.devices.bulb_color;
device.lastSeen = null;
const endpoint = device.getEndpoint(1);
const data = {modelID: 'test'};
const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'graphviz', routes: true}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
let call = MQTT.publish.mock.calls[0];
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
const expected = `digraph G {
node[shape=record];
"0x00124b00120144ae" [style="bold, filled", fillcolor="#e04e5d", fontcolor="#ffffff", label="{Coordinator|0x00124b00120144ae (0x0000)|0 seconds ago}"];
"0x000b57fffec6a5b2" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb|0x000b57fffec6a5b2 (0x9db1)|IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)|9 seconds ago}"];
"0x000b57fffec6a5b2" -> "0x00124b00120144ae" [penwidth=2, weight=1, color="#009900", label="92 (routes: 0x198c)"]
"0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (0x9dcf)|Philips Hue Go (7146060PH)|unknown}"];
"0x000b57fffec6a5b3" -> "0x00124b00120144ae" [penwidth=0.5, weight=0, color="#994444", label="120"]
"0x000b57fffec6a5b3" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="110"]
"0x0017880104e45521" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{button_double_key|0x0017880104e45521 (0x198a)|Aqara Wireless remote switch (double rocker), 2016 model (WXKG02LM_rev1)|9 seconds ago}"];
"0x0017880104e45521" -> "0x0017880104e45559" [penwidth=1, weight=0, color="#994444", label="130"]
"0x0017880104e45525" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{0x0017880104e45525|0x0017880104e45525 (0x1988)failed: lqi,routingTable|Boef Automatically generated definition (notSupportedModelID)|9 seconds ago}"];
"0x0017880104e45559" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{cc2530_router|0x0017880104e45559 (0x198c)|Custom devices (DiY) CC2530 router (CC2530.ROUTER)|9 seconds ago}"];
"0x0017880104e45559" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="100"]
"0x0017880104e45511" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{0x0017880104e45511|0x0017880104e45511 (0x045a)|external external (external_converter_device)|9 seconds ago}"];
"0x0017880104e45511" -> "0x00124b00120144ae" [penwidth=1, weight=0, color="#994444", label="92"]
}`;
const expectedLines = expected.split('\n');
const actualLines = JSON.parse(call[1]).data.value.split('\n');
for (let i = 0; i < expectedLines.length; i++) {
expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim());
}
});
it('Output plantuml networkmap', async () => {
mock();
const device = zigbeeHerdsman.devices.bulb_color;
device.lastSeen = null;
const endpoint = device.getEndpoint(1);
const data = {modelID: 'test'};
const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'plantuml', routes: true}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
let call = MQTT.publish.mock.calls[0];
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
const expected = `' paste into: https://www.planttext.com/
@startuml
card 0x0017880104e45511 [
0x0017880104e45511
---
0x0017880104e45511 (0x045a)
---
external external (external_converter_device)
---
9 seconds ago
]
card 0x0017880104e45525 [
0x0017880104e45525
---
0x0017880104e45525 (0x1988) failed: lqi,routingTable
---
Boef Automatically generated definition (notSupportedModelID)
---
9 seconds ago
]
card 0x000b57fffec6a5b2 [
bulb
---
0x000b57fffec6a5b2 (0x9db1)
---
IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)
---
9 seconds ago
]
card 0x000b57fffec6a5b3 [
bulb_color
---
0x000b57fffec6a5b3 (0x9dcf)
---
Philips Hue Go (7146060PH)
---
unknown
]
card 0x0017880104e45521 [
button_double_key
---
0x0017880104e45521 (0x198a)
---
Aqara Wireless remote switch (double rocker), 2016 model (WXKG02LM_rev1)
---
9 seconds ago
]
card 0x0017880104e45559 [
cc2530_router
---
0x0017880104e45559 (0x198c)
---
Custom devices (DiY) CC2530 router (CC2530.ROUTER)
---
9 seconds ago
]
card 0x00124b00120144ae [
Coordinator
---
0x00124b00120144ae (0x0000)
---
0 seconds ago
]
0x000b57fffec6a5b3 --> 0x00124b00120144ae: 120
0x000b57fffec6a5b2 --> 0x00124b00120144ae: 92
0x0017880104e45511 --> 0x00124b00120144ae: 92
0x000b57fffec6a5b3 --> 0x000b57fffec6a5b2: 110
0x0017880104e45559 --> 0x000b57fffec6a5b2: 100
0x0017880104e45521 --> 0x0017880104e45559: 130
@enduml`;
const expectedLines = expected.split('\n');
const actualLines = JSON.parse(call[1]).data.value.split('\n');
for (let i = 0; i < expectedLines.length; i++) {
expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim());
}
});
it('Should throw error when requesting invalid type', async () => {
mock();
MQTT.publish.mockClear();
+9 -129
View File
@@ -94,19 +94,19 @@ describe('OTA update', () => {
expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined});
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({update_available: false, update: {state: 'updating', progress: 0}}),
stringify({update: {state: 'updating', progress: 0}}),
{retain: true, qos: 0},
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({update_available: false, update: {state: 'updating', progress: 10, remaining: 3600}}),
stringify({update: {state: 'updating', progress: 10, remaining: 3600}}),
{retain: true, qos: 0},
expect.any(Function),
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({update_available: false, update: {state: 'idle', installed_version: 90, latest_version: 90}}),
stringify({update: {state: 'idle', installed_version: 90, latest_version: 90}}),
{retain: true, qos: 0},
expect.any(Function),
);
@@ -139,7 +139,7 @@ describe('OTA update', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({update_available: true, update: {state: 'available'}}),
stringify({update: {state: 'available'}}),
{retain: true, qos: 0},
expect.any(Function),
);
@@ -163,7 +163,7 @@ describe('OTA update', () => {
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {id: 'bulb', updateAvailable: false}, status: 'ok'}),
stringify({data: {id: 'bulb', update_available: false}, status: 'ok'}),
{retain: false, qos: 0},
expect.any(Function),
);
@@ -176,7 +176,7 @@ describe('OTA update', () => {
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {id: 'bulb', updateAvailable: true}, status: 'ok'}),
stringify({data: {id: 'bulb', update_available: true}, status: 'ok'}),
{retain: false, qos: 0},
expect.any(Function),
);
@@ -309,7 +309,7 @@ describe('OTA update', () => {
expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({update_available: true, update: {state: 'available', installed_version: 10, latest_version: 12}}),
stringify({update: {state: 'available', installed_version: 10, latest_version: 12}}),
{retain: true, qos: 0},
expect.any(Function),
);
@@ -342,7 +342,7 @@ describe('OTA update', () => {
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({update_available: false, update: {state: 'idle'}}),
stringify({update: {state: 'idle'}}),
{retain: true, qos: 0},
expect.any(Function),
);
@@ -373,7 +373,7 @@ describe('OTA update', () => {
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({update_available: false, update: {state: 'idle', installed_version: 13, latest_version: 13}}),
stringify({update: {state: 'idle', installed_version: 13, latest_version: 13}}),
{retain: true, qos: 0},
expect.any(Function),
);
@@ -438,126 +438,6 @@ describe('OTA update', () => {
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10);
});
it('Legacy api: Should OTA update a device', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.endpoints[0];
let count = 0;
endpoint.read.mockImplementation(() => {
count++;
return {swBuildId: count, dateCode: '2019010' + count};
});
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
mockClear(mapped);
logger.info.mockClear();
logger.error.mockClear();
device.save.mockClear();
mapped.ota.updateToLatest.mockImplementationOnce((a, onUpdate) => {
onUpdate(0, null);
onUpdate(10, 3600);
return 91;
});
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
await flushPromises();
expect(logger.info).toHaveBeenCalledWith(`Updating 'bulb' to latest firmware`);
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(0);
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(1);
expect(mapped.ota.updateToLatest).toHaveBeenCalledWith(device, expect.any(Function));
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`);
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`);
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`);
expect(logger.info).toHaveBeenCalledWith(
`Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`,
);
expect(logger.error).toHaveBeenCalledTimes(0);
expect(device.save).toHaveBeenCalledTimes(2);
expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: 'immediate'});
expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined});
});
it('Legacy api: Should handle when OTA update fails', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.endpoints[0];
endpoint.read.mockImplementation(() => {
return {swBuildId: 1, dateCode: '2019010'};
});
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
mockClear(mapped);
logger.info.mockClear();
logger.error.mockClear();
device.save.mockClear();
mapped.ota.updateToLatest.mockImplementationOnce((a, onUpdate) => {
throw new Error('Update failed');
});
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
await flushPromises();
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(`Update of 'bulb' failed (Update failed)`);
});
it('Legacy api: Should be able to check if OTA update is available', async () => {
const device = zigbeeHerdsman.devices.bulb;
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
mockClear(mapped);
logger.info.mockClear();
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13});
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
await flushPromises();
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
expect(logger.info).toHaveBeenCalledWith(`No update available for 'bulb'`);
logger.info.mockClear();
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 13, otaFileVersion: 15});
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
await flushPromises();
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2);
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
expect(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`);
});
it('Legacy api: Should handle if OTA update check fails', async () => {
const device = zigbeeHerdsman.devices.bulb;
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
mockClear(mapped);
logger.error.mockClear();
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {
throw new Error('RF signals disturbed because of dogs barking');
});
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
await flushPromises();
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
expect(logger.error).toHaveBeenCalledWith(`Failed to check if update available for 'bulb' (RF signals disturbed because of dogs barking)`);
});
it('Legacy api: Should not check for OTA when device does not support it', async () => {
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'dimmer_wall_switch');
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(`Device 'dimmer_wall_switch' does not support OTA updates`);
});
it('Legacy api: Shouldnt crash when read modelID after OTA update fails', async () => {
const device = zigbeeHerdsman.devices.bulb;
const endpoint = device.endpoints[0];
let count = 0;
endpoint.read.mockImplementation(() => {
if (count === 1) throw new Error('Failed!');
count++;
return {swBuildId: 1, dateCode: '2019010'};
});
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
mockClear(mapped);
logger.info.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
await flushPromises();
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`);
});
it('Set zigbee_ota_override_index_location', async () => {
settings.set(['ota', 'zigbee_ota_override_index_location'], 'local.index.json');
await resetExtension();
+40 -63
View File
@@ -402,24 +402,28 @@ describe('Publish', () => {
it('Should publish messages groups', async () => {
const group = zigbeeHerdsman.groups.group_1;
group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1));
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'});
expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'});
group.members.pop();
});
it('Should publish messages to groups with brightness_percent', async () => {
const group = zigbeeHerdsman.groups.group_1;
group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1));
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({brightness_percent: 50}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 128, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 128});
expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 128});
group.members.pop();
});
it('Should publish messages to groups when converter is not in the default list but device in it supports it', async () => {
@@ -437,46 +441,54 @@ describe('Publish', () => {
it('Should publish messages to groups with on and brightness', async () => {
const group = zigbeeHerdsman.groups.group_1;
group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1));
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 50}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 50});
expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 50});
group.members.pop();
});
it('Should publish messages to groups with off and brightness', async () => {
const group = zigbeeHerdsman.groups.group_1;
group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1));
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF', brightness: 50}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'OFF'});
group.members.pop();
});
it('Should publish messages to groups color', async () => {
const group = zigbeeHerdsman.groups.group_1;
group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1));
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.37, y: 0.28}}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 24248, colory: 18350, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: 'xy'});
expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: 'xy'});
group.members.pop();
});
it('Should publish messages to groups color temperature', async () => {
const group = zigbeeHerdsman.groups.group_1;
group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1));
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 100}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColorTemp', {colortemp: 100, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 100, color_mode: 'color_temp'});
expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color_temp: 100, color_mode: 'color_temp'});
group.members.pop();
});
it('Should create and publish to group which is in configuration.yaml but not in zigbee-herdsman', async () => {
@@ -485,8 +497,8 @@ describe('Publish', () => {
await MQTT.events.message('zigbee2mqtt/group_12312/set', stringify({state: 'ON'}));
await flushPromises();
expect(Object.values(zigbeeHerdsman.groups).length).toBe(11);
expect(zigbeeHerdsman.groups.group_12312.command).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.groups.group_12312.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {});
// group contains no device
expect(zigbeeHerdsman.groups.group_12312.command).toHaveBeenCalledTimes(0);
delete zigbeeHerdsman.groups.group_12312;
});
@@ -599,7 +611,6 @@ describe('Publish', () => {
logger.error.mockClear();
await MQTT.events.message(`zigbee2mqtt/${device.ieeeAddr}/set`, stringify({state: 'OFF'}));
await flushPromises();
console.log(logger.error.mock.calls);
expect(logger.log).toHaveBeenCalledWith('error', `Cannot publish to unsupported device 'button_double_key_interviewing'`, 'z2m');
});
@@ -616,24 +627,6 @@ describe('Publish', () => {
expect(endpoint3.read).toHaveBeenCalledWith('genOnOff', ['onOff']);
});
it('Should not respond to bridge/config/devices/get', async () => {
await MQTT.events.message('zigbee2mqtt/bridge/config/devices/get', stringify({state: 'ON'}));
await flushPromises();
expectNothingPublished();
});
it('Should not respond to bridge/config/devices/set', async () => {
await MQTT.events.message('zigbee2mqtt/bridge/config/devices/set', stringify({state: 'ON'}));
await flushPromises();
expectNothingPublished();
});
it('Should not respond to bridge/config/devices', async () => {
await MQTT.events.message('zigbee2mqtt/bridge/config/devices', stringify({state: 'ON'}));
await flushPromises();
expectNothingPublished();
});
it('Should parse topic with when base topic has multiple slashes', async () => {
settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/home');
loadTopicGetSetRegex();
@@ -806,22 +799,6 @@ describe('Publish', () => {
expect(endpoint.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColor', {colorx: 45940, colory: 19595, transtime: 0}, {}]);
});
it('Should read after write when enabled', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
settings.set(['devices', device.ieeeAddr, 'retrieve_state'], true);
const endpoint = device.getEndpoint(1);
const payload = {state: 'ON', color: {x: 0.701, y: 0.299}};
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload));
await flushPromises();
jest.runOnlyPendingTimers();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColor', {colorx: 45940, colory: 19595, transtime: 0}, {}]);
expect(endpoint.read).toHaveBeenCalledTimes(2);
expect(endpoint.read.mock.calls[0]).toEqual(['genOnOff', ['onOff']]);
expect(endpoint.read.mock.calls[1]).toEqual(['lightingColorCtrl', ['colorMode', 'currentX', 'currentY', 'colorTemperature']]);
});
it('Should also use on/off cluster when controlling group with switch', async () => {
const group = zigbeeHerdsman.groups.group_with_switch;
@@ -855,14 +832,16 @@ describe('Publish', () => {
it('Should use transition when brightness with group', async () => {
const group = zigbeeHerdsman.groups.group_1;
group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1));
settings.set(['groups', '1', 'transition'], 20);
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({brightness: 100}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 100, transtime: 200}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 100});
expect(MQTT.publish).toHaveBeenCalledTimes(2); // zigbee2mqtt/bulb_color + below
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 100});
group.members.pop();
});
it('Should use transition on brightness command', async () => {
@@ -1545,18 +1524,16 @@ describe('Publish', () => {
const endpoint = device.getEndpoint(1);
await MQTT.events.message('zigbee2mqtt/GL-S-007ZS/set', stringify({state: 'ON', brightness: 20}));
await flushPromises();
jest.runOnlyPendingTimers();
await flushPromises();
await jest.runOnlyPendingTimersAsync();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0]).toEqual([
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/GL-S-007ZS',
stringify({state: 'ON', brightness: 20}),
{qos: 0, retain: false},
expect.any(Function),
]);
);
});
it('Should log as error when setting property with no defined converter', async () => {
+12 -17
View File
@@ -66,7 +66,7 @@ describe('Settings', () => {
it('Should apply environment variables', () => {
process.env['ZIGBEE2MQTT_CONFIG_SERIAL_DISABLE_LED'] = 'true';
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_SOFT_RESET_TIMEOUT'] = 1;
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_CHANNEL'] = 15;
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_OUTPUT'] = 'attribute_and_json';
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_LOG_OUTPUT'] = '["console"]';
process.env['ZIGBEE2MQTT_CONFIG_MAP_OPTIONS_GRAPHVIZ_COLORS_FILL'] =
@@ -97,7 +97,7 @@ describe('Settings', () => {
};
expected.groups = {};
expected.serial.disable_led = true;
expected.advanced.soft_reset_timeout = 1;
expected.advanced.channel = 15;
expected.advanced.log_output = ['console'];
expected.advanced.output = 'attribute_and_json';
expected.map_options.graphviz.colors.fill = {enddevice: '#ff0000', coordinator: '#00ff00', router: '#0000ff'};
@@ -548,7 +548,15 @@ describe('Settings', () => {
expect(() => {
settings.changeEntityOptions('not_existing_123', {});
}).toThrow(new Error("Device or group 'not_existing_123' does not exist"));
}).toThrow(`Device or group 'not_existing_123' does not exist`);
});
it('Should throw error when changing entity friendlyName of non-existing device', () => {
write(configurationFile, {});
expect(() => {
settings.changeFriendlyName('not_existing_123', 'non_existing_456');
}).toThrow(`Device or group 'not_existing_123' does not exist`);
});
it('Should not add duplicate groups', () => {
@@ -557,7 +565,7 @@ describe('Settings', () => {
settings.addGroup('test123');
expect(() => {
settings.addGroup('test123');
}).toThrow(new Error("friendly_name 'test123' is already in use"));
}).toThrow(`friendly_name 'test123' is already in use`);
const expected = {
1: {
friendly_name: 'test123',
@@ -746,19 +754,6 @@ describe('Settings', () => {
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it('Should not allow non-existing entities in availability_blocklist', () => {
write(configurationFile, {
...minimalConfig,
devices: {'0x0017880104e45519': {friendly_name: 'tain'}},
advanced: {availability_blocklist: ['0x0017880104e45519', 'non_existing']},
});
settings.reRead();
const error = `Non-existing entity 'non_existing' specified in 'availability_blocklist'`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
});
it('Should validate if settings do not conform to scheme', () => {
write(configurationFile, {
...minimalConfig,
+2 -19
View File
@@ -3,13 +3,6 @@ cd "$(dirname "$0")"
NEED_RESTART=0
if [ -d data-backup ]; then
echo "ERROR: Backup directory exists. May be previous restoring was failed?"
echo "1. Save 'data-backup' and 'data' dirs to safe location to make possibility to restore config later."
echo "2. Manually delete 'data-backup' dir and try again."
exit 1
fi
if which systemctl 2> /dev/null > /dev/null; then
echo "Checking Zigbee2MQTT status..."
if systemctl is-active --quiet zigbee2mqtt; then
@@ -21,24 +14,14 @@ else
echo "Skipped stopping Zigbee2MQTT, no systemctl found"
fi
echo "Creating backup of configuration..."
cp -R data data-backup
echo "Checking out changes to package-lock.json..."
git checkout package-lock.json
echo "Updating..."
git pull --no-rebase
echo "Installing dependencies..."
npm ci
pnpm i --frozen-lockfile
echo "Building..."
npm run build
echo "Restore configuration..."
cp -R data-backup/* data
rm -rf data-backup
pnpm run build
if [ $NEED_RESTART -eq 1 ]; then
echo "Starting Zigbee2MQTT..."