mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-09-01 16:48:31 +00:00
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:
+18
-13
@@ -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
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -1,2 +1,2 @@
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
CHANGELOG.md
|
||||
+1
-1
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Vendored
+1
-13
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Generated
-8610
File diff suppressed because it is too large
Load Diff
Generated
+5968
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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'],
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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..."
|
||||
|
||||
Reference in New Issue
Block a user