mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2024-11-15 01:48:32 -07:00
chore: Implement prettier (#23153)
* chore: Implement prettier * Run prettier * fix lint * process feedback * process feedback
This commit is contained in:
parent
8780ab2792
commit
30227a13ae
52
.eslintrc.js
52
.eslintrc.js
@ -5,21 +5,19 @@ module.exports = {
|
||||
'es6': true,
|
||||
'node': true,
|
||||
},
|
||||
'extends': ['eslint:recommended', 'google', 'plugin:jest/recommended', 'plugin:jest/style'],
|
||||
'extends': ['eslint:recommended', 'plugin:jest/recommended', 'plugin:jest/style', 'prettier'],
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 2018,
|
||||
'sourceType': 'module',
|
||||
},
|
||||
'rules': {
|
||||
'require-jsdoc': 'off',
|
||||
'indent': ['error', 4],
|
||||
'max-len': ['error', {'code': 150}],
|
||||
'no-prototype-builtins': 'off',
|
||||
'linebreak-style': ['error', (process.platform === 'win32' ? 'windows' : 'unix')], // https://stackoverflow.com/q/39114446/2771889
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
},
|
||||
'plugins': [
|
||||
'jest',
|
||||
'perfectionist',
|
||||
],
|
||||
'overrides': [{
|
||||
files: ['*.ts'],
|
||||
@ -35,14 +33,46 @@ module.exports = {
|
||||
'@typescript-eslint/explicit-function-return-type': 'error',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'@typescript-eslint/semi': ['error'],
|
||||
'array-bracket-spacing': ['error', 'never'],
|
||||
'indent': ['error', 4],
|
||||
'max-len': ['error', {'code': 150}],
|
||||
'no-return-await': 'error',
|
||||
'object-curly-spacing': ['error', 'never'],
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'no-return-await': 'error',
|
||||
"perfectionist/sort-imports": [
|
||||
"error",
|
||||
{
|
||||
"groups": [
|
||||
"type",
|
||||
[
|
||||
"builtin",
|
||||
"external"
|
||||
],
|
||||
"internal-type",
|
||||
"internal",
|
||||
[
|
||||
"parent-type",
|
||||
"sibling-type",
|
||||
"index-type"
|
||||
],
|
||||
[
|
||||
"parent",
|
||||
"sibling",
|
||||
"index"
|
||||
],
|
||||
"object",
|
||||
"unknown"
|
||||
],
|
||||
"custom-groups": {
|
||||
"value": {},
|
||||
"type": {}
|
||||
},
|
||||
"newlines-between": "always",
|
||||
"internal-pattern": [
|
||||
"~/**"
|
||||
],
|
||||
"type": "natural",
|
||||
"order": "asc",
|
||||
"ignore-case": false
|
||||
}
|
||||
],
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -26,10 +26,12 @@ jobs:
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Lint
|
||||
run: |
|
||||
npm run pretty:check
|
||||
npm run eslint
|
||||
- name: Test
|
||||
run: npm run test-with-coverage
|
||||
- name: Lint
|
||||
run: npm run eslint
|
||||
- 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
|
||||
|
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 150,
|
||||
"bracketSpacing": false,
|
||||
"endOfLine": "lf",
|
||||
"tabWidth": 4
|
||||
}
|
@ -1,46 +1,67 @@
|
||||
import MQTT from './mqtt';
|
||||
import Zigbee from './zigbee';
|
||||
import assert from 'assert';
|
||||
import bind from 'bind-decorator';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import {setLogger as zhSetLogger} from 'zigbee-herdsman';
|
||||
import {setLogger as zhcSetLogger} from 'zigbee-herdsman-converters';
|
||||
|
||||
import EventBus from './eventBus';
|
||||
import ExtensionAvailability from './extension/availability';
|
||||
import ExtensionBind from './extension/bind';
|
||||
import ExtensionBridge from './extension/bridge';
|
||||
import ExtensionConfigure from './extension/configure';
|
||||
import ExtensionExternalConverters from './extension/externalConverters';
|
||||
import ExtensionExternalExtension from './extension/externalExtension';
|
||||
// Extensions
|
||||
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';
|
||||
import ExtensionPublish from './extension/publish';
|
||||
import ExtensionReceive from './extension/receive';
|
||||
import MQTT from './mqtt';
|
||||
import State from './state';
|
||||
import logger from './util/logger';
|
||||
import * as settings from './util/settings';
|
||||
import utils from './util/utils';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import assert from 'assert';
|
||||
import bind from 'bind-decorator';
|
||||
import {setLogger as zhcSetLogger} from 'zigbee-herdsman-converters';
|
||||
import {setLogger as zhSetLogger} from 'zigbee-herdsman';
|
||||
|
||||
// Extensions
|
||||
import ExtensionFrontend from './extension/frontend';
|
||||
import ExtensionPublish from './extension/publish';
|
||||
import ExtensionReceive from './extension/receive';
|
||||
import ExtensionNetworkMap from './extension/networkMap';
|
||||
import ExtensionSoftReset from './extension/legacy/softReset';
|
||||
import ExtensionHomeAssistant from './extension/homeassistant';
|
||||
import ExtensionConfigure from './extension/configure';
|
||||
import ExtensionDeviceGroupMembership from './extension/legacy/deviceGroupMembership';
|
||||
import ExtensionBridgeLegacy from './extension/legacy/bridgeLegacy';
|
||||
import ExtensionBridge from './extension/bridge';
|
||||
import ExtensionGroups from './extension/groups';
|
||||
import ExtensionAvailability from './extension/availability';
|
||||
import ExtensionBind from './extension/bind';
|
||||
import ExtensionReport from './extension/legacy/report';
|
||||
import ExtensionOnEvent from './extension/onEvent';
|
||||
import ExtensionOTAUpdate from './extension/otaUpdate';
|
||||
import ExtensionExternalConverters from './extension/externalConverters';
|
||||
import ExtensionExternalExtension from './extension/externalExtension';
|
||||
import Zigbee from './zigbee';
|
||||
|
||||
const AllExtensions = [
|
||||
ExtensionPublish, ExtensionReceive, ExtensionNetworkMap, ExtensionSoftReset, ExtensionHomeAssistant,
|
||||
ExtensionConfigure, ExtensionDeviceGroupMembership, ExtensionBridgeLegacy, ExtensionBridge, ExtensionGroups,
|
||||
ExtensionBind, ExtensionReport, ExtensionOnEvent, ExtensionOTAUpdate,
|
||||
ExtensionExternalConverters, ExtensionFrontend, ExtensionExternalExtension, ExtensionAvailability,
|
||||
ExtensionPublish,
|
||||
ExtensionReceive,
|
||||
ExtensionNetworkMap,
|
||||
ExtensionSoftReset,
|
||||
ExtensionHomeAssistant,
|
||||
ExtensionConfigure,
|
||||
ExtensionDeviceGroupMembership,
|
||||
ExtensionBridgeLegacy,
|
||||
ExtensionBridge,
|
||||
ExtensionGroups,
|
||||
ExtensionBind,
|
||||
ExtensionReport,
|
||||
ExtensionOnEvent,
|
||||
ExtensionOTAUpdate,
|
||||
ExtensionExternalConverters,
|
||||
ExtensionFrontend,
|
||||
ExtensionExternalExtension,
|
||||
ExtensionAvailability,
|
||||
];
|
||||
|
||||
type ExtensionArgs = [Zigbee, MQTT, State, PublishEntityState, EventBus,
|
||||
enableDisableExtension: (enable: boolean, name: string) => Promise<void>, restartCallback: () => Promise<void>,
|
||||
addExtension: (extension: Extension) => Promise<void>];
|
||||
type ExtensionArgs = [
|
||||
Zigbee,
|
||||
MQTT,
|
||||
State,
|
||||
PublishEntityState,
|
||||
EventBus,
|
||||
enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => Promise<void>,
|
||||
addExtension: (extension: Extension) => Promise<void>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let sdNotify: any = null;
|
||||
@ -72,8 +93,16 @@ export class Controller {
|
||||
this.exitCallback = exitCallback;
|
||||
|
||||
// Initialize extensions.
|
||||
this.extensionArgs = [this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
|
||||
this.enableDisableExtension, this.restartCallback, this.addExtension];
|
||||
this.extensionArgs = [
|
||||
this.zigbee,
|
||||
this.mqtt,
|
||||
this.state,
|
||||
this.publishEntityState,
|
||||
this.eventBus,
|
||||
this.enableDisableExtension,
|
||||
this.restartCallback,
|
||||
this.addExtension,
|
||||
];
|
||||
|
||||
this.extensions = [
|
||||
new ExtensionOnEvent(...this.extensionArgs),
|
||||
@ -111,7 +140,7 @@ export class Controller {
|
||||
this.eventBus.onAdapterDisconnected(this, this.onZigbeeAdapterDisconnected);
|
||||
} catch (error) {
|
||||
logger.error('Failed to start zigbee');
|
||||
logger.error('Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html for possible solutions'); /* eslint-disable-line max-len */
|
||||
logger.error('Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html for possible solutions');
|
||||
logger.error('Exiting...');
|
||||
logger.error(error.stack);
|
||||
return this.exit(1);
|
||||
@ -130,9 +159,9 @@ export class Controller {
|
||||
const devices = this.zigbee.devices(false);
|
||||
logger.info(`Currently ${devices.length} devices are joined:`);
|
||||
for (const device of devices) {
|
||||
const model = device.isSupported ?
|
||||
`${device.definition.model} - ${device.definition.vendor} ${device.definition.description}` :
|
||||
'Not supported';
|
||||
const model = device.isSupported
|
||||
? `${device.definition.model} - ${device.definition.vendor} ${device.definition.description}`
|
||||
: 'Not supported';
|
||||
logger.info(`${device.name} (${device.ieeeAddr}): ${model} (${device.zh.type})`);
|
||||
}
|
||||
|
||||
@ -170,8 +199,7 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
this.eventBus.onLastSeenChanged(this,
|
||||
(data) => utils.publishLastSeen(data, settings.get(), false, this.publishEntityState));
|
||||
this.eventBus.onLastSeenChanged(this, (data) => utils.publishLastSeen(data, settings.get(), false, this.publishEntityState));
|
||||
|
||||
logger.info(`Zigbee2MQTT started!`);
|
||||
|
||||
@ -237,8 +265,7 @@ export class Controller {
|
||||
await this.stop();
|
||||
}
|
||||
|
||||
@bind async publishEntityState(entity: Group | Device, payload: KeyValue,
|
||||
stateChangeReason?: StateChangeReason): Promise<void> {
|
||||
@bind async publishEntityState(entity: Group | Device, payload: KeyValue, stateChangeReason?: StateChangeReason): Promise<void> {
|
||||
let message = {...payload};
|
||||
|
||||
// Update state cache with new state.
|
||||
@ -261,12 +288,18 @@ export class Controller {
|
||||
|
||||
if (entity.isDevice() && settings.get().mqtt.include_device_information) {
|
||||
message.device = {
|
||||
friendlyName: entity.name, model: entity.definition?.model,
|
||||
ieeeAddr: entity.ieeeAddr, networkAddress: entity.zh.networkAddress, type: entity.zh.type,
|
||||
friendlyName: entity.name,
|
||||
model: entity.definition?.model,
|
||||
ieeeAddr: entity.ieeeAddr,
|
||||
networkAddress: entity.zh.networkAddress,
|
||||
type: entity.zh.type,
|
||||
manufacturerID: entity.zh.manufacturerID,
|
||||
powerSource: entity.zh.powerSource, applicationVersion: entity.zh.applicationVersion,
|
||||
stackVersion: entity.zh.stackVersion, zclVersion: entity.zh.zclVersion,
|
||||
hardwareVersion: entity.zh.hardwareVersion, dateCode: entity.zh.dateCode,
|
||||
powerSource: entity.zh.powerSource,
|
||||
applicationVersion: entity.zh.applicationVersion,
|
||||
stackVersion: entity.zh.stackVersion,
|
||||
zclVersion: entity.zh.zclVersion,
|
||||
hardwareVersion: entity.zh.hardwareVersion,
|
||||
dateCode: entity.zh.dateCode,
|
||||
softwareBuildID: entity.zh.softwareBuildID,
|
||||
// Manufacturer name can contain \u0000, remove this.
|
||||
// https://github.com/home-assistant/core/issues/85691
|
||||
|
@ -1,11 +1,12 @@
|
||||
import events from 'events';
|
||||
|
||||
import logger from './util/logger';
|
||||
|
||||
// eslint-disable-next-line
|
||||
type ListenerKey = object;
|
||||
|
||||
export default class EventBus {
|
||||
private callbacksByExtension: { [s: string]: { event: string, callback: (...args: unknown[]) => void }[] } = {};
|
||||
private callbacksByExtension: {[s: string]: {event: string; callback: (...args: unknown[]) => void}[]} = {};
|
||||
private emitter = new events.EventEmitter();
|
||||
|
||||
constructor() {
|
||||
@ -57,8 +58,7 @@ export default class EventBus {
|
||||
public emitDeviceNetworkAddressChanged(data: eventdata.DeviceNetworkAddressChanged): void {
|
||||
this.emitter.emit('deviceNetworkAddressChanged', data);
|
||||
}
|
||||
public onDeviceNetworkAddressChanged(
|
||||
key: ListenerKey, callback: (data: eventdata.DeviceNetworkAddressChanged) => void): void {
|
||||
public onDeviceNetworkAddressChanged(key: ListenerKey, callback: (data: eventdata.DeviceNetworkAddressChanged) => void): void {
|
||||
this.on('deviceNetworkAddressChanged', callback, key);
|
||||
}
|
||||
|
||||
@ -167,7 +167,7 @@ export default class EventBus {
|
||||
this.on('stateChange', callback, key);
|
||||
}
|
||||
|
||||
private on(event: string, callback: (...args: unknown[]) => (Promise<void> | void), key: ListenerKey): void {
|
||||
private on(event: string, callback: (...args: unknown[]) => Promise<void> | void, key: ListenerKey): void {
|
||||
if (!this.callbacksByExtension[key.constructor.name]) this.callbacksByExtension[key.constructor.name] = [];
|
||||
const wrappedCallback = async (...args: unknown[]): Promise<void> => {
|
||||
try {
|
||||
@ -182,7 +182,6 @@ export default class EventBus {
|
||||
}
|
||||
|
||||
public removeListeners(key: ListenerKey): void {
|
||||
this.callbacksByExtension[key.constructor.name]?.forEach(
|
||||
(e) => this.emitter.removeListener(e.event, e.callback));
|
||||
this.callbacksByExtension[key.constructor.name]?.forEach((e) => this.emitter.removeListener(e.event, e.callback));
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
import Extension from './extension';
|
||||
import logger from '../util/logger';
|
||||
import utils from '../util/utils';
|
||||
import * as settings from '../util/settings';
|
||||
import debounce from 'debounce';
|
||||
import bind from 'bind-decorator';
|
||||
import debounce from 'debounce';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
const RETRIEVE_ON_RECONNECT: readonly {keys: string[], condition?: (state: KeyValue) => boolean}[] = [
|
||||
import logger from '../util/logger';
|
||||
import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
|
||||
const RETRIEVE_ON_RECONNECT: readonly {keys: string[]; condition?: (state: KeyValue) => boolean}[] = [
|
||||
{keys: ['state']},
|
||||
{keys: ['brightness'], condition: (state: KeyValue): boolean => state.state === 'ON'},
|
||||
{keys: ['color', 'color_temp'], condition: (state: KeyValue): boolean => state.state === 'ON'},
|
||||
@ -40,8 +41,9 @@ export default class Availability extends Extension {
|
||||
}
|
||||
|
||||
private isAvailable(entity: Device | Group): boolean {
|
||||
return entity.isDevice() ? (Date.now() - entity.zh.lastSeen) < this.getTimeout(entity) :
|
||||
entity.membersDevices().length === 0 || entity.membersDevices().some((d) => this.availabilityCache[d.ieeeAddr]);
|
||||
return entity.isDevice()
|
||||
? Date.now() - entity.zh.lastSeen < this.getTimeout(entity)
|
||||
: entity.membersDevices().length === 0 || entity.membersDevices().some((d) => this.availabilityCache[d.ieeeAddr]);
|
||||
}
|
||||
|
||||
private resetTimer(device: Device): void {
|
||||
@ -80,7 +82,7 @@ export default class Availability extends Extension {
|
||||
for (let i = 1; i <= attempts; i++) {
|
||||
try {
|
||||
// Enable recovery if device is marked as available and first ping fails.
|
||||
await device.zh.ping(!available || (i !== 2));
|
||||
await device.zh.ping(!available || i !== 2);
|
||||
|
||||
pingedSuccessfully = true;
|
||||
|
||||
@ -152,7 +154,7 @@ export default class Availability extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
private async publishAvailability(entity: Device | Group, logLastSeen: boolean, forcePublish=false, skipGroups=false): Promise<void> {
|
||||
private async publishAvailability(entity: Device | Group, logLastSeen: boolean, forcePublish = false, skipGroups = false): Promise<void> {
|
||||
if (logLastSeen && entity.isDevice()) {
|
||||
const ago = Date.now() - entity.zh.lastSeen;
|
||||
if (this.isActiveDevice(entity)) {
|
||||
@ -226,8 +228,12 @@ export default class Availability extends Extension {
|
||||
const options: KeyValue = device.options;
|
||||
const state = this.state.get(device);
|
||||
const meta: zhc.Tz.Meta = {
|
||||
message: this.state.get(device), mapped: device.definition, endpoint_name: null,
|
||||
options, state, device: device.zh,
|
||||
message: this.state.get(device),
|
||||
mapped: device.definition,
|
||||
endpoint_name: null,
|
||||
options,
|
||||
state,
|
||||
device: device.zh,
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -1,28 +1,38 @@
|
||||
import * as settings from '../util/settings';
|
||||
import logger from '../util/logger';
|
||||
import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import debounce from 'debounce';
|
||||
import {Zcl} from 'zigbee-herdsman';
|
||||
import bind from 'bind-decorator';
|
||||
import debounce from 'debounce';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import {Zcl} from 'zigbee-herdsman';
|
||||
import {ClusterName} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
|
||||
|
||||
import Device from '../model/device';
|
||||
import Group from '../model/group';
|
||||
import {ClusterName} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
|
||||
import logger from '../util/logger';
|
||||
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', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl', 'closuresWindowCovering', 'hvacThermostat', 'msIlluminanceMeasurement',
|
||||
'msTemperatureMeasurement', 'msRelativeHumidity', 'msSoilMoisture', 'msCO2',
|
||||
'genScenes',
|
||||
'genOnOff',
|
||||
'genLevelCtrl',
|
||||
'lightingColorCtrl',
|
||||
'closuresWindowCovering',
|
||||
'hvacThermostat',
|
||||
'msIlluminanceMeasurement',
|
||||
'msTemperatureMeasurement',
|
||||
'msRelativeHumidity',
|
||||
'msSoilMoisture',
|
||||
'msCO2',
|
||||
];
|
||||
|
||||
// See zigbee-herdsman-converters
|
||||
const DEFAULT_BIND_GROUP = {type: 'group_number', ID: 901, name: 'default_bind_group'};
|
||||
const DEFAULT_REPORT_CONFIG = {minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1};
|
||||
|
||||
const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemperature: boolean, colorXY: boolean}> => {
|
||||
const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemperature: boolean; colorXY: boolean}> => {
|
||||
if (endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') == null) {
|
||||
await endpoint.read('lightingColorCtrl', ['colorCapabilities']);
|
||||
}
|
||||
@ -30,47 +40,53 @@ const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemper
|
||||
const value = endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') as number;
|
||||
|
||||
return {
|
||||
colorTemperature: (value & 1 << 4) > 0,
|
||||
colorXY: (value & 1 << 3) > 0,
|
||||
colorTemperature: (value & (1 << 4)) > 0,
|
||||
colorXY: (value & (1 << 3)) > 0,
|
||||
};
|
||||
};
|
||||
|
||||
const REPORT_CLUSTERS: Readonly<Partial<Record<ClusterName, Readonly<{
|
||||
attribute: string;
|
||||
minimumReportInterval: number;
|
||||
maximumReportInterval: number;
|
||||
reportableChange: number;
|
||||
condition?: (endpoint: zh.Endpoint) => Promise<boolean>;
|
||||
}>[]>>> = {
|
||||
'genOnOff': [
|
||||
{attribute: 'onOff', ...DEFAULT_REPORT_CONFIG, minimumReportInterval: 0, reportableChange: 0},
|
||||
],
|
||||
'genLevelCtrl': [
|
||||
{attribute: 'currentLevel', ...DEFAULT_REPORT_CONFIG},
|
||||
],
|
||||
'lightingColorCtrl': [
|
||||
const REPORT_CLUSTERS: Readonly<
|
||||
Partial<
|
||||
Record<
|
||||
ClusterName,
|
||||
Readonly<{
|
||||
attribute: string;
|
||||
minimumReportInterval: number;
|
||||
maximumReportInterval: number;
|
||||
reportableChange: number;
|
||||
condition?: (endpoint: zh.Endpoint) => Promise<boolean>;
|
||||
}>[]
|
||||
>
|
||||
>
|
||||
> = {
|
||||
genOnOff: [{attribute: 'onOff', ...DEFAULT_REPORT_CONFIG, minimumReportInterval: 0, reportableChange: 0}],
|
||||
genLevelCtrl: [{attribute: 'currentLevel', ...DEFAULT_REPORT_CONFIG}],
|
||||
lightingColorCtrl: [
|
||||
{
|
||||
attribute: 'colorTemperature', ...DEFAULT_REPORT_CONFIG,
|
||||
attribute: 'colorTemperature',
|
||||
...DEFAULT_REPORT_CONFIG,
|
||||
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorTemperature,
|
||||
},
|
||||
{
|
||||
attribute: 'currentX', ...DEFAULT_REPORT_CONFIG,
|
||||
attribute: 'currentX',
|
||||
...DEFAULT_REPORT_CONFIG,
|
||||
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
|
||||
},
|
||||
{
|
||||
attribute: 'currentY', ...DEFAULT_REPORT_CONFIG,
|
||||
attribute: 'currentY',
|
||||
...DEFAULT_REPORT_CONFIG,
|
||||
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
|
||||
},
|
||||
],
|
||||
'closuresWindowCovering': [
|
||||
closuresWindowCovering: [
|
||||
{attribute: 'currentPositionLiftPercentage', ...DEFAULT_REPORT_CONFIG},
|
||||
{attribute: 'currentPositionTiltPercentage', ...DEFAULT_REPORT_CONFIG},
|
||||
],
|
||||
};
|
||||
|
||||
type PollOnMessage = {
|
||||
cluster: Readonly<Partial<Record<ClusterName, {type: string, data: KeyValue}[]>>>;
|
||||
read: Readonly<{cluster: string, attributes: string[], attributesForEndpoint?: (endpoint: zh.Endpoint) => Promise<string[]>}>;
|
||||
cluster: Readonly<Partial<Record<ClusterName, {type: string; data: KeyValue}[]>>>;
|
||||
read: Readonly<{cluster: string; attributes: string[]; attributesForEndpoint?: (endpoint: zh.Endpoint) => Promise<string[]>}>;
|
||||
manufacturerIDs: readonly number[];
|
||||
manufacturerNames: readonly string[];
|
||||
}[];
|
||||
@ -92,9 +108,7 @@ const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
|
||||
{type: 'commandMove', data: {}},
|
||||
{type: 'commandMoveToLevelWithOnOff', data: {}},
|
||||
],
|
||||
genScenes: [
|
||||
{type: 'commandRecall', data: {}},
|
||||
],
|
||||
genScenes: [{type: 'commandRecall', data: {}}],
|
||||
},
|
||||
// Read the following attributes
|
||||
read: {cluster: 'genLevelCtrl', attributes: ['currentLevel']},
|
||||
@ -107,10 +121,7 @@ const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
|
||||
Zcl.ManufacturerCode.TELINK_MICRO,
|
||||
Zcl.ManufacturerCode.BUSCH_JAEGER_ELEKTRO,
|
||||
],
|
||||
manufacturerNames: [
|
||||
'GLEDOPTO',
|
||||
'Trust International B.V.\u0000',
|
||||
],
|
||||
manufacturerNames: ['GLEDOPTO', 'Trust International B.V.\u0000'],
|
||||
},
|
||||
{
|
||||
cluster: {
|
||||
@ -126,9 +137,7 @@ const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
|
||||
{type: 'commandOffWithEffect', data: {}},
|
||||
{type: 'commandToggle', data: {}},
|
||||
],
|
||||
genScenes: [
|
||||
{type: 'commandRecall', data: {}},
|
||||
],
|
||||
genScenes: [{type: 'commandRecall', data: {}}],
|
||||
manuSpecificPhilips: [
|
||||
{type: 'commandHueNotification', data: {button: 1}},
|
||||
{type: 'commandHueNotification', data: {button: 4}},
|
||||
@ -143,16 +152,11 @@ const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
|
||||
Zcl.ManufacturerCode.TELINK_MICRO,
|
||||
Zcl.ManufacturerCode.BUSCH_JAEGER_ELEKTRO,
|
||||
],
|
||||
manufacturerNames: [
|
||||
'GLEDOPTO',
|
||||
'Trust International B.V.\u0000',
|
||||
],
|
||||
manufacturerNames: ['GLEDOPTO', 'Trust International B.V.\u0000'],
|
||||
},
|
||||
{
|
||||
cluster: {
|
||||
genScenes: [
|
||||
{type: 'commandRecall', data: {}},
|
||||
],
|
||||
genScenes: [{type: 'commandRecall', data: {}}],
|
||||
},
|
||||
read: {
|
||||
cluster: 'lightingColorCtrl',
|
||||
@ -184,15 +188,16 @@ const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
|
||||
Zcl.ManufacturerCode.TELINK_MICRO,
|
||||
// Note: ManufacturerCode.BUSCH_JAEGER is left out intentionally here as their devices don't support colors
|
||||
],
|
||||
manufacturerNames: [
|
||||
'GLEDOPTO',
|
||||
'Trust International B.V.\u0000',
|
||||
],
|
||||
manufacturerNames: ['GLEDOPTO', 'Trust International B.V.\u0000'],
|
||||
},
|
||||
];
|
||||
|
||||
interface ParsedMQTTMessage {
|
||||
type: 'bind' | 'unbind', sourceKey: string, targetKey: string, clusters: string[], skipDisableReporting: boolean
|
||||
type: 'bind' | 'unbind';
|
||||
sourceKey: string;
|
||||
targetKey: string;
|
||||
clusters: string[];
|
||||
skipDisableReporting: boolean;
|
||||
}
|
||||
|
||||
export default class Bind extends Extension {
|
||||
@ -258,8 +263,8 @@ export default class Bind extends Extension {
|
||||
const attemptedClusters = [];
|
||||
|
||||
const bindSource: zh.Endpoint = parsedSource.endpoint;
|
||||
const bindTarget: number | zh.Group | zh.Endpoint = (target instanceof Device) ? parsedTarget.endpoint :
|
||||
((target instanceof Group) ? target.zh : Number(target.ID));
|
||||
const bindTarget: number | zh.Group | zh.Endpoint =
|
||||
target instanceof Device ? parsedTarget.endpoint : target instanceof Group ? target.zh : Number(target.ID);
|
||||
// Find which clusters are supported by both the source and target.
|
||||
// Groups are assumed to support all clusters.
|
||||
const clusterCandidates = clusters ?? ALL_CLUSTER_CANDIDATES;
|
||||
@ -270,8 +275,9 @@ export default class Bind extends Extension {
|
||||
const anyClusterValid = utils.isZHGroup(bindTarget) || typeof bindTarget === 'number' || (target as Device).zh.type === 'Coordinator';
|
||||
|
||||
if (!anyClusterValid && utils.isEndpoint(bindTarget)) {
|
||||
matchingClusters = ((bindTarget.supportsInputCluster(cluster) && bindSource.supportsOutputCluster(cluster)) ||
|
||||
(bindSource.supportsInputCluster(cluster) && bindTarget.supportsOutputCluster(cluster)));
|
||||
matchingClusters =
|
||||
(bindTarget.supportsInputCluster(cluster) && bindSource.supportsOutputCluster(cluster)) ||
|
||||
(bindSource.supportsInputCluster(cluster) && bindTarget.supportsOutputCluster(cluster));
|
||||
}
|
||||
|
||||
const sourceValid = bindSource.supportsInputCluster(cluster) || bindSource.supportsOutputCluster(cluster);
|
||||
@ -332,7 +338,7 @@ export default class Bind extends Extension {
|
||||
if (successfulClusters.length !== 0) {
|
||||
if (type === 'bind') {
|
||||
await this.setupReporting(bindSource.binds.filter((b) => successfulClusters.includes(b.cluster.name) && b.target === bindTarget));
|
||||
} else if ((typeof bindTarget !== 'number') && !skipDisableReporting) {
|
||||
} else if (typeof bindTarget !== 'number' && !skipDisableReporting) {
|
||||
await this.disableUnnecessaryReportings(bindTarget);
|
||||
}
|
||||
}
|
||||
@ -368,7 +374,8 @@ export default class Bind extends Extension {
|
||||
}
|
||||
|
||||
await this.setupReporting(bindsToGroup);
|
||||
} else { // action === remove/remove_all
|
||||
} else {
|
||||
// action === remove/remove_all
|
||||
if (!data.skipDisableReporting) {
|
||||
await this.disableUnnecessaryReportings(data.endpoint);
|
||||
}
|
||||
@ -411,7 +418,7 @@ export default class Bind extends Extension {
|
||||
|
||||
for (const c of REPORT_CLUSTERS[bind.cluster.name as ClusterName]) {
|
||||
/* istanbul ignore else */
|
||||
if (!c.condition || await c.condition(endpoint)) {
|
||||
if (!c.condition || (await c.condition(endpoint))) {
|
||||
const i = {...c};
|
||||
delete i.condition;
|
||||
|
||||
@ -458,7 +465,7 @@ export default class Bind extends Extension {
|
||||
|
||||
for (const b of endpoint.binds) {
|
||||
/* istanbul ignore else */
|
||||
if (b.target === coordinator && !requiredClusters.includes(b.cluster.name) && (b.cluster.name in REPORT_CLUSTERS)) {
|
||||
if (b.target === coordinator && !requiredClusters.includes(b.cluster.name) && b.cluster.name in REPORT_CLUSTERS) {
|
||||
boundClusters.push(b.cluster.name);
|
||||
}
|
||||
}
|
||||
@ -471,11 +478,11 @@ export default class Bind extends Extension {
|
||||
|
||||
for (const item of REPORT_CLUSTERS[cluster as ClusterName]) {
|
||||
/* istanbul ignore else */
|
||||
if (!item.condition || await item.condition(endpoint)) {
|
||||
if (!item.condition || (await item.condition(endpoint))) {
|
||||
const i = {...item};
|
||||
delete i.condition;
|
||||
|
||||
items.push({...i, maximumReportInterval: 0xFFFF});
|
||||
items.push({...i, maximumReportInterval: 0xffff});
|
||||
}
|
||||
}
|
||||
|
||||
@ -499,8 +506,8 @@ export default class Bind extends Extension {
|
||||
* When dimming the bulb via the dimmer switch the state is therefore not reported.
|
||||
* When we receive a message from a Hue dimmer we read the brightness from the bulb (if bound).
|
||||
*/
|
||||
const polls = POLL_ON_MESSAGE.filter(
|
||||
(p) => p.cluster[data.cluster as ClusterName]?.some((c) => c.type === data.type && utils.equalsPartial(data.data, c.data)),
|
||||
const polls = POLL_ON_MESSAGE.filter((p) =>
|
||||
p.cluster[data.cluster as ClusterName]?.some((c) => c.type === data.type && utils.equalsPartial(data.data, c.data)),
|
||||
);
|
||||
|
||||
if (polls.length) {
|
||||
@ -526,9 +533,11 @@ export default class Bind extends Extension {
|
||||
|
||||
for (const endpoint of toPoll) {
|
||||
for (const poll of polls) {
|
||||
if ((!poll.manufacturerIDs.includes(endpoint.getDevice().manufacturerID) &&
|
||||
!poll.manufacturerNames.includes(endpoint.getDevice().manufacturerName)) ||
|
||||
!endpoint.supportsInputCluster(poll.read.cluster)) {
|
||||
if (
|
||||
(!poll.manufacturerIDs.includes(endpoint.getDevice().manufacturerID) &&
|
||||
!poll.manufacturerNames.includes(endpoint.getDevice().manufacturerName)) ||
|
||||
!endpoint.supportsInputCluster(poll.read.cluster)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -1,31 +1,37 @@
|
||||
/* eslint-disable camelcase */
|
||||
import logger from '../util/logger';
|
||||
import utils from '../util/utils';
|
||||
import * as settings from '../util/settings';
|
||||
import Transport from 'winston-transport';
|
||||
import bind from 'bind-decorator';
|
||||
import fs from 'fs';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import JSZip from 'jszip';
|
||||
import objectAssignDeep from 'object-assign-deep';
|
||||
import Extension from './extension';
|
||||
import winston from 'winston';
|
||||
import Transport from 'winston-transport';
|
||||
import {Clusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/cluster';
|
||||
import {CustomClusters, ClusterDefinition, ClusterName} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import Device from '../model/device';
|
||||
import Group from '../model/group';
|
||||
import data from '../util/data';
|
||||
import JSZip from 'jszip';
|
||||
import fs from 'fs';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import {CustomClusters, ClusterDefinition, ClusterName} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
|
||||
import {Clusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/cluster';
|
||||
import winston from 'winston';
|
||||
import logger from '../util/logger';
|
||||
import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
|
||||
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
|
||||
|
||||
type DefinitionPayload = {
|
||||
model: string, vendor: string, description: string, exposes: zhc.Expose[], supports_ota:
|
||||
boolean, icon: string, options: zhc.Expose[],
|
||||
model: string;
|
||||
vendor: string;
|
||||
description: string;
|
||||
exposes: zhc.Expose[];
|
||||
supports_ota: boolean;
|
||||
icon: string;
|
||||
options: zhc.Expose[];
|
||||
};
|
||||
|
||||
export default class Bridge extends Extension {
|
||||
private zigbee2mqttVersion: {commitHash: string, version: string};
|
||||
private zigbee2mqttVersion: {commitHash: string; version: string};
|
||||
private zigbeeHerdsmanVersion: {version: string};
|
||||
private zigbeeHerdsmanConvertersVersion: {version: string};
|
||||
private coordinatorVersion: zh.CoordinatorVersion;
|
||||
@ -47,16 +53,16 @@ export default class Bridge extends Extension {
|
||||
'group/options': this.groupOptions,
|
||||
'group/remove': this.groupRemove,
|
||||
'group/rename': this.groupRename,
|
||||
'permit_join': this.permitJoin,
|
||||
'restart': this.restart,
|
||||
'backup': this.backup,
|
||||
permit_join: this.permitJoin,
|
||||
restart: this.restart,
|
||||
backup: this.backup,
|
||||
'touchlink/factory_reset': this.touchlinkFactoryReset,
|
||||
'touchlink/identify': this.touchlinkIdentify,
|
||||
'install_code/add': this.installCodeAdd,
|
||||
'touchlink/scan': this.touchlinkScan,
|
||||
'health_check': this.healthCheck,
|
||||
'coordinator_check': this.coordinatorCheck,
|
||||
'options': this.bridgeOptions,
|
||||
health_check: this.healthCheck,
|
||||
coordinator_check: this.coordinatorCheck,
|
||||
options: this.bridgeOptions,
|
||||
// Below are deprecated
|
||||
'config/last_seen': this.configLastSeen,
|
||||
'config/homeassistant': this.configHomeAssistant,
|
||||
@ -78,7 +84,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
if (debugToMQTTFrontend) {
|
||||
class DebugEventTransport extends Transport {
|
||||
log(info: {message: string, level: string, namespace: string}, next: () => void): void {
|
||||
log(info: {message: string; level: string; namespace: string}, next: () => void): void {
|
||||
bridgeLogging(info.message, info.level, info.namespace);
|
||||
next();
|
||||
}
|
||||
@ -87,7 +93,7 @@ export default class Bridge extends Extension {
|
||||
this.logTransport = new DebugEventTransport();
|
||||
} else {
|
||||
class EventTransport extends Transport {
|
||||
log(info: {message: string, level: string, namespace: string}, next: () => void): void {
|
||||
log(info: {message: string; level: string; namespace: string}, next: () => void): void {
|
||||
if (info.level !== 'debug') {
|
||||
bridgeLogging(info.message, info.level, info.namespace);
|
||||
}
|
||||
@ -107,9 +113,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
this.eventBus.onEntityRenamed(this, () => this.publishInfo());
|
||||
this.eventBus.onGroupMembersChanged(this, () => this.publishGroups());
|
||||
this.eventBus.onDevicesChanged(this, () => this.publishDevices() &&
|
||||
this.publishInfo() &&
|
||||
this.publishDefinitions());
|
||||
this.eventBus.onDevicesChanged(this, () => this.publishDevices() && this.publishInfo() && this.publishDefinitions());
|
||||
this.eventBus.onPermitJoinChanged(this, () => !this.zigbee.isStopping() && this.publishInfo());
|
||||
this.eventBus.onScenesChanged(this, async () => {
|
||||
await this.publishDevices();
|
||||
@ -132,8 +136,7 @@ export default class Bridge extends Extension {
|
||||
this.eventBus.onDeviceNetworkAddressChanged(this, () => this.publishDevices());
|
||||
this.eventBus.onDeviceInterview(this, async (data) => {
|
||||
await this.publishDevices();
|
||||
const payload: KeyValue =
|
||||
{friendly_name: data.device.name, status: data.status, ieee_address: data.device.ieeeAddr};
|
||||
const payload: KeyValue = {friendly_name: data.device.name, status: data.status, ieee_address: data.device.ieeeAddr};
|
||||
if (data.status === 'successful') {
|
||||
payload.supported = data.device.isSupported;
|
||||
payload.definition = this.getDefinitionPayload(data.device);
|
||||
@ -274,7 +277,9 @@ export default class Bridge extends Extension {
|
||||
@bind async backup(message: string | KeyValue): Promise<MQTTResponse> {
|
||||
await this.zigbee.backup();
|
||||
const dataPath = data.getPath();
|
||||
const files = utils.getAllFiles(dataPath).map((f) => [f, f.substring(dataPath.length + 1)])
|
||||
const files = utils
|
||||
.getAllFiles(dataPath)
|
||||
.map((f) => [f, f.substring(dataPath.length + 1)])
|
||||
.filter((f) => !f[1].startsWith('log'));
|
||||
const zip = new JSZip();
|
||||
files.forEach((f) => zip.file(f[1], fs.readFileSync(f[0])));
|
||||
@ -321,7 +326,7 @@ export default class Bridge extends Extension {
|
||||
}
|
||||
|
||||
await this.zigbee.permitJoin(value, device, time);
|
||||
const response: {value: boolean, device?: string, time?: number} = {value};
|
||||
const response: {value: boolean; device?: string; time?: number} = {value};
|
||||
if (device && typeof message === 'object') response.device = message.device;
|
||||
if (time && typeof message === 'object') response.time = message.time;
|
||||
return utils.getResponse(message, response, null);
|
||||
@ -380,8 +385,7 @@ export default class Bridge extends Extension {
|
||||
}
|
||||
|
||||
@bind async touchlinkIdentify(message: KeyValue | string): Promise<MQTTResponse> {
|
||||
if (typeof message !== 'object' || !message.hasOwnProperty('ieee_address') ||
|
||||
!message.hasOwnProperty('channel')) {
|
||||
if (typeof message !== 'object' || !message.hasOwnProperty('ieee_address') || !message.hasOwnProperty('channel')) {
|
||||
throw new Error('Invalid payload');
|
||||
}
|
||||
|
||||
@ -392,9 +396,8 @@ export default class Bridge extends Extension {
|
||||
|
||||
@bind async touchlinkFactoryReset(message: KeyValue | string): Promise<MQTTResponse> {
|
||||
let result = false;
|
||||
const payload: {ieee_address?: string, channel?: number} = {};
|
||||
if (typeof message === 'object' && message.hasOwnProperty('ieee_address') &&
|
||||
message.hasOwnProperty('channel')) {
|
||||
const payload: {ieee_address?: string; channel?: number} = {};
|
||||
if (typeof message === 'object' && message.hasOwnProperty('ieee_address') && message.hasOwnProperty('channel')) {
|
||||
logger.info(`Start Touchlink factory reset of '${message.ieee_address}' on channel ${message.channel}`);
|
||||
result = await this.zigbee.touchlinkFactoryReset(message.ieee_address, message.channel);
|
||||
payload.ieee_address = message.ieee_address;
|
||||
@ -445,7 +448,11 @@ export default class Bridge extends Extension {
|
||||
}
|
||||
|
||||
const cleanup = (o: KeyValue): KeyValue => {
|
||||
delete o.friendlyName; delete o.friendly_name; delete o.ID; delete o.type; delete o.devices;
|
||||
delete o.friendlyName;
|
||||
delete o.friendly_name;
|
||||
delete o.ID;
|
||||
delete o.type;
|
||||
delete o.devices;
|
||||
return o;
|
||||
};
|
||||
|
||||
@ -460,17 +467,19 @@ export default class Bridge extends Extension {
|
||||
logger.info(`Changed config for ${entityType} ${ID}`);
|
||||
|
||||
this.eventBus.emitEntityOptionsChanged({from: oldOptions, to: newOptions, entity});
|
||||
return utils.getResponse(
|
||||
message,
|
||||
{from: oldOptions, to: newOptions, id: ID, restart_required: this.restartRequired},
|
||||
null,
|
||||
);
|
||||
return utils.getResponse(message, {from: oldOptions, to: newOptions, id: ID, restart_required: this.restartRequired}, null);
|
||||
}
|
||||
|
||||
@bind async deviceConfigureReporting(message: string | KeyValue): Promise<MQTTResponse> {
|
||||
if (typeof message !== 'object' || !message.hasOwnProperty('id') || !message.hasOwnProperty('cluster') ||
|
||||
!message.hasOwnProperty('maximum_report_interval') || !message.hasOwnProperty('minimum_report_interval') ||
|
||||
!message.hasOwnProperty('reportable_change') || !message.hasOwnProperty('attribute')) {
|
||||
if (
|
||||
typeof message !== 'object' ||
|
||||
!message.hasOwnProperty('id') ||
|
||||
!message.hasOwnProperty('cluster') ||
|
||||
!message.hasOwnProperty('maximum_report_interval') ||
|
||||
!message.hasOwnProperty('minimum_report_interval') ||
|
||||
!message.hasOwnProperty('reportable_change') ||
|
||||
!message.hasOwnProperty('attribute')
|
||||
) {
|
||||
throw new Error(`Invalid payload`);
|
||||
}
|
||||
|
||||
@ -485,20 +494,35 @@ export default class Bridge extends Extension {
|
||||
const coordinatorEndpoint = this.zigbee.firstCoordinatorEndpoint();
|
||||
await endpoint.bind(message.cluster, coordinatorEndpoint);
|
||||
|
||||
await endpoint.configureReporting(message.cluster, [{
|
||||
attribute: message.attribute, minimumReportInterval: message.minimum_report_interval,
|
||||
maximumReportInterval: message.maximum_report_interval, reportableChange: message.reportable_change,
|
||||
}], message.options);
|
||||
await endpoint.configureReporting(
|
||||
message.cluster,
|
||||
[
|
||||
{
|
||||
attribute: message.attribute,
|
||||
minimumReportInterval: message.minimum_report_interval,
|
||||
maximumReportInterval: message.maximum_report_interval,
|
||||
reportableChange: message.reportable_change,
|
||||
},
|
||||
],
|
||||
message.options,
|
||||
);
|
||||
|
||||
await this.publishDevices();
|
||||
|
||||
logger.info(`Configured reporting for '${message.id}', '${message.cluster}.${message.attribute}'`);
|
||||
|
||||
return utils.getResponse(message, {
|
||||
id: message.id, cluster: message.cluster, maximum_report_interval: message.maximum_report_interval,
|
||||
minimum_report_interval: message.minimum_report_interval, reportable_change: message.reportable_change,
|
||||
attribute: message.attribute,
|
||||
}, null);
|
||||
return utils.getResponse(
|
||||
message,
|
||||
{
|
||||
id: message.id,
|
||||
cluster: message.cluster,
|
||||
maximum_report_interval: message.maximum_report_interval,
|
||||
minimum_report_interval: message.minimum_report_interval,
|
||||
reportable_change: message.reportable_change,
|
||||
attribute: message.attribute,
|
||||
},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
@bind async deviceInterview(message: string | KeyValue): Promise<MQTTResponse> {
|
||||
@ -537,8 +561,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
async renameEntity(entityType: 'group' | 'device', message: string | KeyValue): Promise<MQTTResponse> {
|
||||
const deviceAndHasLast = entityType === 'device' && typeof message === 'object' && message.last === true;
|
||||
if (typeof message !== 'object' || (!message.hasOwnProperty('from') && !deviceAndHasLast) ||
|
||||
!message.hasOwnProperty('to')) {
|
||||
if (typeof message !== 'object' || (!message.hasOwnProperty('from') && !deviceAndHasLast) || !message.hasOwnProperty('to')) {
|
||||
throw new Error(`Invalid payload`);
|
||||
}
|
||||
|
||||
@ -548,8 +571,7 @@ export default class Bridge extends Extension {
|
||||
|
||||
const from = deviceAndHasLast ? this.lastJoinedDeviceIeeeAddr : message.from;
|
||||
const to = message.to;
|
||||
const homeAssisantRename = message.hasOwnProperty('homeassistant_rename') ?
|
||||
message.homeassistant_rename : false;
|
||||
const homeAssisantRename = message.hasOwnProperty('homeassistant_rename') ? message.homeassistant_rename : false;
|
||||
const entity = this.getEntity(entityType, from);
|
||||
const oldFriendlyName = entity.options.friendly_name;
|
||||
|
||||
@ -570,11 +592,7 @@ export default class Bridge extends Extension {
|
||||
// Republish entity state
|
||||
await this.publishEntityState(entity, {});
|
||||
|
||||
return utils.getResponse(
|
||||
message,
|
||||
{from: oldFriendlyName, to, homeassistant_rename: homeAssisantRename},
|
||||
null,
|
||||
);
|
||||
return utils.getResponse(message, {from: oldFriendlyName, to, homeassistant_rename: homeAssisantRename}, null);
|
||||
}
|
||||
|
||||
async removeEntity(entityType: 'group' | 'device', message: string | KeyValue): Promise<MQTTResponse> {
|
||||
@ -650,9 +668,7 @@ export default class Bridge extends Extension {
|
||||
return utils.getResponse(message, {id: ID, force: force}, null);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to remove ${entityType} '${friendlyName}'${blockForceLog} (${error})`,
|
||||
);
|
||||
throw new Error(`Failed to remove ${entityType} '${friendlyName}'${blockForceLog} (${error})`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -687,16 +703,21 @@ export default class Bridge extends Extension {
|
||||
config_schema: settings.schema,
|
||||
};
|
||||
|
||||
await this.mqtt.publish(
|
||||
'bridge/info', stringify(payload), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
await this.mqtt.publish('bridge/info', stringify(payload), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
}
|
||||
|
||||
async publishDevices(): Promise<void> {
|
||||
interface Data {
|
||||
bindings: {cluster: string, target: {type: string, endpoint?: number, ieee_address?: string, id?: number}}[]
|
||||
configured_reportings: {cluster: string, attribute: string | number,
|
||||
minimum_report_interval: number, maximum_report_interval: number, reportable_change: number}[],
|
||||
clusters: {input: string[], output: string[]}, scenes: Scene[]
|
||||
bindings: {cluster: string; target: {type: string; endpoint?: number; ieee_address?: string; id?: number}}[];
|
||||
configured_reportings: {
|
||||
cluster: string;
|
||||
attribute: string | number;
|
||||
minimum_report_interval: number;
|
||||
maximum_report_interval: number;
|
||||
reportable_change: number;
|
||||
}[];
|
||||
clusters: {input: string[]; output: string[]};
|
||||
scenes: Scene[];
|
||||
}
|
||||
|
||||
const devices = this.zigbee.devices().map((device) => {
|
||||
@ -713,9 +734,9 @@ export default class Bridge extends Extension {
|
||||
};
|
||||
|
||||
for (const bind of endpoint.binds) {
|
||||
const target = utils.isEndpoint(bind.target) ?
|
||||
{type: 'endpoint', ieee_address: bind.target.getDevice().ieeeAddr, endpoint: bind.target.ID} :
|
||||
{type: 'group', id: bind.target.groupID};
|
||||
const target = utils.isEndpoint(bind.target)
|
||||
? {type: 'endpoint', ieee_address: bind.target.getDevice().ieeeAddr, endpoint: bind.target.ID}
|
||||
: {type: 'group', id: bind.target.groupID};
|
||||
data.bindings.push({cluster: bind.cluster.name, target});
|
||||
}
|
||||
|
||||
@ -752,8 +773,7 @@ export default class Bridge extends Extension {
|
||||
};
|
||||
});
|
||||
|
||||
await this.mqtt.publish('bridge/devices', stringify(devices),
|
||||
{retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
await this.mqtt.publish('bridge/devices', stringify(devices), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
}
|
||||
|
||||
async publishGroups(): Promise<void> {
|
||||
@ -768,14 +788,13 @@ export default class Bridge extends Extension {
|
||||
}),
|
||||
};
|
||||
});
|
||||
await this.mqtt.publish(
|
||||
'bridge/groups', stringify(groups), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
await this.mqtt.publish('bridge/groups', stringify(groups), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
}
|
||||
|
||||
async publishDefinitions(): Promise<void> {
|
||||
interface ClusterDefinitionPayload {
|
||||
clusters: Readonly<Record<ClusterName, Readonly<ClusterDefinition>>>,
|
||||
custom_clusters: {[key: string] : CustomClusters}
|
||||
clusters: Readonly<Record<ClusterName, Readonly<ClusterDefinition>>>;
|
||||
custom_clusters: {[key: string]: CustomClusters};
|
||||
}
|
||||
|
||||
const data: ClusterDefinitionPayload = {
|
||||
@ -789,8 +808,7 @@ export default class Bridge extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
await this.mqtt.publish('bridge/definitions', stringify(data),
|
||||
{retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
await this.mqtt.publish('bridge/definitions', stringify(data), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
|
||||
}
|
||||
|
||||
getDefinitionPayload(device: Device): DefinitionPayload {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import logger from '../util/logger';
|
||||
import bind from 'bind-decorator';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import Extension from './extension';
|
||||
import bind from 'bind-decorator';
|
||||
|
||||
import Device from '../model/device';
|
||||
import logger from '../util/logger';
|
||||
import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
|
||||
/**
|
||||
* This extension calls the zigbee-herdsman-converters definition configure() method
|
||||
@ -89,8 +90,12 @@ export default class Configure extends Extension {
|
||||
this.eventBus.onReconfigure(this, this.onReconfigure);
|
||||
}
|
||||
|
||||
private async configure(device: Device, event: 'started' | 'zigbee_event' | 'reporting_disabled' | 'mqtt_message',
|
||||
force=false, throwError=false): Promise<void> {
|
||||
private async configure(
|
||||
device: Device,
|
||||
event: 'started' | 'zigbee_event' | 'reporting_disabled' | 'mqtt_message',
|
||||
force = false,
|
||||
throwError = false,
|
||||
): Promise<void> {
|
||||
if (!force) {
|
||||
if (device.options.disabled || !device.definition?.configure || !device.zh.interviewCompleted) {
|
||||
return;
|
||||
|
@ -20,9 +20,16 @@ abstract class Extension {
|
||||
* @param {restartCallback} restartCallback Restart Zigbee2MQTT
|
||||
* @param {addExtension} addExtension Add an extension
|
||||
*/
|
||||
constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => Promise<void>, addExtension: (extension: Extension) => Promise<void>) {
|
||||
constructor(
|
||||
zigbee: Zigbee,
|
||||
mqtt: MQTT,
|
||||
state: State,
|
||||
publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus,
|
||||
enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => Promise<void>,
|
||||
addExtension: (extension: Extension) => Promise<void>,
|
||||
) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
|
@ -1,13 +1,21 @@
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import logger from '../util/logger';
|
||||
import * as settings from '../util/settings';
|
||||
import {loadExternalConverter} from '../util/utils';
|
||||
import Extension from './extension';
|
||||
import logger from '../util/logger';
|
||||
|
||||
export default class ExternalConverters extends Extension {
|
||||
constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => Promise<void>, addExtension: (extension: Extension) => Promise<void>) {
|
||||
constructor(
|
||||
zigbee: Zigbee,
|
||||
mqtt: MQTT,
|
||||
state: State,
|
||||
publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus,
|
||||
enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => Promise<void>,
|
||||
addExtension: (extension: Extension) => Promise<void>,
|
||||
) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
|
||||
|
||||
for (const file of settings.get().external_converters) {
|
||||
@ -21,10 +29,11 @@ export default class ExternalConverters extends Extension {
|
||||
logger.error(`Failed to load external converter file '${file}' (${error.message})`);
|
||||
logger.error(
|
||||
`Probably there is a syntax error in the file or the external converter is not ` +
|
||||
`compatible with the current Zigbee2MQTT version`);
|
||||
`compatible with the current Zigbee2MQTT version`,
|
||||
);
|
||||
logger.error(
|
||||
`Note that external converters are not meant for long term usage, it's meant for local ` +
|
||||
`testing after which a pull request should be created to add out-of-the-box support for the device`,
|
||||
`testing after which a pull request should be created to add out-of-the-box support for the device`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import bind from 'bind-decorator';
|
||||
import fs from 'fs';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import path from 'path';
|
||||
|
||||
import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import fs from 'fs';
|
||||
import data from './../util/data';
|
||||
import path from 'path';
|
||||
import logger from './../util/logger';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import bind from 'bind-decorator';
|
||||
import Extension from './extension';
|
||||
|
||||
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/extension/(save|remove)`);
|
||||
@ -15,7 +16,7 @@ export default class ExternalExtension extends Extension {
|
||||
|
||||
override async start(): Promise<void> {
|
||||
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
||||
this.requestLookup = {'save': this.saveExtension, 'remove': this.removeExtension};
|
||||
this.requestLookup = {save: this.saveExtension, remove: this.removeExtension};
|
||||
await this.loadUserDefinedExtensions();
|
||||
await this.publishExtensions();
|
||||
}
|
||||
@ -24,13 +25,16 @@ export default class ExternalExtension extends Extension {
|
||||
return data.joinPath('extension');
|
||||
}
|
||||
|
||||
private getListOfUserDefinedExtensions(): {name: string, code: string}[] {
|
||||
private getListOfUserDefinedExtensions(): {name: string; code: string}[] {
|
||||
const basePath = this.getExtensionsBasePath();
|
||||
if (fs.existsSync(basePath)) {
|
||||
return fs.readdirSync(basePath).filter((f) => f.endsWith('.js')).map((fileName) => {
|
||||
const extensionFilePath = path.join(basePath, fileName);
|
||||
return {'name': fileName, 'code': fs.readFileSync(extensionFilePath, 'utf-8')};
|
||||
});
|
||||
return fs
|
||||
.readdirSync(basePath)
|
||||
.filter((f) => f.endsWith('.js'))
|
||||
.map((fileName) => {
|
||||
const extensionFilePath = path.join(basePath, fileName);
|
||||
return {name: fileName, code: fs.readFileSync(extensionFilePath, 'utf-8')};
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@ -88,8 +92,7 @@ export default class ExternalExtension extends Extension {
|
||||
@bind private async loadExtension(ConstructorClass: typeof Extension): Promise<void> {
|
||||
await this.enableDisableExtension(false, ConstructorClass.name);
|
||||
// @ts-ignore
|
||||
await this.addExtension(new ConstructorClass(this.zigbee, this.mqtt, this.state, this.publishEntityState,
|
||||
this.eventBus, settings, logger));
|
||||
await this.addExtension(new ConstructorClass(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus, settings, logger));
|
||||
}
|
||||
|
||||
private async loadUserDefinedExtensions(): Promise<void> {
|
||||
@ -100,9 +103,15 @@ export default class ExternalExtension extends Extension {
|
||||
|
||||
private async publishExtensions(): Promise<void> {
|
||||
const extensions = this.getListOfUserDefinedExtensions();
|
||||
await this.mqtt.publish('bridge/extensions', stringify(extensions), {
|
||||
retain: true,
|
||||
qos: 0,
|
||||
}, settings.get().mqtt.base_topic, true);
|
||||
await this.mqtt.publish(
|
||||
'bridge/extensions',
|
||||
stringify(extensions),
|
||||
{
|
||||
retain: true,
|
||||
qos: 0,
|
||||
},
|
||||
settings.get().mqtt.base_topic,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,19 @@
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import bind from 'bind-decorator';
|
||||
import gzipStatic, {RequestHandler} from 'connect-gzip-static';
|
||||
import finalhandler from 'finalhandler';
|
||||
import logger from '../util/logger';
|
||||
import frontend from 'zigbee2mqtt-frontend';
|
||||
import WebSocket from 'ws';
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import net from 'net';
|
||||
import url from 'url';
|
||||
import fs from 'fs';
|
||||
import WebSocket from 'ws';
|
||||
import frontend from 'zigbee2mqtt-frontend';
|
||||
|
||||
import logger from '../util/logger';
|
||||
import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import Extension from './extension';
|
||||
import bind from 'bind-decorator';
|
||||
|
||||
/**
|
||||
* This extension servers the frontend
|
||||
@ -30,17 +31,24 @@ export default class Frontend extends Extension {
|
||||
private fileServer: RequestHandler;
|
||||
private wss: WebSocket.Server = null;
|
||||
|
||||
constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => Promise<void>, addExtension: (extension: Extension) => Promise<void>) {
|
||||
constructor(
|
||||
zigbee: Zigbee,
|
||||
mqtt: MQTT,
|
||||
state: State,
|
||||
publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus,
|
||||
enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => Promise<void>,
|
||||
addExtension: (extension: Extension) => Promise<void>,
|
||||
) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
|
||||
this.eventBus.onMQTTMessagePublished(this, this.onMQTTPublishMessage);
|
||||
}
|
||||
|
||||
private isHttpsConfigured():boolean {
|
||||
private isHttpsConfigured(): boolean {
|
||||
if (this.sslCert && this.sslKey) {
|
||||
if (!fs.existsSync(this.sslCert) || !fs.existsSync(this.sslKey)) {
|
||||
logger.error(`defined ssl_cert '${this.sslCert}' or ssl_key '${this.sslKey}' file path does not exists, server won't be secured.`); /* eslint-disable-line max-len */
|
||||
logger.error(`defined ssl_cert '${this.sslCert}' or ssl_key '${this.sslKey}' file path does not exists, server won't be secured.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -48,12 +56,12 @@ export default class Frontend extends Extension {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
override async start(): Promise<void> {
|
||||
if (this.isHttpsConfigured()) {
|
||||
const serverOptions = {
|
||||
key: fs.readFileSync(this.sslKey),
|
||||
cert: fs.readFileSync(this.sslCert)};
|
||||
cert: fs.readFileSync(this.sslCert),
|
||||
};
|
||||
this.server = https.createServer(serverOptions, this.onRequest);
|
||||
} else {
|
||||
this.server = http.createServer(this.onRequest);
|
||||
|
@ -1,31 +1,42 @@
|
||||
import * as settings from '../util/settings';
|
||||
import logger from '../util/logger';
|
||||
import utils from '../util/utils';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import equals from 'fast-deep-equal/es6';
|
||||
import bind from 'bind-decorator';
|
||||
import Extension from './extension';
|
||||
import equals from 'fast-deep-equal/es6';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import Device from '../model/device';
|
||||
import Group from '../model/group';
|
||||
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 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,
|
||||
'brightness': (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'brightness')),
|
||||
'color_temp': (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_temp')),
|
||||
'color': (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_xy' || f.name === 'color_hs')),
|
||||
'color_mode': (value, exposes) => exposes.some((e) => e.type === 'light' &&
|
||||
((e.features.some((f) => f.name === `color_${value}`)) || (value === 'color_temp' && e.features.some((f) => f.name === 'color_temp')))),
|
||||
state: () => true,
|
||||
brightness: (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'brightness')),
|
||||
color_temp: (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_temp')),
|
||||
color: (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_xy' || f.name === 'color_hs')),
|
||||
color_mode: (value, exposes) =>
|
||||
exposes.some(
|
||||
(e) =>
|
||||
e.type === 'light' &&
|
||||
(e.features.some((f) => f.name === `color_${value}`) || (value === 'color_temp' && e.features.some((f) => f.name === 'color_temp'))),
|
||||
),
|
||||
};
|
||||
|
||||
interface ParsedMQTTMessage {
|
||||
type: 'remove' | 'add' | 'remove_all', resolvedEntityGroup: Group, resolvedEntityDevice: Device,
|
||||
error: string, groupKey: string, deviceKey: string, triggeredViaLegacyApi: boolean,
|
||||
skipDisableReporting: boolean, resolvedEntityEndpoint: zh.Endpoint,
|
||||
type: 'remove' | 'add' | 'remove_all';
|
||||
resolvedEntityGroup: Group;
|
||||
resolvedEntityDevice: Device;
|
||||
error: string;
|
||||
groupKey: string;
|
||||
deviceKey: string;
|
||||
triggeredViaLegacyApi: boolean;
|
||||
skipDisableReporting: boolean;
|
||||
resolvedEntityEndpoint: zh.Endpoint;
|
||||
}
|
||||
|
||||
export default class Groups extends Extension {
|
||||
@ -42,8 +53,13 @@ export default class Groups extends Extension {
|
||||
const settingsGroups = settings.getGroups();
|
||||
const zigbeeGroups = this.zigbee.groups();
|
||||
|
||||
const addRemoveFromGroup = async (action: 'add' | 'remove', deviceName: string, groupName: string | number,
|
||||
endpoint: zh.Endpoint, group: Group): Promise<void> => {
|
||||
const addRemoveFromGroup = async (
|
||||
action: 'add' | 'remove',
|
||||
deviceName: string,
|
||||
groupName: string | number,
|
||||
endpoint: zh.Endpoint,
|
||||
group: Group,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
logger.info(`${action === 'add' ? 'Adding' : 'Removing'} '${deviceName}' to group '${groupName}'`);
|
||||
|
||||
@ -119,7 +135,8 @@ export default class Groups extends Extension {
|
||||
let endpointName: string = null;
|
||||
const endpointNames: string[] = data.entity instanceof Device ? data.entity.getEndpointNames() : [];
|
||||
|
||||
for (let [prop, value] of Object.entries(data.update)) {
|
||||
for (let prop of Object.keys(data.update)) {
|
||||
const value = data.update[prop];
|
||||
const endpointNameMatch = endpointNames.find((n) => prop.endsWith(`_${n}`));
|
||||
|
||||
if (endpointNameMatch) {
|
||||
@ -140,8 +157,11 @@ export default class Groups extends Extension {
|
||||
|
||||
if (entity instanceof Device) {
|
||||
for (const group of groups) {
|
||||
if (group.zh.hasMember(entity.endpoint(endpointName)) && !equals(this.lastOptimisticState[group.ID], payload) &&
|
||||
this.shouldPublishPayloadForGroup(group, payload)) {
|
||||
if (
|
||||
group.zh.hasMember(entity.endpoint(endpointName)) &&
|
||||
!equals(this.lastOptimisticState[group.ID], payload) &&
|
||||
this.shouldPublishPayloadForGroup(group, payload)
|
||||
) {
|
||||
this.lastOptimisticState[group.ID] = payload;
|
||||
|
||||
await this.publishEntityState(group, payload, reason);
|
||||
@ -197,7 +217,7 @@ export default class Groups extends Extension {
|
||||
}
|
||||
|
||||
private shouldPublishPayloadForGroup(group: Group, payload: KeyValue): boolean {
|
||||
return ((group.options.off_state === 'last_member_state') || (!payload || payload.state !== 'OFF') || this.areAllMembersOff(group));
|
||||
return group.options.off_state === 'last_member_state' || !payload || payload.state !== 'OFF' || this.areAllMembersOff(group);
|
||||
}
|
||||
|
||||
private areAllMembersOff(group: Group): boolean {
|
||||
@ -263,7 +283,7 @@ export default class Groups extends Extension {
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const message = {friendly_name: data.message, group: legacyTopicRegexMatch[1], error: 'entity doesn\'t exists'};
|
||||
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}));
|
||||
}
|
||||
@ -309,8 +329,15 @@ export default class Groups extends Extension {
|
||||
}
|
||||
|
||||
return {
|
||||
resolvedEntityGroup, resolvedEntityDevice, type, error, groupKey, deviceKey,
|
||||
triggeredViaLegacyApi, skipDisableReporting, resolvedEntityEndpoint,
|
||||
resolvedEntityGroup,
|
||||
resolvedEntityDevice,
|
||||
type,
|
||||
error,
|
||||
groupKey,
|
||||
deviceKey,
|
||||
triggeredViaLegacyApi,
|
||||
skipDisableReporting,
|
||||
resolvedEntityEndpoint,
|
||||
};
|
||||
}
|
||||
|
||||
@ -321,10 +348,17 @@ export default class Groups extends Extension {
|
||||
return;
|
||||
}
|
||||
|
||||
let {
|
||||
resolvedEntityGroup, resolvedEntityDevice, type, error, triggeredViaLegacyApi,
|
||||
groupKey, deviceKey, skipDisableReporting, resolvedEntityEndpoint,
|
||||
const {
|
||||
resolvedEntityGroup,
|
||||
resolvedEntityDevice,
|
||||
type,
|
||||
triggeredViaLegacyApi,
|
||||
groupKey,
|
||||
deviceKey,
|
||||
skipDisableReporting,
|
||||
resolvedEntityEndpoint,
|
||||
} = parsed;
|
||||
let error = parsed.error;
|
||||
let changedGroups: Group[] = [];
|
||||
|
||||
if (!error) {
|
||||
@ -369,7 +403,8 @@ export default class Groups extends Extension {
|
||||
|
||||
await this.mqtt.publish('bridge/log', stringify({type: `device_group_remove`, message}));
|
||||
}
|
||||
} else { // remove_all
|
||||
} else {
|
||||
// remove_all
|
||||
logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`);
|
||||
changedGroups = this.zigbee.groups().filter((g) => g.zh.members.includes(resolvedEntityEndpoint));
|
||||
await resolvedEntityEndpoint.removeFromAllGroups();
|
||||
|
@ -1,16 +1,25 @@
|
||||
import * as settings from '../util/settings';
|
||||
import logger from '../util/logger';
|
||||
import utils, {isNumericExposeFeature, isBinaryExposeFeature, isEnumExposeFeature} from '../util/utils';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import assert from 'assert';
|
||||
import Extension from './extension';
|
||||
import bind from 'bind-decorator';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
interface MockProperty {property: string, value: KeyValue | string}
|
||||
import logger from '../util/logger';
|
||||
import * as settings from '../util/settings';
|
||||
import utils, {isNumericExposeFeature, isBinaryExposeFeature, isEnumExposeFeature} from '../util/utils';
|
||||
import Extension from './extension';
|
||||
|
||||
interface MockProperty {
|
||||
property: string;
|
||||
value: KeyValue | string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
interface DiscoveryEntry {mockProperties: MockProperty[], type: string, object_id: string, discovery_payload: KeyValue}
|
||||
interface DiscoveryEntry {
|
||||
mockProperties: MockProperty[];
|
||||
type: string;
|
||||
object_id: string;
|
||||
discovery_payload: KeyValue;
|
||||
}
|
||||
|
||||
const sensorClick: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
@ -24,10 +33,10 @@ const sensorClick: DiscoveryEntry = {
|
||||
};
|
||||
|
||||
interface Discovered {
|
||||
mockProperties: Set<MockProperty>,
|
||||
messages: {[s: string]: {payload: string, published: boolean}},
|
||||
triggers: Set<string>,
|
||||
discovered: boolean,
|
||||
mockProperties: Set<MockProperty>;
|
||||
messages: {[s: string]: {payload: string; published: boolean}};
|
||||
triggers: Set<string>;
|
||||
discovered: boolean;
|
||||
}
|
||||
|
||||
const ACCESS_STATE = 0b001;
|
||||
@ -37,11 +46,36 @@ const defaultStatusTopic = 'homeassistant/status';
|
||||
|
||||
const legacyMapping = [
|
||||
{
|
||||
models: ['WXKG01LM', 'HS1EB/HS1EB-E', 'ICZB-KPD14S', 'TERNCY-SD01', 'TERNCY-PP01', 'ICZB-KPD18S',
|
||||
'E1766', 'ZWallRemote0', 'ptvo.switch', '2AJZ4KPKEY', 'ZGRC-KEY-013', 'HGZB-02S', 'HGZB-045',
|
||||
'HGZB-1S', 'AV2010/34', 'IM6001-BTP01', 'WXKG11LM', 'WXKG03LM', 'WXKG02LM_rev1', 'WXKG02LM_rev2',
|
||||
'QBKG04LM', 'QBKG03LM', 'QBKG11LM', 'QBKG21LM', 'QBKG22LM', 'WXKG12LM', 'QBKG12LM',
|
||||
'E1743'],
|
||||
models: [
|
||||
'WXKG01LM',
|
||||
'HS1EB/HS1EB-E',
|
||||
'ICZB-KPD14S',
|
||||
'TERNCY-SD01',
|
||||
'TERNCY-PP01',
|
||||
'ICZB-KPD18S',
|
||||
'E1766',
|
||||
'ZWallRemote0',
|
||||
'ptvo.switch',
|
||||
'2AJZ4KPKEY',
|
||||
'ZGRC-KEY-013',
|
||||
'HGZB-02S',
|
||||
'HGZB-045',
|
||||
'HGZB-1S',
|
||||
'AV2010/34',
|
||||
'IM6001-BTP01',
|
||||
'WXKG11LM',
|
||||
'WXKG03LM',
|
||||
'WXKG02LM_rev1',
|
||||
'WXKG02LM_rev2',
|
||||
'QBKG04LM',
|
||||
'QBKG03LM',
|
||||
'QBKG11LM',
|
||||
'QBKG21LM',
|
||||
'QBKG22LM',
|
||||
'WXKG12LM',
|
||||
'QBKG12LM',
|
||||
'E1743',
|
||||
],
|
||||
discovery: sensorClick,
|
||||
},
|
||||
{
|
||||
@ -78,16 +112,26 @@ class Bridge {
|
||||
private discoveryEntries: DiscoveryEntry[];
|
||||
|
||||
readonly options: {
|
||||
ID?: string,
|
||||
homeassistant?: KeyValue,
|
||||
ID?: string;
|
||||
homeassistant?: KeyValue;
|
||||
};
|
||||
|
||||
/* eslint-disable brace-style */
|
||||
get ID(): string {return this.coordinatorIeeeAddress;}
|
||||
get name(): string {return 'bridge';}
|
||||
get hardwareVersion(): string {return this.coordinatorType;}
|
||||
get firmwareVersion(): string {return this.coordinatorFirmwareVersion;}
|
||||
get configs(): DiscoveryEntry[] {return this.discoveryEntries;}
|
||||
get ID(): string {
|
||||
return this.coordinatorIeeeAddress;
|
||||
}
|
||||
get name(): string {
|
||||
return 'bridge';
|
||||
}
|
||||
get hardwareVersion(): string {
|
||||
return this.coordinatorType;
|
||||
}
|
||||
get firmwareVersion(): string {
|
||||
return this.coordinatorFirmwareVersion;
|
||||
}
|
||||
get configs(): DiscoveryEntry[] {
|
||||
return this.discoveryEntries;
|
||||
}
|
||||
|
||||
constructor(ieeeAdress: string, version: zh.CoordinatorVersion, discovery: DiscoveryEntry[]) {
|
||||
this.coordinatorIeeeAddress = ieeeAdress;
|
||||
@ -104,8 +148,12 @@ class Bridge {
|
||||
};
|
||||
}
|
||||
|
||||
isDevice(): this is Device {return false;}
|
||||
isGroup(): this is Group {return false;}
|
||||
isDevice(): this is Device {
|
||||
return false;
|
||||
}
|
||||
isGroup(): this is Group {
|
||||
return false;
|
||||
}
|
||||
/* eslint-enable brace-style */
|
||||
}
|
||||
|
||||
@ -120,13 +168,20 @@ export default class HomeAssistant extends Extension {
|
||||
private statusTopic = settings.get().homeassistant.status_topic;
|
||||
private entityAttributes = settings.get().homeassistant.legacy_entity_attributes;
|
||||
private zigbee2MQTTVersion: string;
|
||||
private discoveryOrigin: {name: string, sw: string, url: string};
|
||||
private discoveryOrigin: {name: string; sw: string; url: string};
|
||||
private bridge: Bridge;
|
||||
private bridgeIdentifier: string;
|
||||
|
||||
constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => Promise<void>, addExtension: (extension: Extension) => Promise<void>) {
|
||||
constructor(
|
||||
zigbee: Zigbee,
|
||||
mqtt: MQTT,
|
||||
state: State,
|
||||
publishEntityState: PublishEntityState,
|
||||
eventBus: EventBus,
|
||||
enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
|
||||
restartCallback: () => Promise<void>,
|
||||
addExtension: (extension: Extension) => Promise<void>,
|
||||
) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
|
||||
if (settings.get().advanced.output === 'attribute') {
|
||||
throw new Error('Home Assistant integration is not possible with attribute output!');
|
||||
@ -193,19 +248,21 @@ export default class HomeAssistant extends Extension {
|
||||
return this.discovered[ID];
|
||||
}
|
||||
|
||||
private exposeToConfig(exposes: zhc.Expose[], entityType: 'device' | 'group',
|
||||
allExposes: zhc.Expose[], definition?: zhc.Definition): DiscoveryEntry[] {
|
||||
private exposeToConfig(
|
||||
exposes: zhc.Expose[],
|
||||
entityType: 'device' | 'group',
|
||||
allExposes: zhc.Expose[],
|
||||
definition?: zhc.Definition,
|
||||
): DiscoveryEntry[] {
|
||||
// For groups an array of exposes (of the same type) is passed, this is to determine e.g. what features
|
||||
// to use for a bulb (e.g. color_xy/color_temp)
|
||||
assert(entityType === 'group' || exposes.length === 1, 'Multiple exposes for device not allowed');
|
||||
const firstExpose = exposes[0];
|
||||
assert(entityType === 'device' || groupSupportedTypes.includes(firstExpose.type),
|
||||
`Unsupported expose type ${firstExpose.type} for group`);
|
||||
assert(entityType === 'device' || groupSupportedTypes.includes(firstExpose.type), `Unsupported expose type ${firstExpose.type} for group`);
|
||||
|
||||
const discoveryEntries: DiscoveryEntry[] = [];
|
||||
const endpoint = entityType === 'device' ? exposes[0].endpoint : undefined;
|
||||
const getProperty = (feature: zhc.Feature): string => entityType === 'group' ?
|
||||
featurePropertyWithoutEndpoint(feature) : feature.property;
|
||||
const getProperty = (feature: zhc.Feature): string => (entityType === 'group' ? featurePropertyWithoutEndpoint(feature) : feature.property);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (firstExpose.type === 'light') {
|
||||
@ -216,9 +273,10 @@ export default class HomeAssistant extends Extension {
|
||||
const state = firstExpose.features.find((f) => f.name === 'state');
|
||||
// Prefer HS over XY when at least one of the lights in the group prefers HS over XY.
|
||||
// A light prefers HS over XY when HS is earlier in the feature array than HS.
|
||||
const preferHS = exposes.map((e) => [e.features.findIndex((ee) => ee.name === 'color_xy'),
|
||||
e.features.findIndex((ee) => ee.name === 'color_hs')])
|
||||
.filter((d) => d[0] !== -1 && d[1] !== -1 && d[1] < d[0]).length !== 0;
|
||||
const preferHS =
|
||||
exposes
|
||||
.map((e) => [e.features.findIndex((ee) => ee.name === 'color_xy'), e.features.findIndex((ee) => ee.name === 'color_hs')])
|
||||
.filter((d) => d[0] !== -1 && d[1] !== -1 && d[1] < d[0]).length !== 0;
|
||||
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'light',
|
||||
@ -246,16 +304,24 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (hasColorTemp) {
|
||||
const colorTemps = exposes.map((expose) => expose.features.find((e) => e.name === 'color_temp'))
|
||||
.filter((e) => e).filter(isNumericExposeFeature);
|
||||
const colorTemps = exposes
|
||||
.map((expose) => expose.features.find((e) => e.name === 'color_temp'))
|
||||
.filter((e) => e)
|
||||
.filter(isNumericExposeFeature);
|
||||
const max = Math.min(...colorTemps.map((e) => e.value_max));
|
||||
const min = Math.max(...colorTemps.map((e) => e.value_min));
|
||||
discoveryEntry.discovery_payload.max_mireds = max;
|
||||
discoveryEntry.discovery_payload.min_mireds = min;
|
||||
}
|
||||
|
||||
const effects = utils.arrayUnique(utils.flatten(
|
||||
allExposes.filter(isEnumExposeFeature).filter((e) => e.name === 'effect').map((e) => e.values)));
|
||||
const effects = utils.arrayUnique(
|
||||
utils.flatten(
|
||||
allExposes
|
||||
.filter(isEnumExposeFeature)
|
||||
.filter((e) => e.name === 'effect')
|
||||
.map((e) => e.values),
|
||||
),
|
||||
);
|
||||
if (effects.length) {
|
||||
discoveryEntry.discovery_payload.effect = true;
|
||||
discoveryEntry.discovery_payload.effect_list = effects;
|
||||
@ -295,8 +361,7 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else if (firstExpose.type === 'climate') {
|
||||
const setpointProperties = ['occupied_heating_setpoint', 'current_heating_setpoint'];
|
||||
const setpoint = firstExpose.features.filter(isNumericExposeFeature)
|
||||
.find((f) => setpointProperties.includes(f.name));
|
||||
const setpoint = firstExpose.features.filter(isNumericExposeFeature).find((f) => setpointProperties.includes(f.name));
|
||||
assert(setpoint, 'No setpoint found');
|
||||
const temperature = firstExpose.features.find((f) => f.name === 'local_temperature');
|
||||
assert(temperature, 'No temperature found');
|
||||
@ -339,45 +404,39 @@ export default class HomeAssistant extends Extension {
|
||||
if (state) {
|
||||
discoveryEntry.mockProperties.push({property: state.property, value: null});
|
||||
discoveryEntry.discovery_payload.action_topic = true;
|
||||
discoveryEntry.discovery_payload.action_template = `{% set values = ` +
|
||||
`{None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'}` +
|
||||
` %}{{ values[value_json.${state.property}] }}`;
|
||||
discoveryEntry.discovery_payload.action_template =
|
||||
`{% set values = ` +
|
||||
`{None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'}` +
|
||||
` %}{{ values[value_json.${state.property}] }}`;
|
||||
}
|
||||
|
||||
const coolingSetpoint = firstExpose.features.find((f) => f.name === 'occupied_cooling_setpoint');
|
||||
if (coolingSetpoint) {
|
||||
discoveryEntry.discovery_payload.temperature_low_command_topic = setpoint.name;
|
||||
discoveryEntry.discovery_payload.temperature_low_state_template =
|
||||
`{{ value_json.${setpoint.property} }}`;
|
||||
discoveryEntry.discovery_payload.temperature_low_state_template = `{{ value_json.${setpoint.property} }}`;
|
||||
discoveryEntry.discovery_payload.temperature_low_state_topic = true;
|
||||
discoveryEntry.discovery_payload.temperature_high_command_topic = coolingSetpoint.name;
|
||||
discoveryEntry.discovery_payload.temperature_high_state_template =
|
||||
`{{ value_json.${coolingSetpoint.property} }}`;
|
||||
discoveryEntry.discovery_payload.temperature_high_state_template = `{{ value_json.${coolingSetpoint.property} }}`;
|
||||
discoveryEntry.discovery_payload.temperature_high_state_topic = true;
|
||||
} else {
|
||||
discoveryEntry.discovery_payload.temperature_command_topic = setpoint.name;
|
||||
discoveryEntry.discovery_payload.temperature_state_template =
|
||||
`{{ value_json.${setpoint.property} }}`;
|
||||
discoveryEntry.discovery_payload.temperature_state_template = `{{ value_json.${setpoint.property} }}`;
|
||||
discoveryEntry.discovery_payload.temperature_state_topic = true;
|
||||
}
|
||||
|
||||
const fanMode = firstExpose.features.filter(isEnumExposeFeature)
|
||||
.find((f) => f.name === 'fan_mode');
|
||||
const fanMode = firstExpose.features.filter(isEnumExposeFeature).find((f) => f.name === 'fan_mode');
|
||||
if (fanMode) {
|
||||
discoveryEntry.discovery_payload.fan_modes = fanMode.values;
|
||||
discoveryEntry.discovery_payload.fan_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.fan_mode_state_template =
|
||||
`{{ value_json.${fanMode.property} }}`;
|
||||
discoveryEntry.discovery_payload.fan_mode_state_template = `{{ value_json.${fanMode.property} }}`;
|
||||
discoveryEntry.discovery_payload.fan_mode_state_topic = true;
|
||||
}
|
||||
|
||||
const swingMode = firstExpose.features.filter(isEnumExposeFeature)
|
||||
.find((f) => f.name === 'swing_mode');
|
||||
const swingMode = firstExpose.features.filter(isEnumExposeFeature).find((f) => f.name === 'swing_mode');
|
||||
if (swingMode) {
|
||||
discoveryEntry.discovery_payload.swing_modes = swingMode.values;
|
||||
discoveryEntry.discovery_payload.swing_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.swing_mode_state_template =
|
||||
`{{ value_json.${swingMode.property} }}`;
|
||||
discoveryEntry.discovery_payload.swing_mode_state_template = `{{ value_json.${swingMode.property} }}`;
|
||||
discoveryEntry.discovery_payload.swing_mode_state_topic = true;
|
||||
}
|
||||
|
||||
@ -385,13 +444,11 @@ export default class HomeAssistant extends Extension {
|
||||
if (preset) {
|
||||
discoveryEntry.discovery_payload.preset_modes = preset.values;
|
||||
discoveryEntry.discovery_payload.preset_mode_command_topic = 'preset';
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template =
|
||||
`{{ value_json.${preset.property} }}`;
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json.${preset.property} }}`;
|
||||
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
|
||||
}
|
||||
|
||||
const tempCalibration = firstExpose.features.filter(isNumericExposeFeature)
|
||||
.find((f) => f.name === 'local_temperature_calibration');
|
||||
const tempCalibration = firstExpose.features.filter(isNumericExposeFeature).find((f) => f.name === 'local_temperature_calibration');
|
||||
if (tempCalibration) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'number',
|
||||
@ -418,8 +475,7 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
}
|
||||
|
||||
const piHeatingDemand = firstExpose.features.filter(isNumericExposeFeature)
|
||||
.find((f) => f.name === 'pi_heating_demand');
|
||||
const piHeatingDemand = firstExpose.features.filter(isNumericExposeFeature).find((f) => f.name === 'pi_heating_demand');
|
||||
if (piHeatingDemand) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
@ -480,13 +536,13 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else if (firstExpose.type === 'cover') {
|
||||
const state = exposes.find((expose) => expose.features.find((e) => e.name === 'state'))
|
||||
?.features.find((f) => f.name === 'state');
|
||||
const position = exposes.find((expose) => expose.features.find((e) => e.name === 'position'))
|
||||
const state = exposes.find((expose) => expose.features.find((e) => e.name === 'state'))?.features.find((f) => f.name === 'state');
|
||||
const position = exposes
|
||||
.find((expose) => expose.features.find((e) => e.name === 'position'))
|
||||
?.features.find((f) => f.name === 'position');
|
||||
const tilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'))
|
||||
?.features.find((f) => f.name === 'tilt');
|
||||
const motorState = allExposes?.filter(isEnumExposeFeature)
|
||||
const tilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'))?.features.find((f) => f.name === 'tilt');
|
||||
const motorState = allExposes
|
||||
?.filter(isEnumExposeFeature)
|
||||
.find((e) => ['motor_state', 'moving'].includes(e.name) && e.access === ACCESS_STATE);
|
||||
const running = allExposes?.find((e) => e.type === 'binary' && e.name === 'running');
|
||||
|
||||
@ -506,7 +562,8 @@ export default class HomeAssistant extends Extension {
|
||||
// If curtains have `running` property, use this in discovery.
|
||||
// The movement direction is calculated (assumed) in this case.
|
||||
if (running) {
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${running.property}" in value_json ` +
|
||||
discoveryEntry.discovery_payload.value_template =
|
||||
`{% if "${running.property}" in value_json ` +
|
||||
`and value_json.${running.property} %} {% if value_json.${position.property} > 0 %} closing ` +
|
||||
`{% else %} opening {% endif %} {% else %} stopped {% endif %}`;
|
||||
}
|
||||
@ -526,7 +583,8 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.state_opening = openingState;
|
||||
discoveryEntry.discovery_payload.state_closing = closingState;
|
||||
discoveryEntry.discovery_payload.state_stopped = stoppedState;
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${motorState.property}" in value_json ` +
|
||||
discoveryEntry.discovery_payload.value_template =
|
||||
`{% if "${motorState.property}" in value_json ` +
|
||||
`and value_json.${motorState.property} %} {{ value_json.${motorState.property} }} {% else %} ` +
|
||||
`${stoppedState} {% endif %}`;
|
||||
}
|
||||
@ -534,9 +592,8 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
// If curtains do not have `running`, `motor_state` or `moving` properties.
|
||||
if (!discoveryEntry.discovery_payload.value_template) {
|
||||
discoveryEntry.discovery_payload.value_template =
|
||||
`{{ value_json.${featurePropertyWithoutEndpoint(state)} }}`,
|
||||
discoveryEntry.discovery_payload.state_open = 'OPEN';
|
||||
(discoveryEntry.discovery_payload.value_template = `{{ value_json.${featurePropertyWithoutEndpoint(state)} }}`),
|
||||
(discoveryEntry.discovery_payload.state_open = 'OPEN');
|
||||
discoveryEntry.discovery_payload.state_closed = 'CLOSE';
|
||||
discoveryEntry.discovery_payload.state_stopped = 'STOP';
|
||||
}
|
||||
@ -546,7 +603,8 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (position) {
|
||||
discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload,
|
||||
discoveryEntry.discovery_payload = {
|
||||
...discoveryEntry.discovery_payload,
|
||||
position_template: `{{ value_json.${featurePropertyWithoutEndpoint(position)} }}`,
|
||||
set_position_template: `{ "${getProperty(position)}": {{ position }} }`,
|
||||
set_position_topic: true,
|
||||
@ -555,7 +613,8 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (tilt) {
|
||||
discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload,
|
||||
discoveryEntry.discovery_payload = {
|
||||
...discoveryEntry.discovery_payload,
|
||||
tilt_command_topic: true,
|
||||
tilt_status_topic: true,
|
||||
tilt_status_template: `{{ value_json.${featurePropertyWithoutEndpoint(tilt)} }}`,
|
||||
@ -592,8 +651,9 @@ export default class HomeAssistant extends Extension {
|
||||
// presets "on", "auto" and "smart" to cover the remaining modes in
|
||||
// ZCL. This supports a generic ZCL HVAC Fan Control fan. "Off" is
|
||||
// always a valid speed.
|
||||
let speeds = ['off'].concat(['low', 'medium', 'high', '1', '2', '3', '4', '5',
|
||||
'6', '7', '8', '9'].filter((s) => speed.values.includes(s)));
|
||||
let speeds = ['off'].concat(
|
||||
['low', 'medium', 'high', '1', '2', '3', '4', '5', '6', '7', '8', '9'].filter((s) => speed.values.includes(s)),
|
||||
);
|
||||
let presets = ['on', 'auto', 'smart'].filter((s) => speed.values.includes(s));
|
||||
|
||||
if (['99432'].includes(definition.model)) {
|
||||
@ -613,24 +673,21 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
discoveryEntry.discovery_payload.percentage_state_topic = true;
|
||||
discoveryEntry.discovery_payload.percentage_command_topic = true;
|
||||
discoveryEntry.discovery_payload.percentage_value_template =
|
||||
`{{ {${percentValues}}[value_json.${speed.property}] | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.percentage_command_template =
|
||||
`{{ {${percentCommands}}[value] | default('') }}`;
|
||||
discoveryEntry.discovery_payload.percentage_value_template = `{{ {${percentValues}}[value_json.${speed.property}] | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.percentage_command_template = `{{ {${percentCommands}}[value] | default('') }}`;
|
||||
discoveryEntry.discovery_payload.speed_range_min = 1;
|
||||
discoveryEntry.discovery_payload.speed_range_max = speeds.length - 1;
|
||||
assert(presets.length !== 0);
|
||||
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
|
||||
discoveryEntry.discovery_payload.preset_mode_command_topic = 'fan_mode';
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template =
|
||||
`{{ value_json.${speed.property} if value_json.${speed.property} in [${presetList}]` +
|
||||
` else 'None' | default('None') }}`;
|
||||
`{{ value_json.${speed.property} if value_json.${speed.property} in [${presetList}]` + ` else 'None' | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.preset_modes = presets;
|
||||
}
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else if (isBinaryExposeFeature(firstExpose)) {
|
||||
const lookup: {[s: string]: KeyValue}= {
|
||||
const lookup: {[s: string]: KeyValue} = {
|
||||
activity_led_indicator: {icon: 'mdi:led-on'},
|
||||
auto_off: {icon: 'mdi:flash-auto'},
|
||||
battery_low: {entity_category: 'diagnostic', device_class: 'battery'},
|
||||
@ -699,14 +756,13 @@ export default class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'switch',
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
object_id: endpoint ?
|
||||
`switch_${firstExpose.name}_${endpoint}` :
|
||||
`switch_${firstExpose.name}`,
|
||||
object_id: endpoint ? `switch_${firstExpose.name}_${endpoint}` : `switch_${firstExpose.name}`,
|
||||
discovery_payload: {
|
||||
name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label,
|
||||
value_template: typeof firstExpose.value_on === 'boolean' ?
|
||||
`{% if value_json.${firstExpose.property} %} true {% else %} false {% endif %}` :
|
||||
`{{ value_json.${firstExpose.property} }}`,
|
||||
value_template:
|
||||
typeof firstExpose.value_on === 'boolean'
|
||||
? `{% if value_json.${firstExpose.property} %} true {% else %} false {% endif %}`
|
||||
: `{{ value_json.${firstExpose.property} }}`,
|
||||
payload_on: firstExpose.value_on.toString(),
|
||||
payload_off: firstExpose.value_off.toString(),
|
||||
command_topic: true,
|
||||
@ -735,15 +791,12 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
} else if (isNumericExposeFeature(firstExpose)) {
|
||||
const lookup: {[s: string]: KeyValue} = {
|
||||
ac_frequency: {device_class: 'frequency', enabled_by_default: false, entity_category: 'diagnostic',
|
||||
state_class: 'measurement'},
|
||||
ac_frequency: {device_class: 'frequency', enabled_by_default: false, entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
action_duration: {icon: 'mdi:timer', device_class: 'duration'},
|
||||
alarm_humidity_max: {device_class: 'humidity', entity_category: 'config', icon: 'mdi:water-plus'},
|
||||
alarm_humidity_min: {device_class: 'humidity', entity_category: 'config', icon: 'mdi:water-minus'},
|
||||
alarm_temperature_max: {device_class: 'temperature', entity_category: 'config',
|
||||
icon: 'mdi:thermometer-high'},
|
||||
alarm_temperature_min: {device_class: 'temperature', entity_category: 'config',
|
||||
icon: 'mdi:thermometer-low'},
|
||||
alarm_temperature_max: {device_class: 'temperature', entity_category: 'config', icon: 'mdi:thermometer-high'},
|
||||
alarm_temperature_min: {device_class: 'temperature', entity_category: 'config', icon: 'mdi:thermometer-low'},
|
||||
angle: {icon: 'angle-acute'},
|
||||
angle_axis: {icon: 'angle-acute'},
|
||||
aqi: {device_class: 'aqi', state_class: 'measurement'},
|
||||
@ -756,8 +809,7 @@ export default class HomeAssistant extends Extension {
|
||||
ballast_physical_minimum_level: {entity_category: 'diagnostic'},
|
||||
battery: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
battery2: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
battery_voltage: {device_class: 'voltage', entity_category: 'diagnostic', state_class: 'measurement',
|
||||
enabled_by_default: true},
|
||||
battery_voltage: {device_class: 'voltage', entity_category: 'diagnostic', state_class: 'measurement', enabled_by_default: true},
|
||||
boost_heating_countdown: {device_class: 'duration'},
|
||||
boost_heating_countdown_time_set: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
boost_time: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
@ -766,7 +818,9 @@ export default class HomeAssistant extends Extension {
|
||||
co2: {device_class: 'carbon_dioxide', state_class: 'measurement'},
|
||||
comfort_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
cpu_temperature: {
|
||||
device_class: 'temperature', entity_category: 'diagnostic', state_class: 'measurement',
|
||||
device_class: 'temperature',
|
||||
entity_category: 'diagnostic',
|
||||
state_class: 'measurement',
|
||||
},
|
||||
cube_side: {icon: 'mdi:cube'},
|
||||
current: {
|
||||
@ -790,7 +844,9 @@ export default class HomeAssistant extends Extension {
|
||||
deadzone_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
detection_interval: {icon: 'mdi:timer'},
|
||||
device_temperature: {
|
||||
device_class: 'temperature', entity_category: 'diagnostic', state_class: 'measurement',
|
||||
device_class: 'temperature',
|
||||
entity_category: 'diagnostic',
|
||||
state_class: 'measurement',
|
||||
},
|
||||
duration: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
eco2: {device_class: 'carbon_dioxide', state_class: 'measurement'},
|
||||
@ -832,18 +888,21 @@ export default class HomeAssistant extends Extension {
|
||||
people: {state_class: 'measurement', icon: 'mdi:account-multiple'},
|
||||
position: {icon: 'mdi:valve', state_class: 'measurement'},
|
||||
power: {device_class: 'power', entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
power_factor: {device_class: 'power_factor', enabled_by_default: false,
|
||||
entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
power_factor: {device_class: 'power_factor', enabled_by_default: false, entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
power_outage_count: {icon: 'mdi:counter', enabled_by_default: false},
|
||||
precision: {entity_category: 'config', icon: 'mdi:decimal-comma-increase'},
|
||||
pressure: {device_class: 'atmospheric_pressure', state_class: 'measurement'},
|
||||
presence_timeout: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
reporting_time: {entity_category: 'config', icon: 'mdi:clock-time-one-outline'},
|
||||
requested_brightness_level: {
|
||||
enabled_by_default: false, entity_category: 'diagnostic', icon: 'mdi:brightness-5',
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
icon: 'mdi:brightness-5',
|
||||
},
|
||||
requested_brightness_percent: {
|
||||
enabled_by_default: false, entity_category: 'diagnostic', icon: 'mdi:brightness-5',
|
||||
enabled_by_default: false,
|
||||
entity_category: 'diagnostic',
|
||||
icon: 'mdi:brightness-5',
|
||||
},
|
||||
smoke_density: {icon: 'mdi:google-circles-communities', state_class: 'measurement'},
|
||||
soil_moisture: {device_class: 'moisture', state_class: 'measurement'},
|
||||
@ -892,7 +951,6 @@ export default class HomeAssistant extends Extension {
|
||||
Object.assign(extraAttrs, {device_class: 'energy', state_class: 'total_increasing'});
|
||||
}
|
||||
|
||||
|
||||
const allowsSet = firstExpose.access & ACCESS_SET;
|
||||
|
||||
let key = firstExpose.name;
|
||||
@ -918,8 +976,7 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
// When a device_class is set, unit_of_measurement must be set, otherwise warnings are generated.
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/15958#issuecomment-1377483202
|
||||
if (discoveryEntry.discovery_payload.device_class &&
|
||||
!discoveryEntry.discovery_payload.unit_of_measurement) {
|
||||
if (discoveryEntry.discovery_payload.device_class && !discoveryEntry.discovery_payload.unit_of_measurement) {
|
||||
delete discoveryEntry.discovery_payload.device_class;
|
||||
}
|
||||
|
||||
@ -1008,8 +1065,7 @@ export default class HomeAssistant extends Extension {
|
||||
week: {entity_category: 'config', icon: 'mdi:calendar-clock'},
|
||||
};
|
||||
|
||||
const valueTemplate = firstExpose.access & ACCESS_STATE ?
|
||||
`{{ value_json.${firstExpose.property} }}` : undefined;
|
||||
const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json.${firstExpose.property} }}` : undefined;
|
||||
|
||||
if (firstExpose.access & ACCESS_STATE) {
|
||||
discoveryEntries.push({
|
||||
@ -1078,8 +1134,7 @@ export default class HomeAssistant extends Extension {
|
||||
color_options: {icon: 'mdi:palette'},
|
||||
level_config: {entity_category: 'diagnostic'},
|
||||
programming_mode: {icon: 'mdi:calendar-clock'},
|
||||
program: {value_template: `{{ value_json.${firstExpose.property}|default('',True) ` +
|
||||
`| truncate(254, True, '', 0) }}`},
|
||||
program: {value_template: `{{ value_json.${firstExpose.property}|default('',True) ` + `| truncate(254, True, '', 0) }}`},
|
||||
schedule_settings: {icon: 'mdi:calendar-clock'},
|
||||
};
|
||||
if (firstExpose.access & ACCESS_STATE) {
|
||||
@ -1119,9 +1174,9 @@ export default class HomeAssistant extends Extension {
|
||||
// Exposes with category 'config' or 'diagnostic' are always added to the respective category.
|
||||
// This takes precedence over definitions in this file.
|
||||
if (firstExpose.category === 'config') {
|
||||
discoveryEntries.forEach((d) => d.discovery_payload.entity_category = 'config');
|
||||
discoveryEntries.forEach((d) => (d.discovery_payload.entity_category = 'config'));
|
||||
} else if (firstExpose.category === 'diagnostic') {
|
||||
discoveryEntries.forEach((d) => d.discovery_payload.entity_category = 'diagnostic');
|
||||
discoveryEntries.forEach((d) => (d.discovery_payload.entity_category = 'diagnostic'));
|
||||
}
|
||||
|
||||
discoveryEntries.forEach((d) => {
|
||||
@ -1273,12 +1328,15 @@ export default class HomeAssistant extends Extension {
|
||||
// @ts-ignore
|
||||
configs.push(entity.definition.homeassistant);
|
||||
}
|
||||
} else if (isGroup) { // group
|
||||
} else if (isGroup) {
|
||||
// group
|
||||
const exposesByType: {[s: string]: zhc.Expose[]} = {};
|
||||
const allExposes: zhc.Expose[] = [];
|
||||
|
||||
entity.zh.members.map((e) => this.zigbee.resolveEntity(e.getDevice()) as Device)
|
||||
.filter((d) => d.definition).forEach((device) => {
|
||||
entity.zh.members
|
||||
.map((e) => this.zigbee.resolveEntity(e.getDevice()) as Device)
|
||||
.filter((d) => d.definition)
|
||||
.forEach((device) => {
|
||||
const exposes = device.exposes();
|
||||
allExposes.push(...exposes);
|
||||
for (const expose of exposes.filter((e) => groupSupportedTypes.includes(e.type))) {
|
||||
@ -1295,8 +1353,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
});
|
||||
|
||||
configs = [].concat(...Object.values(exposesByType)
|
||||
.map((exposes) => this.exposeToConfig(exposes, 'group', allExposes)));
|
||||
configs = [].concat(...Object.values(exposesByType).map((exposes) => this.exposeToConfig(exposes, 'group', allExposes)));
|
||||
} else {
|
||||
// Discover bridge config.
|
||||
configs.push(...entity.configs);
|
||||
@ -1370,8 +1427,7 @@ export default class HomeAssistant extends Extension {
|
||||
value_template: `{{ value_json['update']['installed_version'] }}`,
|
||||
latest_version_template: `{{ value_json['update']['latest_version'] }}`,
|
||||
json_attributes_topic: `${settings.get().mqtt.base_topic}/${entity.name}`, // state topic
|
||||
json_attributes_template:
|
||||
`{"in_progress": {{ iif(value_json['update']['state'] == 'updating', 'true', 'false') }} }`,
|
||||
json_attributes_template: `{"in_progress": {{ iif(value_json['update']['state'] == 'updating', 'true', 'false') }} }`,
|
||||
},
|
||||
};
|
||||
configs.push(updateSensor);
|
||||
@ -1431,8 +1487,10 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
if (isGroup && entity.zh.members.length === 0) {
|
||||
return;
|
||||
} else if (isDevice && (!entity.definition || entity.zh.interviewing ||
|
||||
(entity.options.hasOwnProperty('homeassistant') && !entity.options.homeassistant))) {
|
||||
} else if (
|
||||
isDevice &&
|
||||
(!entity.definition || entity.zh.interviewing || (entity.options.hasOwnProperty('homeassistant') && !entity.options.homeassistant))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1467,7 +1525,7 @@ export default class HomeAssistant extends Extension {
|
||||
payload.tilt_status_topic = stateTopic;
|
||||
}
|
||||
|
||||
if (this.entityAttributes && (isDevice || isGroup) ) {
|
||||
if (this.entityAttributes && (isDevice || isGroup)) {
|
||||
payload.json_attributes_topic = stateTopic;
|
||||
}
|
||||
|
||||
@ -1494,23 +1552,24 @@ export default class HomeAssistant extends Extension {
|
||||
payload.origin = this.discoveryOrigin;
|
||||
|
||||
// Availability payload (can be disabled by setting `payload.availability = false`).
|
||||
if (! payload.hasOwnProperty('availability') || payload.availability) {
|
||||
if (!payload.hasOwnProperty('availability') || payload.availability) {
|
||||
payload.availability = [{topic: `${settings.get().mqtt.base_topic}/bridge/state`}];
|
||||
|
||||
if (isDevice||isGroup) {
|
||||
if (isDevice || isGroup) {
|
||||
if (utils.isAvailabilityEnabledForEntity(entity, settings.get())) {
|
||||
payload.availability_mode = 'all';
|
||||
payload.availability.push({topic: `${baseTopic}/availability`});
|
||||
}
|
||||
} else { // Bridge availability is different.
|
||||
} else {
|
||||
// Bridge availability is different.
|
||||
payload.availability_mode = 'all';
|
||||
}
|
||||
|
||||
if (isDevice && entity.options.disabled) {
|
||||
// Mark disabled device always as unavailable
|
||||
payload.availability.forEach((a: KeyValue) => a.value_template = '{{ "offline" }}');
|
||||
payload.availability.forEach((a: KeyValue) => (a.value_template = '{{ "offline" }}'));
|
||||
} else if (!settings.get().advanced.legacy_availability_payload) {
|
||||
payload.availability.forEach((a: KeyValue) => a.value_template = '{{ value_json.state }}');
|
||||
payload.availability.forEach((a: KeyValue) => (a.value_template = '{{ value_json.state }}'));
|
||||
}
|
||||
} else {
|
||||
delete payload.availability;
|
||||
@ -1559,18 +1618,15 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.temperature_command_topic) {
|
||||
payload.temperature_command_topic =
|
||||
`${baseTopic}/${commandTopicPrefix}set/${payload.temperature_command_topic}`;
|
||||
payload.temperature_command_topic = `${baseTopic}/${commandTopicPrefix}set/${payload.temperature_command_topic}`;
|
||||
}
|
||||
|
||||
if (payload.temperature_low_command_topic) {
|
||||
payload.temperature_low_command_topic =
|
||||
`${baseTopic}/${commandTopicPrefix}set/${payload.temperature_low_command_topic}`;
|
||||
payload.temperature_low_command_topic = `${baseTopic}/${commandTopicPrefix}set/${payload.temperature_low_command_topic}`;
|
||||
}
|
||||
|
||||
if (payload.temperature_high_command_topic) {
|
||||
payload.temperature_high_command_topic =
|
||||
`${baseTopic}/${commandTopicPrefix}set/${payload.temperature_high_command_topic}`;
|
||||
payload.temperature_high_command_topic = `${baseTopic}/${commandTopicPrefix}set/${payload.temperature_high_command_topic}`;
|
||||
}
|
||||
|
||||
if (payload.fan_mode_state_topic) {
|
||||
@ -1606,8 +1662,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.preset_mode_command_topic) {
|
||||
payload.preset_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/` +
|
||||
payload.preset_mode_command_topic;
|
||||
payload.preset_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/` + payload.preset_mode_command_topic;
|
||||
}
|
||||
|
||||
if (payload.action_topic) {
|
||||
@ -1622,8 +1677,7 @@ export default class HomeAssistant extends Extension {
|
||||
return;
|
||||
} else if (ignoreName && key === 'name') {
|
||||
return;
|
||||
} else if (['number', 'string', 'boolean'].includes(typeof obj[key]) ||
|
||||
Array.isArray(obj[key])) {
|
||||
} else if (['number', 'string', 'boolean'].includes(typeof obj[key]) || Array.isArray(obj[key])) {
|
||||
payload[key] = obj[key];
|
||||
} else if (obj[key] === null) {
|
||||
delete payload[key];
|
||||
@ -1680,8 +1734,7 @@ export default class HomeAssistant extends Extension {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDeviceAutomation &&
|
||||
(!message.availability || !message.availability[0].topic.startsWith(baseTopic))) {
|
||||
if (!isDeviceAutomation && (!message.availability || !message.availability[0].topic.startsWith(baseTopic))) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
@ -1691,7 +1744,7 @@ export default class HomeAssistant extends Extension {
|
||||
// Group discovery topic uses "ENCODEDBASETOPIC_GROUPID", device use ieeeAddr
|
||||
const ID = discoveryMatch[2].includes('_') ? discoveryMatch[2].split('_')[1] : discoveryMatch[2];
|
||||
const entity = ID === this.bridge.ID ? this.bridge : this.zigbee.resolveEntity(ID);
|
||||
let clear = !entity || entity.isDevice() && !entity.definition;
|
||||
let clear = !entity || (entity.isDevice() && !entity.definition);
|
||||
|
||||
// Only save when topic matches otherwise config is not updated when renamed by editing configuration.yaml
|
||||
if (entity) {
|
||||
@ -1716,8 +1769,7 @@ export default class HomeAssistant extends Extension {
|
||||
} else {
|
||||
this.getDiscovered(entity).messages[topic] = {payload: stringify(message), published: true};
|
||||
}
|
||||
} else if ((data.topic === this.statusTopic || data.topic === defaultStatusTopic) &&
|
||||
data.message.toLowerCase() === 'online') {
|
||||
} else if ((data.topic === this.statusTopic || data.topic === defaultStatusTopic) && data.message.toLowerCase() === 'online') {
|
||||
const timer = setTimeout(async () => {
|
||||
// Publish all device states.
|
||||
for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) {
|
||||
@ -1762,8 +1814,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
private getDevicePayload(entity: Device | Group | Bridge): KeyValue {
|
||||
const identifierPostfix = entity.isGroup() ?
|
||||
`zigbee2mqtt_${this.getEncodedBaseTopic()}` : 'zigbee2mqtt';
|
||||
const identifierPostfix = entity.isGroup() ? `zigbee2mqtt_${this.getEncodedBaseTopic()}` : 'zigbee2mqtt';
|
||||
|
||||
// Allow device name to be overridden by homeassistant config
|
||||
let deviceName = entity.name;
|
||||
@ -1830,7 +1881,11 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
private getEncodedBaseTopic(): string {
|
||||
return settings.get().mqtt.base_topic.split('').map((s) => s.charCodeAt(0).toString()).join('');
|
||||
return settings
|
||||
.get()
|
||||
.mqtt.base_topic.split('')
|
||||
.map((s) => s.charCodeAt(0).toString())
|
||||
.join('');
|
||||
}
|
||||
|
||||
private getDiscoveryTopic(config: DiscoveryEntry, entity: Device | Group | Bridge): string {
|
||||
@ -1838,11 +1893,12 @@ export default class HomeAssistant extends Extension {
|
||||
return `${config.type}/${key}/${config.object_id}/config`;
|
||||
}
|
||||
|
||||
private async publishDeviceTriggerDiscover(device: Device, key: string, value: string, force=false): Promise<void> {
|
||||
private async publishDeviceTriggerDiscover(device: Device, key: string, value: string, force = false): Promise<void> {
|
||||
const haConfig = device.options.homeassistant;
|
||||
if (device.options.hasOwnProperty('homeassistant') && (haConfig == null ||
|
||||
(haConfig.hasOwnProperty('device_automation') && typeof haConfig === 'object' &&
|
||||
haConfig.device_automation == null))) {
|
||||
if (
|
||||
device.options.hasOwnProperty('homeassistant') &&
|
||||
(haConfig == null || (haConfig.hasOwnProperty('device_automation') && typeof haConfig === 'object' && haConfig.device_automation == null))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1944,8 +2000,7 @@ export default class HomeAssistant extends Extension {
|
||||
state_topic_postfix: 'info',
|
||||
value_template: '{{ value_json.log_level | lower }}',
|
||||
command_topic: `${baseTopic}/request/options`,
|
||||
command_template:
|
||||
'{"options": {"advanced": {"log_level": "{{ value }}" } } }',
|
||||
command_template: '{"options": {"advanced": {"log_level": "{{ value }}" } } }',
|
||||
options: settings.LOG_LEVELS,
|
||||
},
|
||||
},
|
||||
@ -1962,7 +2017,6 @@ export default class HomeAssistant extends Extension {
|
||||
state_topic_postfix: 'info',
|
||||
value_template: '{{ value_json.version }}',
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
type: 'sensor',
|
||||
@ -1977,7 +2031,6 @@ export default class HomeAssistant extends Extension {
|
||||
state_topic_postfix: 'info',
|
||||
value_template: '{{ value_json.coordinator.meta.revision }}',
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
type: 'sensor',
|
||||
@ -1989,7 +2042,7 @@ export default class HomeAssistant extends Extension {
|
||||
enabled_by_default: false,
|
||||
state_topic: true,
|
||||
state_topic_postfix: 'response/networkmap',
|
||||
value_template: '{{ now().strftime(\'%Y-%m-%d %H:%M:%S\') }}',
|
||||
value_template: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}",
|
||||
json_attributes_topic: `${baseTopic}/response/networkmap`,
|
||||
json_attributes_template: '{{ value_json.data.value | tojson }}',
|
||||
},
|
||||
@ -2005,8 +2058,7 @@ export default class HomeAssistant extends Extension {
|
||||
entity_category: 'diagnostic',
|
||||
state_topic: true,
|
||||
state_topic_postfix: 'info',
|
||||
value_template:
|
||||
'{{ iif(value_json.permit_join_timeout is defined, value_json.permit_join_timeout, None) }}',
|
||||
value_template: '{{ iif(value_json.permit_join_timeout is defined, value_json.permit_join_timeout, None) }}',
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
import * as settings from '../../util/settings';
|
||||
import logger from '../../util/logger';
|
||||
import utils from '../../util/utils';
|
||||
import assert from 'assert';
|
||||
import Extension from '../extension';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import bind from 'bind-decorator';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
|
||||
const configRegex =
|
||||
new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+/factory_reset)|(?:\\w+))`);
|
||||
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 = null;
|
||||
@ -15,24 +15,24 @@ export default class BridgeLegacy extends Extension {
|
||||
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
};
|
||||
|
||||
@ -51,10 +51,7 @@ export default class BridgeLegacy extends Extension {
|
||||
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}}),
|
||||
);
|
||||
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}'`);
|
||||
}
|
||||
@ -162,9 +159,7 @@ export default class BridgeLegacy extends Extension {
|
||||
});
|
||||
|
||||
if (topic.split('/').pop() == 'get') {
|
||||
await this.mqtt.publish(
|
||||
`bridge/config/devices`, stringify(devices), {}, settings.get().mqtt.base_topic, false, false,
|
||||
);
|
||||
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}));
|
||||
}
|
||||
@ -179,8 +174,7 @@ export default class BridgeLegacy extends Extension {
|
||||
}
|
||||
|
||||
@bind async rename(topic: string, message: string): Promise<void> {
|
||||
const invalid =
|
||||
`Invalid rename message format expected {"old": "friendly_name", "new": "new_name"} got ${message}`;
|
||||
const invalid = `Invalid rename message format expected {"old": "friendly_name", "new": "new_name"} got ${message}`;
|
||||
|
||||
let json = null;
|
||||
try {
|
||||
@ -218,10 +212,7 @@ export default class BridgeLegacy extends Extension {
|
||||
this.eventBus.emitEntityRenamed({homeAssisantRename: false, from, to, entity});
|
||||
}
|
||||
|
||||
await this.mqtt.publish(
|
||||
'bridge/log',
|
||||
stringify({type: `${isGroup ? 'group' : 'device'}_renamed`, message: {from, to}}),
|
||||
);
|
||||
await this.mqtt.publish('bridge/log', stringify({type: `${isGroup ? 'group' : 'device'}_renamed`, message: {from, to}}));
|
||||
} catch (error) {
|
||||
logger.error(`Failed to rename - ${from} to ${to}`);
|
||||
}
|
||||
@ -377,40 +368,25 @@ export default class BridgeLegacy extends Extension {
|
||||
}
|
||||
|
||||
if (type === 'deviceJoined') {
|
||||
await this.mqtt.publish(
|
||||
'bridge/log',
|
||||
stringify({type: `device_connected`, message: {friendly_name: 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;
|
||||
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}),
|
||||
);
|
||||
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}),
|
||||
);
|
||||
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}),
|
||||
);
|
||||
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}),
|
||||
);
|
||||
await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_started', meta}));
|
||||
}
|
||||
}
|
||||
} else if (type === 'deviceAnnounce') {
|
||||
@ -421,34 +397,22 @@ export default class BridgeLegacy extends Extension {
|
||||
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}),
|
||||
);
|
||||
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'}}),
|
||||
);
|
||||
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'}}),
|
||||
);
|
||||
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'}}),
|
||||
);
|
||||
await this.mqtt.publish('bridge/log', stringify({type: `touchlink`, message: 'reset_failed', meta: {status: 'failed'}}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
/* istanbul ignore file */
|
||||
import * as settings from '../../util/settings';
|
||||
import logger from '../../util/logger';
|
||||
import Extension from '../extension';
|
||||
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$`);
|
||||
|
||||
@ -31,16 +32,14 @@ export default class DeviceGroupMembership extends Extension {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await endpoint.command(
|
||||
`genGroups`, 'getMembership', {groupcount: 0, grouplist: []}, {},
|
||||
);
|
||||
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, capacity} = response;
|
||||
let {grouplist} = response;
|
||||
|
||||
grouplist = grouplist.map((gid: string) => {
|
||||
const g = settings.getGroup(gid);
|
||||
@ -49,13 +48,13 @@ export default class DeviceGroupMembership extends Extension {
|
||||
|
||||
const msgGroupList = `${device.ieeeAddr} is in groups [${grouplist}]`;
|
||||
let msgCapacity;
|
||||
if (capacity === 254) {
|
||||
if (response.capacity === 254) {
|
||||
msgCapacity = 'it can be a part of at least 1 more group';
|
||||
} else {
|
||||
msgCapacity = `its remaining group capacity is ${capacity === 255 ? 'unknown' : capacity}`;
|
||||
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: capacity});
|
||||
await this.publishEntityState(device, {group_list: grouplist, group_capacity: response.capacity});
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import logger from '../../util/logger';
|
||||
import * as settings from '../../util/settings';
|
||||
import Extension from '../extension';
|
||||
|
||||
const defaultConfiguration = {
|
||||
minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 1,
|
||||
minimumReportInterval: 3,
|
||||
maximumReportInterval: 300,
|
||||
reportableChange: 1,
|
||||
};
|
||||
|
||||
const ZNLDP12LM = zhc.definitions.find((d) => d.model === 'ZNLDP12LM');
|
||||
@ -19,43 +22,47 @@ const devicesNotSupportingReporting = [
|
||||
|
||||
const reportKey = 1;
|
||||
|
||||
const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemperature: boolean, colorXY: boolean}> => {
|
||||
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,
|
||||
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': [
|
||||
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,
|
||||
attribute: 'colorTemperature',
|
||||
...defaultConfiguration,
|
||||
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorTemperature,
|
||||
},
|
||||
{
|
||||
attribute: 'currentX', ...defaultConfiguration,
|
||||
attribute: 'currentX',
|
||||
...defaultConfiguration,
|
||||
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
|
||||
},
|
||||
{
|
||||
attribute: 'currentY', ...defaultConfiguration,
|
||||
attribute: 'currentY',
|
||||
...defaultConfiguration,
|
||||
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
|
||||
},
|
||||
],
|
||||
'closuresWindowCovering': [
|
||||
closuresWindowCovering: [
|
||||
{attribute: 'currentPositionLiftPercentage', ...defaultConfiguration},
|
||||
{attribute: 'currentPositionTiltPercentage', ...defaultConfiguration},
|
||||
],
|
||||
@ -86,28 +93,25 @@ export default class Report extends Extension {
|
||||
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)) {
|
||||
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.hasOwnProperty('condition') || (await entry.condition(ep))) {
|
||||
const toAdd = {...entry};
|
||||
if (!this.enabled) toAdd.maximumReportInterval = 0xFFFF;
|
||||
if (!this.enabled) toAdd.maximumReportInterval = 0xffff;
|
||||
items.push(toAdd);
|
||||
delete items[items.length - 1].condition;
|
||||
}
|
||||
}
|
||||
|
||||
this.enabled ?
|
||||
await ep.bind(cluster, this.zigbee.firstCoordinatorEndpoint()) :
|
||||
await ep.unbind(cluster, this.zigbee.firstCoordinatorEndpoint());
|
||||
this.enabled
|
||||
? await ep.bind(cluster, this.zigbee.firstCoordinatorEndpoint())
|
||||
: await ep.unbind(cluster, this.zigbee.firstCoordinatorEndpoint());
|
||||
|
||||
await ep.configureReporting(cluster, items);
|
||||
logger.info(
|
||||
`Successfully ${term2} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`,
|
||||
);
|
||||
logger.info(`Successfully ${term2} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -121,9 +125,7 @@ export default class Report extends Extension {
|
||||
|
||||
this.eventBus.emitDevicesChanged();
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to ${term1.toLowerCase()} reporting for '${device.ieeeAddr}' - ${error.stack}`,
|
||||
);
|
||||
logger.error(`Failed to ${term1.toLowerCase()} reporting for '${device.ieeeAddr}' - ${error.stack}`);
|
||||
|
||||
this.failed.add(device.ieeeAddr);
|
||||
}
|
||||
@ -143,26 +145,26 @@ export default class Report extends Extension {
|
||||
// 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) {
|
||||
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 reproting.
|
||||
// https://github.com/Koenkk/zigbee-herdsman/issues/110
|
||||
const philipsIgnoreSw = ['5.127.1.26581', '5.130.1.30000'];
|
||||
if (device.zh.manufacturerName === 'Philips' &&
|
||||
philipsIgnoreSw.includes(device.zh.softwareBuildID)) return false;
|
||||
if (device.zh.manufacturerName === 'Philips' && philipsIgnoreSw.includes(device.zh.softwareBuildID)) 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 (devicesNotSupportingReporting.includes(device.definition) || device.definition.vendor === 'Gledopto') return false;
|
||||
|
||||
if (this.enabled && device.zh.meta.hasOwnProperty('reporting') &&
|
||||
device.zh.meta.reporting === reportKey) {
|
||||
if (this.enabled && device.zh.meta.hasOwnProperty('reporting') && device.zh.meta.reporting === reportKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* istanbul ignore file */
|
||||
import logger from '../../util/logger';
|
||||
// DEPRECATED
|
||||
import * as settings from '../../util/settings';
|
||||
import logger from '../../util/logger';
|
||||
import utils from '../../util/utils';
|
||||
import Extension from '../extension';
|
||||
|
||||
|
@ -1,22 +1,37 @@
|
||||
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 logger from '../util/logger';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import Extension from './extension';
|
||||
import bind from 'bind-decorator';
|
||||
|
||||
interface Link {
|
||||
source: {ieeeAddr: string, networkAddress: number}, target: {ieeeAddr: string, networkAddress: number},
|
||||
linkquality: number, depth: number, routes: zh.RoutingTableEntry[],
|
||||
sourceIeeeAddr: string, targetIeeeAddr: string, sourceNwkAddr: number, lqi: number, relationship: number,
|
||||
source: {ieeeAddr: string; networkAddress: number};
|
||||
target: {ieeeAddr: string; networkAddress: number};
|
||||
linkquality: number;
|
||||
depth: number;
|
||||
routes: zh.RoutingTableEntry[];
|
||||
sourceIeeeAddr: string;
|
||||
targetIeeeAddr: string;
|
||||
sourceNwkAddr: number;
|
||||
lqi: number;
|
||||
relationship: number;
|
||||
}
|
||||
|
||||
interface Topology {
|
||||
nodes: {
|
||||
ieeeAddr: string, friendlyName: string, type: string, networkAddress: number, manufacturerName: string,
|
||||
modelID: string, failed: string[], lastSeen: number,
|
||||
definition: {model: string, vendor: string, supports: string, description: string}}[],
|
||||
links: Link[],
|
||||
ieeeAddr: string;
|
||||
friendlyName: string;
|
||||
type: string;
|
||||
networkAddress: number;
|
||||
manufacturerName: string;
|
||||
modelID: string;
|
||||
failed: string[];
|
||||
lastSeen: number;
|
||||
definition: {model: string; vendor: string; supports: string; description: string};
|
||||
}[];
|
||||
links: Link[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,17 +47,16 @@ export default class NetworkMap extends Extension {
|
||||
override async start(): Promise<void> {
|
||||
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
||||
this.supportedFormats = {
|
||||
'raw': this.raw,
|
||||
'graphviz': this.graphviz,
|
||||
'plantuml': this.plantuml,
|
||||
raw: this.raw,
|
||||
graphviz: this.graphviz,
|
||||
plantuml: this.plantuml,
|
||||
};
|
||||
}
|
||||
|
||||
@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.hasOwnProperty(data.message)) {
|
||||
if ((data.topic === this.legacyTopic || data.topic === this.legacyTopicRoutes) && this.supportedFormats.hasOwnProperty(data.message)) {
|
||||
const includeRoutes = data.topic === this.legacyTopicRoutes;
|
||||
const topology = await this.networkScan(includeRoutes);
|
||||
let converted = this.supportedFormats[data.message](topology);
|
||||
@ -62,15 +76,9 @@ export default class NetworkMap extends Extension {
|
||||
const routes = typeof message === 'object' && message.routes;
|
||||
const topology = await this.networkScan(routes);
|
||||
const value = this.supportedFormats[type](topology);
|
||||
await this.mqtt.publish(
|
||||
'bridge/response/networkmap',
|
||||
stringify(utils.getResponse(message, {routes, type, value}, null)),
|
||||
);
|
||||
await this.mqtt.publish('bridge/response/networkmap', stringify(utils.getResponse(message, {routes, type, value}, null)));
|
||||
} catch (error) {
|
||||
await this.mqtt.publish(
|
||||
'bridge/response/networkmap',
|
||||
stringify(utils.getResponse(message, {}, error.message)),
|
||||
);
|
||||
await this.mqtt.publish('bridge/response/networkmap', stringify(utils.getResponse(message, {}, error.message)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -94,7 +102,7 @@ export default class NetworkMap extends Extension {
|
||||
// Add the device short network address, ieeaddr and scan note (if any)
|
||||
labels.push(
|
||||
`${node.ieeeAddr} (${utils.toNetworkAddressHex(node.networkAddress)})` +
|
||||
((node.failed && node.failed.length) ? `failed: ${node.failed.join(',')}` : ''),
|
||||
(node.failed && node.failed.length ? `failed: ${node.failed.join(',')}` : ''),
|
||||
);
|
||||
|
||||
// Add the device model
|
||||
@ -113,35 +121,31 @@ export default class NetworkMap extends Extension {
|
||||
|
||||
// Shape the record according to device type
|
||||
if (node.type == 'Coordinator') {
|
||||
style = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` +
|
||||
`fontcolor="${colors.font.coordinator}"`;
|
||||
style = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` + `fontcolor="${colors.font.coordinator}"`;
|
||||
} else if (node.type == 'Router') {
|
||||
style = `style="rounded, filled", fillcolor="${colors.fill.router}", ` +
|
||||
`fontcolor="${colors.font.router}"`;
|
||||
style = `style="rounded, filled", fillcolor="${colors.fill.router}", ` + `fontcolor="${colors.font.router}"`;
|
||||
} else {
|
||||
style = `style="rounded, dashed, filled", fillcolor="${colors.fill.enddevice}", ` +
|
||||
`fontcolor="${colors.font.enddevice}"`;
|
||||
style = `style="rounded, dashed, filled", fillcolor="${colors.fill.enddevice}", ` + `fontcolor="${colors.font.enddevice}"`;
|
||||
}
|
||||
|
||||
// Add the device with its labels to the graph as a node.
|
||||
text += ` "${node.ieeeAddr}" [`+style+`, label="{${labels.join('|')}}"];\n`;
|
||||
text += ` "${node.ieeeAddr}" [${style}, label="{${labels.join('|')}}"];\n`;
|
||||
|
||||
/**
|
||||
* Add an edge between the device and its child to the graph
|
||||
* NOTE: There are situations where a device is NOT in the topology, this can be e.g.
|
||||
* due to not responded to the lqi scan. In that case we do not add an edge for this device.
|
||||
*/
|
||||
topology.links.filter((e) => (e.source.ieeeAddr === node.ieeeAddr)).forEach((e) => {
|
||||
const lineStyle = (node.type=='EndDevice') ? 'penwidth=1, ' :
|
||||
(!e.routes.length) ? 'penwidth=0.5, ' : 'penwidth=2, ';
|
||||
const lineWeight = (!e.routes.length) ? `weight=0, color="${colors.line.inactive}", ` :
|
||||
`weight=1, color="${colors.line.active}", `;
|
||||
const textRoutes = e.routes.map((r) => utils.toNetworkAddressHex(r.destinationAddress));
|
||||
const lineLabels = (!e.routes.length) ? `label="${e.linkquality}"` :
|
||||
`label="${e.linkquality} (routes: ${textRoutes.join(',')})"`;
|
||||
text += ` "${node.ieeeAddr}" -> "${e.target.ieeeAddr}"`;
|
||||
text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`;
|
||||
});
|
||||
topology.links
|
||||
.filter((e) => e.source.ieeeAddr === node.ieeeAddr)
|
||||
.forEach((e) => {
|
||||
const lineStyle = node.type == 'EndDevice' ? 'penwidth=1, ' : !e.routes.length ? 'penwidth=0.5, ' : 'penwidth=2, ';
|
||||
const lineWeight = !e.routes.length ? `weight=0, color="${colors.line.inactive}", ` : `weight=1, color="${colors.line.active}", `;
|
||||
const textRoutes = e.routes.map((r) => utils.toNetworkAddressHex(r.destinationAddress));
|
||||
const lineLabels = !e.routes.length ? `label="${e.linkquality}"` : `label="${e.linkquality} (routes: ${textRoutes.join(',')})"`;
|
||||
text += ` "${node.ieeeAddr}" -> "${e.target.ieeeAddr}"`;
|
||||
text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`;
|
||||
});
|
||||
});
|
||||
|
||||
text += '}';
|
||||
@ -156,36 +160,38 @@ export default class NetworkMap extends Extension {
|
||||
text.push(``);
|
||||
text.push('@startuml');
|
||||
|
||||
topology.nodes.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName)).forEach((node) => {
|
||||
// Add friendly name
|
||||
text.push(`card ${node.ieeeAddr} [`);
|
||||
text.push(`${node.friendlyName}`);
|
||||
text.push(`---`);
|
||||
|
||||
// Add the device short network address, ieeaddr and scan note (if any)
|
||||
text.push(
|
||||
`${node.ieeeAddr} (${utils.toNetworkAddressHex(node.networkAddress)})` +
|
||||
((node.failed && node.failed.length) ? ` failed: ${node.failed.join(',')}` : ''),
|
||||
);
|
||||
|
||||
// Add the device model
|
||||
if (node.type !== 'Coordinator') {
|
||||
topology.nodes
|
||||
.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName))
|
||||
.forEach((node) => {
|
||||
// Add friendly name
|
||||
text.push(`card ${node.ieeeAddr} [`);
|
||||
text.push(`${node.friendlyName}`);
|
||||
text.push(`---`);
|
||||
const definition = (this.zigbee.resolveEntity(node.ieeeAddr) as Device).definition;
|
||||
text.push(`${definition?.vendor} ${definition?.description} (${definition?.model})`);
|
||||
}
|
||||
|
||||
// Add the device last_seen timestamp
|
||||
let lastSeen = 'unknown';
|
||||
const date = node.type === 'Coordinator' ? Date.now() : node.lastSeen;
|
||||
if (date) {
|
||||
lastSeen = utils.formatDate(date, 'relative') as string;
|
||||
}
|
||||
text.push(`---`);
|
||||
text.push(lastSeen);
|
||||
text.push(`]`);
|
||||
text.push(``);
|
||||
});
|
||||
// Add the device short network address, ieeaddr and scan note (if any)
|
||||
text.push(
|
||||
`${node.ieeeAddr} (${utils.toNetworkAddressHex(node.networkAddress)})` +
|
||||
(node.failed && node.failed.length ? ` failed: ${node.failed.join(',')}` : ''),
|
||||
);
|
||||
|
||||
// Add the device model
|
||||
if (node.type !== 'Coordinator') {
|
||||
text.push(`---`);
|
||||
const definition = (this.zigbee.resolveEntity(node.ieeeAddr) as Device).definition;
|
||||
text.push(`${definition?.vendor} ${definition?.description} (${definition?.model})`);
|
||||
}
|
||||
|
||||
// Add the device last_seen timestamp
|
||||
let lastSeen = 'unknown';
|
||||
const date = node.type === 'Coordinator' ? Date.now() : node.lastSeen;
|
||||
if (date) {
|
||||
lastSeen = utils.formatDate(date, 'relative') as string;
|
||||
}
|
||||
text.push(`---`);
|
||||
text.push(lastSeen);
|
||||
text.push(`]`);
|
||||
text.push(``);
|
||||
});
|
||||
|
||||
/**
|
||||
* Add edges between the devices
|
||||
@ -254,19 +260,30 @@ export default class NetworkMap extends Extension {
|
||||
const topology: Topology = {nodes: [], links: []};
|
||||
// Add nodes
|
||||
for (const device of devices) {
|
||||
const definition = device.definition ? {
|
||||
model: device.definition.model,
|
||||
vendor: device.definition.vendor,
|
||||
description: device.definition.description,
|
||||
supports: Array.from(new Set((device.exposes()).map((e) => {
|
||||
return e.name ?? `${e.type} (${e.features.map((f) => f.name).join(', ')})`;
|
||||
}))).join(', '),
|
||||
} : null;
|
||||
const definition = device.definition
|
||||
? {
|
||||
model: device.definition.model,
|
||||
vendor: device.definition.vendor,
|
||||
description: device.definition.description,
|
||||
supports: Array.from(
|
||||
new Set(
|
||||
device.exposes().map((e) => {
|
||||
return e.name ?? `${e.type} (${e.features.map((f) => f.name).join(', ')})`;
|
||||
}),
|
||||
),
|
||||
).join(', '),
|
||||
}
|
||||
: null;
|
||||
|
||||
topology.nodes.push({
|
||||
ieeeAddr: device.ieeeAddr, friendlyName: device.name, type: device.zh.type,
|
||||
networkAddress: device.zh.networkAddress, manufacturerName: device.zh.manufacturerName,
|
||||
modelID: device.zh.modelID, failed: failed.get(device), lastSeen: device.zh.lastSeen,
|
||||
ieeeAddr: device.ieeeAddr,
|
||||
friendlyName: device.name,
|
||||
type: device.zh.type,
|
||||
networkAddress: device.zh.networkAddress,
|
||||
manufacturerName: device.zh.manufacturerName,
|
||||
modelID: device.zh.modelID,
|
||||
failed: failed.get(device),
|
||||
lastSeen: device.zh.lastSeen,
|
||||
definition,
|
||||
});
|
||||
}
|
||||
@ -289,17 +306,20 @@ export default class NetworkMap extends Extension {
|
||||
const link: Link = {
|
||||
source: {ieeeAddr: neighbor.ieeeAddr, networkAddress: neighbor.networkAddress},
|
||||
target: {ieeeAddr: device.ieeeAddr, networkAddress: device.zh.networkAddress},
|
||||
linkquality: neighbor.linkquality, depth: neighbor.depth, routes: [],
|
||||
linkquality: neighbor.linkquality,
|
||||
depth: neighbor.depth,
|
||||
routes: [],
|
||||
// DEPRECATED:
|
||||
sourceIeeeAddr: neighbor.ieeeAddr, targetIeeeAddr: device.ieeeAddr,
|
||||
sourceNwkAddr: neighbor.networkAddress, lqi: neighbor.linkquality,
|
||||
sourceIeeeAddr: neighbor.ieeeAddr,
|
||||
targetIeeeAddr: device.ieeeAddr,
|
||||
sourceNwkAddr: neighbor.networkAddress,
|
||||
lqi: neighbor.linkquality,
|
||||
relationship: neighbor.relationship,
|
||||
};
|
||||
|
||||
const routingTable = routingTables.get(device);
|
||||
if (routingTable) {
|
||||
link.routes = routingTable.table
|
||||
.filter((t) => t.status === 'ACTIVE' && t.nextHop === neighbor.networkAddress);
|
||||
link.routes = routingTable.table.filter((t) => t.status === 'ACTIVE' && t.nextHop === neighbor.networkAddress);
|
||||
}
|
||||
|
||||
topology.links.push(link);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import Extension from './extension';
|
||||
|
||||
/**
|
||||
@ -11,21 +12,17 @@ export default class OnEvent extends Extension {
|
||||
}
|
||||
|
||||
this.eventBus.onDeviceMessage(this, (data) => this.callOnEvent(data.device, 'message', this.convertData(data)));
|
||||
this.eventBus.onDeviceJoined(this,
|
||||
(data) => this.callOnEvent(data.device, 'deviceJoined', this.convertData(data)));
|
||||
this.eventBus.onDeviceInterview(this,
|
||||
(data) => this.callOnEvent(data.device, 'deviceInterview', this.convertData(data)));
|
||||
this.eventBus.onDeviceAnnounce(this,
|
||||
(data) => this.callOnEvent(data.device, 'deviceAnnounce', this.convertData(data)));
|
||||
this.eventBus.onDeviceNetworkAddressChanged(this,
|
||||
(data) => this.callOnEvent(data.device, 'deviceNetworkAddressChanged', this.convertData(data)));
|
||||
this.eventBus.onEntityOptionsChanged(this,
|
||||
async (data) => {
|
||||
if (data.entity.isDevice()) {
|
||||
await this.callOnEvent(data.entity, 'deviceOptionsChanged', data)
|
||||
.then(() => this.eventBus.emitDevicesChanged());
|
||||
}
|
||||
});
|
||||
this.eventBus.onDeviceJoined(this, (data) => this.callOnEvent(data.device, 'deviceJoined', this.convertData(data)));
|
||||
this.eventBus.onDeviceInterview(this, (data) => this.callOnEvent(data.device, 'deviceInterview', this.convertData(data)));
|
||||
this.eventBus.onDeviceAnnounce(this, (data) => this.callOnEvent(data.device, 'deviceAnnounce', this.convertData(data)));
|
||||
this.eventBus.onDeviceNetworkAddressChanged(this, (data) =>
|
||||
this.callOnEvent(data.device, 'deviceNetworkAddressChanged', this.convertData(data)),
|
||||
);
|
||||
this.eventBus.onEntityOptionsChanged(this, async (data) => {
|
||||
if (data.entity.isDevice()) {
|
||||
await this.callOnEvent(data.entity, 'deviceOptionsChanged', data).then(() => this.eventBus.emitDevicesChanged());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private convertData(data: KeyValue): KeyValue {
|
||||
|
@ -1,15 +1,16 @@
|
||||
import * as settings from '../util/settings';
|
||||
import logger from '../util/logger';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
import bind from 'bind-decorator';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import path from 'path';
|
||||
import * as URI from 'uri-js';
|
||||
import {Zcl} from 'zigbee-herdsman';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import Device from '../model/device';
|
||||
import dataDir from '../util/data';
|
||||
import * as URI from 'uri-js';
|
||||
import path from 'path';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import {Zcl} from 'zigbee-herdsman';
|
||||
import logger from '../util/logger';
|
||||
import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
|
||||
function isValidUrl(url: string): boolean {
|
||||
let parsed;
|
||||
@ -24,17 +25,19 @@ function isValidUrl(url: string): boolean {
|
||||
|
||||
type UpdateState = 'updating' | 'idle' | 'available';
|
||||
interface UpdatePayload {
|
||||
update_available?: boolean
|
||||
update_available?: boolean;
|
||||
// eslint-disable-next-line camelcase
|
||||
update: {
|
||||
progress?: number, remaining?: number, state: UpdateState,
|
||||
installed_version: number | null, latest_version: number | null
|
||||
}
|
||||
progress?: number;
|
||||
remaining?: number;
|
||||
state: UpdateState;
|
||||
installed_version: number | null;
|
||||
latest_version: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
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');
|
||||
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();
|
||||
@ -79,8 +82,7 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
|
||||
@bind private async onZigbeeEvent(data: eventdata.DeviceMessage): Promise<void> {
|
||||
if (data.type !== 'commandQueryNextImageRequest' || !data.device.definition ||
|
||||
this.inProgress.has(data.device.ieeeAddr)) return;
|
||||
if (data.type !== 'commandQueryNextImageRequest' || !data.device.definition || this.inProgress.has(data.device.ieeeAddr)) return;
|
||||
logger.debug(`Device '${data.device.name}' requested OTA`);
|
||||
|
||||
const automaticOTACheckDisabled = settings.get().ota.disable_automatic_update_check;
|
||||
@ -90,8 +92,9 @@ export default class OTAUpdate extends Extension {
|
||||
// with only 10 - 60 seconds inbetween. It doesn't make sense to check for a new update
|
||||
// each time, so this interval can be set by the user. The default is 1,440 minutes (one day).
|
||||
const updateCheckInterval = settings.get().ota.update_check_interval * 1000 * 60;
|
||||
const check = this.lastChecked.hasOwnProperty(data.device.ieeeAddr) ?
|
||||
(Date.now() - this.lastChecked[data.device.ieeeAddr]) > updateCheckInterval : true;
|
||||
const check = this.lastChecked.hasOwnProperty(data.device.ieeeAddr)
|
||||
? Date.now() - this.lastChecked[data.device.ieeeAddr] > updateCheckInterval
|
||||
: true;
|
||||
if (!check) return;
|
||||
|
||||
this.lastChecked[data.device.ieeeAddr] = Date.now();
|
||||
@ -113,23 +116,24 @@ export default class OTAUpdate extends Extension {
|
||||
/* 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}),
|
||||
);
|
||||
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message, meta}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Respond to stop the client from requesting OTAs
|
||||
const endpoint = data.device.zh.endpoints.find((e) => e.supportsOutputCluster('genOta')) || data.endpoint;
|
||||
await endpoint.commandResponse('genOta', 'queryNextImageResponse',
|
||||
{status: Zcl.Status.NO_IMAGE_AVAILABLE}, undefined, data.meta.zclTransactionSequenceNumber);
|
||||
await endpoint.commandResponse(
|
||||
'genOta',
|
||||
'queryNextImageResponse',
|
||||
{status: Zcl.Status.NO_IMAGE_AVAILABLE},
|
||||
undefined,
|
||||
data.meta.zclTransactionSequenceNumber,
|
||||
);
|
||||
logger.debug(`Responded to OTA request of '${data.device.name}' with 'NO_IMAGE_AVAILABLE'`);
|
||||
}
|
||||
|
||||
private async readSoftwareBuildIDAndDateCode(device: Device, sendPolicy?: 'immediate'):
|
||||
Promise<{softwareBuildID: string, dateCode: string}> {
|
||||
private async readSoftwareBuildIDAndDateCode(device: Device, sendPolicy?: 'immediate'): Promise<{softwareBuildID: string; dateCode: string}> {
|
||||
try {
|
||||
const endpoint = device.zh.endpoints.find((e) => e.supportsInputCluster('genBasic'));
|
||||
const result = await endpoint.read('genBasic', ['dateCode', 'swBuildId'], {sendPolicy});
|
||||
@ -139,16 +143,20 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
private getEntityPublishPayload(device: Device, state: zhc.OtaUpdateAvailableResult | UpdateState,
|
||||
progress: number=null, remaining: number=null): UpdatePayload {
|
||||
private getEntityPublishPayload(
|
||||
device: Device,
|
||||
state: zhc.OtaUpdateAvailableResult | UpdateState,
|
||||
progress: number = null,
|
||||
remaining: number = null,
|
||||
): UpdatePayload {
|
||||
const deviceUpdateState = this.state.get(device).update;
|
||||
const payload: UpdatePayload = {update: {
|
||||
state: typeof state === 'string' ? state : (state.available ? 'available' : 'idle'),
|
||||
installed_version: typeof state === 'string' ?
|
||||
deviceUpdateState?.installed_version : state.currentFileVersion,
|
||||
latest_version: typeof state === 'string' ?
|
||||
deviceUpdateState?.latest_version : state.otaFileVersion,
|
||||
}};
|
||||
const payload: UpdatePayload = {
|
||||
update: {
|
||||
state: typeof state === 'string' ? state : state.available ? 'available' : 'idle',
|
||||
installed_version: typeof state === 'string' ? deviceUpdateState?.installed_version : state.currentFileVersion,
|
||||
latest_version: typeof state === 'string' ? deviceUpdateState?.latest_version : state.otaFileVersion,
|
||||
},
|
||||
};
|
||||
if (progress !== null) payload.update.progress = progress;
|
||||
if (remaining !== null) payload.update.remaining = Math.round(remaining);
|
||||
|
||||
@ -169,7 +177,7 @@ export default class OTAUpdate extends Extension {
|
||||
const ID = (typeof message === 'object' && message.hasOwnProperty('id') ? 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?: string, to?: string}= {id: ID};
|
||||
const responseData: {id: string; updateAvailable?: boolean; from?: string; to?: string} = {id: ID};
|
||||
let error = null;
|
||||
let errorStack = null;
|
||||
|
||||
@ -181,10 +189,7 @@ export default class OTAUpdate extends Extension {
|
||||
/* 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}),
|
||||
);
|
||||
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}'`;
|
||||
@ -198,10 +203,7 @@ export default class OTAUpdate extends Extension {
|
||||
/* 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}),
|
||||
);
|
||||
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta}));
|
||||
}
|
||||
|
||||
try {
|
||||
@ -212,11 +214,10 @@ export default class OTAUpdate extends Extension {
|
||||
/* 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}),
|
||||
);
|
||||
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);
|
||||
@ -230,23 +231,18 @@ export default class OTAUpdate extends Extension {
|
||||
/* 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}),
|
||||
);
|
||||
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: error, meta}));
|
||||
}
|
||||
}
|
||||
} else { // type === 'update'
|
||||
} 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}),
|
||||
);
|
||||
await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta}));
|
||||
}
|
||||
|
||||
try {
|
||||
@ -272,8 +268,11 @@ export default class OTAUpdate extends Extension {
|
||||
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});
|
||||
const payload = this.getEntityPublishPayload(device, {
|
||||
available: false,
|
||||
currentFileVersion: fileVersion,
|
||||
otaFileVersion: fileVersion,
|
||||
});
|
||||
await this.publishEntityState(device, payload);
|
||||
const to = await this.readSoftwareBuildIDAndDateCode(device);
|
||||
const [fromS, toS] = [stringify(from_), stringify(to)];
|
||||
|
@ -1,14 +1,14 @@
|
||||
|
||||
import * as settings from '../util/settings';
|
||||
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';
|
||||
import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import Group from '../model/group';
|
||||
import Device from '../model/device';
|
||||
import bind from 'bind-decorator';
|
||||
|
||||
let topicGetSetRegex: RegExp;
|
||||
// Used by `publish.test.js` to reload regex when changing `mqtt.base_topic`.
|
||||
@ -37,7 +37,12 @@ const defaultGroupConverters = [
|
||||
zhc.toZigbee.light_hue_saturation_step,
|
||||
];
|
||||
|
||||
interface ParsedTopic {ID: string, endpoint: string, attribute: string, type: 'get' | 'set'}
|
||||
interface ParsedTopic {
|
||||
ID: string;
|
||||
endpoint: string;
|
||||
attribute: string;
|
||||
type: 'get' | 'set';
|
||||
}
|
||||
|
||||
export default class Publish extends Extension {
|
||||
async start(): Promise<void> {
|
||||
@ -92,8 +97,14 @@ export default class Publish extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
legacyRetrieveState(re: Device | Group, converter: zhc.Tz.Converter, result: zhc.Tz.ConvertSetResult,
|
||||
target: zh.Endpoint | zh.Group, key: string, meta: zhc.Tz.Meta): void {
|
||||
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
|
||||
@ -102,9 +113,7 @@ export default class Publish extends Extension {
|
||||
// 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.hasOwnProperty('readAfterWriteTime') &&
|
||||
re.options.retrieve_state
|
||||
) {
|
||||
if (re instanceof Device && result && result.hasOwnProperty('readAfterWriteTime') && re.options.retrieve_state) {
|
||||
setTimeout(() => converter.convertGet(target, key, meta), result.readAfterWriteTime);
|
||||
}
|
||||
}
|
||||
@ -148,9 +157,12 @@ export default class Publish extends Extension {
|
||||
const device = re instanceof Device ? re.zh : null;
|
||||
const entitySettings = re.options;
|
||||
const entityState = this.state.get(re);
|
||||
const membersState = re instanceof Group ?
|
||||
Object.fromEntries(re.zh.members.map((e) => [e.getDevice().ieeeAddr,
|
||||
this.state.get(this.zigbee.resolveEntity(e.getDevice().ieeeAddr))])) : null;
|
||||
const membersState =
|
||||
re instanceof Group
|
||||
? Object.fromEntries(
|
||||
re.zh.members.map((e) => [e.getDevice().ieeeAddr, this.state.get(this.zigbee.resolveEntity(e.getDevice().ieeeAddr))]),
|
||||
)
|
||||
: null;
|
||||
let converters: zhc.Tz.Converter[];
|
||||
{
|
||||
if (Array.isArray(definition)) {
|
||||
@ -199,7 +211,9 @@ export default class Publish extends Extension {
|
||||
const endpointNames = re instanceof Device ? re.getEndpointNames() : [];
|
||||
const propertyEndpointRegex = new RegExp(`^(.*?)_(${endpointNames.join('|')})$`);
|
||||
|
||||
for (let [key, value] of entries) {
|
||||
for (const entry of entries) {
|
||||
let key = entry[0];
|
||||
const value = entry[1];
|
||||
let endpointName = parsedTopic.endpoint;
|
||||
let localTarget = target;
|
||||
let endpointOrGroupID = utils.isEndpoint(target) ? target.ID : target.groupID;
|
||||
@ -215,8 +229,7 @@ export default class Publish extends Extension {
|
||||
|
||||
if (!usedConverters.hasOwnProperty(endpointOrGroupID)) usedConverters[endpointOrGroupID] = [];
|
||||
/* istanbul ignore next */
|
||||
const converter = converters.find((c) =>
|
||||
c.key.includes(key) && (!c.endpoint || c.endpoint == endpointName));
|
||||
const converter = converters.find((c) => c.key.includes(key) && (!c.endpoint || c.endpoint == endpointName));
|
||||
|
||||
if (parsedTopic.type === 'set' && usedConverters[endpointOrGroupID].includes(converter)) {
|
||||
// Use a converter for set only once
|
||||
@ -230,16 +243,21 @@ export default class Publish extends Extension {
|
||||
}
|
||||
|
||||
// If the endpoint_name name is a number, try to map it to a friendlyName
|
||||
if (!isNaN(Number(endpointName)) && re.isDevice() && utils.isEndpoint(localTarget) &&
|
||||
re.endpointName(localTarget)) {
|
||||
if (!isNaN(Number(endpointName)) && re.isDevice() && utils.isEndpoint(localTarget) && re.endpointName(localTarget)) {
|
||||
endpointName = re.endpointName(localTarget);
|
||||
}
|
||||
|
||||
// Converter didn't return a result, skip
|
||||
const entitySettingsKeyValue: KeyValue = entitySettings;
|
||||
const meta = {
|
||||
endpoint_name: endpointName, options: entitySettingsKeyValue,
|
||||
message: {...message}, logger, device, state: entityState, membersState, mapped: definition,
|
||||
endpoint_name: endpointName,
|
||||
options: entitySettingsKeyValue,
|
||||
message: {...message},
|
||||
logger,
|
||||
device,
|
||||
state: entityState,
|
||||
membersState,
|
||||
mapped: definition,
|
||||
};
|
||||
|
||||
// Strip endpoint name from meta.message properties.
|
||||
@ -289,8 +307,7 @@ export default class Publish extends Extension {
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
`Publish '${parsedTopic.type}' '${key}' to '${re.name}' failed: '${error}'`;
|
||||
const message = `Publish '${parsedTopic.type}' '${key}' to '${re.name}' failed: '${error}'`;
|
||||
logger.error(message);
|
||||
logger.debug(error.stack);
|
||||
await this.legacyLog({type: `zigbee_publish_error`, message, meta: {friendly_name: re.name}});
|
||||
@ -305,8 +322,7 @@ export default class Publish extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
const scenesChanged = Object.values(usedConverters)
|
||||
.some((cl) => cl.some((c) => c.key.some((k) => sceneConverterKeys.includes(k))));
|
||||
const scenesChanged = Object.values(usedConverters).some((cl) => cl.some((c) => c.key.some((k) => sceneConverterKeys.includes(k))));
|
||||
if (scenesChanged) {
|
||||
this.eventBus.emitScenesChanged({entity: re});
|
||||
}
|
||||
|
@ -1,17 +1,18 @@
|
||||
import * as settings from '../util/settings';
|
||||
import logger from '../util/logger';
|
||||
import debounce from 'debounce';
|
||||
import Extension from './extension';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import bind from 'bind-decorator';
|
||||
import utils from '../util/utils';
|
||||
import debounce from 'debounce';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
type DebounceFunction = (() => void) & { clear(): void; } & { flush(): void; };
|
||||
import logger from '../util/logger';
|
||||
import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
|
||||
type DebounceFunction = (() => void) & {clear(): void} & {flush(): void};
|
||||
|
||||
export default class Receive extends Extension {
|
||||
private elapsed: {[s: string]: number} = {};
|
||||
private debouncers: {[s: string]: {payload: KeyValue, publish: DebounceFunction }} = {};
|
||||
private debouncers: {[s: string]: {payload: KeyValue; publish: DebounceFunction}} = {};
|
||||
|
||||
async start(): Promise<void> {
|
||||
this.eventBus.onPublishEntityState(this, this.onPublishEntityState);
|
||||
@ -24,8 +25,12 @@ export default class Receive extends Extension {
|
||||
* In case that e.g. the state is currently held back by a debounce and a new state is published
|
||||
* remove it from the to be send debounced message.
|
||||
*/
|
||||
if (data.entity.isDevice() && this.debouncers[data.entity.ieeeAddr] &&
|
||||
data.stateChangeReason !== 'publishDebounce' && data.stateChangeReason !== 'lastSeenChanged') {
|
||||
if (
|
||||
data.entity.isDevice() &&
|
||||
this.debouncers[data.entity.ieeeAddr] &&
|
||||
data.stateChangeReason !== 'publishDebounce' &&
|
||||
data.stateChangeReason !== 'lastSeenChanged'
|
||||
) {
|
||||
for (const key of Object.keys(data.payload)) {
|
||||
delete this.debouncers[data.entity.ieeeAddr].payload[key];
|
||||
}
|
||||
@ -91,8 +96,7 @@ export default class Receive extends Extension {
|
||||
if (!data.device) return;
|
||||
|
||||
if (!this.shouldProcess(data)) {
|
||||
await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'},
|
||||
settings.get(), true, this.publishEntityState);
|
||||
await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -104,10 +108,11 @@ export default class Receive extends Extension {
|
||||
// Check if there is an available converter, genOta messages are not interesting.
|
||||
const ignoreClusters: (string | number)[] = ['genOta', 'genTime', 'genBasic', 'genPollCtrl'];
|
||||
if (converters.length == 0 && !ignoreClusters.includes(data.cluster)) {
|
||||
logger.debug(`No converter available for '${data.device.definition.model}' with ` +
|
||||
`cluster '${data.cluster}' and type '${data.type}' and data '${stringify(data.data)}'`);
|
||||
await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'},
|
||||
settings.get(), true, this.publishEntityState);
|
||||
logger.debug(
|
||||
`No converter available for '${data.device.definition.model}' with ` +
|
||||
`cluster '${data.cluster}' and type '${data.type}' and data '${stringify(data.data)}'`,
|
||||
);
|
||||
await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -131,8 +136,7 @@ export default class Receive extends Extension {
|
||||
|
||||
// Check if we have to debounce
|
||||
if (data.device.options.debounce) {
|
||||
this.publishDebounce(data.device, payload, data.device.options.debounce,
|
||||
data.device.options.debounce_ignore);
|
||||
this.publishDebounce(data.device, payload, data.device.options.debounce, data.device.options.debounce_ignore);
|
||||
} else {
|
||||
await this.publishEntityState(data.device, payload);
|
||||
}
|
||||
@ -143,15 +147,13 @@ export default class Receive extends Extension {
|
||||
this.eventBus.emitExposesChanged({device: data.device});
|
||||
};
|
||||
|
||||
const meta = {device: data.device.zh, logger, state: this.state.get(data.device),
|
||||
deviceExposesChanged: deviceExposesChanged};
|
||||
const meta = {device: data.device.zh, logger, state: this.state.get(data.device), deviceExposesChanged: deviceExposesChanged};
|
||||
let payload: KeyValue = {};
|
||||
for (const converter of converters) {
|
||||
try {
|
||||
const convertData = {...data, device: data.device.zh};
|
||||
const options: KeyValue = data.device.options;
|
||||
const converted = await converter.convert(
|
||||
data.device.definition, convertData, publish, options, meta);
|
||||
const converted = await converter.convert(data.device.definition, convertData, publish, options, meta);
|
||||
if (converted) {
|
||||
payload = {...payload, ...converted};
|
||||
}
|
||||
@ -164,8 +166,7 @@ export default class Receive extends Extension {
|
||||
if (Object.keys(payload).length) {
|
||||
await publish(payload);
|
||||
} else {
|
||||
await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'},
|
||||
settings.get(), true, this.publishEntityState);
|
||||
await utils.publishLastSeen({device: data.device, reason: 'messageEmitted'}, settings.get(), true, this.publishEntityState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,23 @@
|
||||
/* eslint-disable brace-style */
|
||||
import * as settings from '../util/settings';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
import {CustomClusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import * as settings from '../util/settings';
|
||||
|
||||
export default class Device {
|
||||
public zh: zh.Device;
|
||||
public definition: zhc.Definition;
|
||||
private _definitionModelID: string;
|
||||
|
||||
get ieeeAddr(): string {return this.zh.ieeeAddr;}
|
||||
get ID(): string {return this.zh.ieeeAddr;}
|
||||
get options(): DeviceOptions {return {...settings.get().device_options, ...settings.getDevice(this.ieeeAddr)};}
|
||||
get ieeeAddr(): string {
|
||||
return this.zh.ieeeAddr;
|
||||
}
|
||||
get ID(): string {
|
||||
return this.zh.ieeeAddr;
|
||||
}
|
||||
get options(): DeviceOptions {
|
||||
return {...settings.get().device_options, ...settings.getDevice(this.ieeeAddr)};
|
||||
}
|
||||
get name(): string {
|
||||
return this.zh.type === 'Coordinator' ? 'Coordinator' : this.options?.friendly_name || this.ieeeAddr;
|
||||
}
|
||||
@ -86,9 +93,15 @@ export default class Device {
|
||||
return Object.keys(this.definition?.endpoint?.(this.zh) ?? {}).filter((name) => name !== 'default');
|
||||
}
|
||||
|
||||
isIkeaTradfri(): boolean {return this.zh.manufacturerID === 4476;}
|
||||
isIkeaTradfri(): boolean {
|
||||
return this.zh.manufacturerID === 4476;
|
||||
}
|
||||
|
||||
isDevice(): this is Device {return true;}
|
||||
isDevice(): this is Device {
|
||||
return true;
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
isGroup(): this is Group {return false;}
|
||||
isGroup(): this is Group {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,21 @@
|
||||
/* eslint-disable brace-style */
|
||||
import * as settings from '../util/settings';
|
||||
import * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import * as settings from '../util/settings';
|
||||
|
||||
export default class Group {
|
||||
public zh: zh.Group;
|
||||
private resolveDevice: (ieeeAddr: string) => Device;
|
||||
|
||||
get ID(): number {return this.zh.groupID;}
|
||||
get options(): GroupOptions {return {...settings.getGroup(this.ID)};}
|
||||
get name(): string {return this.options?.friendly_name || this.ID.toString();}
|
||||
get ID(): number {
|
||||
return this.zh.groupID;
|
||||
}
|
||||
get options(): GroupOptions {
|
||||
return {...settings.getGroup(this.ID)};
|
||||
}
|
||||
get name(): string {
|
||||
return this.options?.friendly_name || this.ID.toString();
|
||||
}
|
||||
|
||||
constructor(group: zh.Group, resolveDevice: (ieeeAddr: string) => Device) {
|
||||
this.zh = group;
|
||||
@ -24,9 +31,15 @@ export default class Group {
|
||||
}
|
||||
|
||||
membersDefinitions(): zhc.Definition[] {
|
||||
return this.membersDevices().map((d) => d.definition).filter((d) => d);
|
||||
return this.membersDevices()
|
||||
.map((d) => d.definition)
|
||||
.filter((d) => d);
|
||||
}
|
||||
|
||||
isDevice(): this is Device {return false;}
|
||||
isGroup(): this is Group {return true;}
|
||||
isDevice(): this is Device {
|
||||
return false;
|
||||
}
|
||||
isGroup(): this is Group {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
25
lib/mqtt.ts
25
lib/mqtt.ts
@ -1,10 +1,12 @@
|
||||
import type {QoS} from 'mqtt-packet';
|
||||
|
||||
import bind from 'bind-decorator';
|
||||
import fs from 'fs';
|
||||
import * as mqtt from 'mqtt';
|
||||
|
||||
import logger from './util/logger';
|
||||
import * as settings from './util/settings';
|
||||
import utils from './util/utils';
|
||||
import fs from 'fs';
|
||||
import bind from 'bind-decorator';
|
||||
import type {QoS} from 'mqtt-packet';
|
||||
|
||||
const NS = 'z2m:mqtt';
|
||||
|
||||
@ -15,8 +17,9 @@ export default class MQTT {
|
||||
private eventBus: EventBus;
|
||||
private initialConnect = true;
|
||||
private republishRetainedTimer: NodeJS.Timeout;
|
||||
private retainedMessages: {[s: string]: {payload: string, options: MQTTOptions,
|
||||
skipLog: boolean, skipReceive: boolean, topic: string, base: string}} = {};
|
||||
private retainedMessages: {
|
||||
[s: string]: {payload: string; options: MQTTOptions; skipLog: boolean; skipReceive: boolean; topic: string; base: string};
|
||||
} = {};
|
||||
|
||||
constructor(eventBus: EventBus) {
|
||||
this.eventBus = eventBus;
|
||||
@ -155,9 +158,15 @@ export default class MQTT {
|
||||
return this.client && !this.client.reconnecting;
|
||||
}
|
||||
|
||||
async publish(topic: string, payload: string, options: MQTTOptions={}, base=settings.get().mqtt.base_topic, skipLog=false, skipReceive=true)
|
||||
: Promise<void> {
|
||||
const defaultOptions: {qos: QoS, retain: boolean} = {qos: 0, retain: false};
|
||||
async publish(
|
||||
topic: string,
|
||||
payload: string,
|
||||
options: MQTTOptions = {},
|
||||
base = settings.get().mqtt.base_topic,
|
||||
skipLog = false,
|
||||
skipReceive = true,
|
||||
): Promise<void> {
|
||||
const defaultOptions: {qos: QoS; retain: boolean} = {qos: 0, retain: false};
|
||||
topic = `${base}/${topic}`;
|
||||
|
||||
if (skipReceive) {
|
||||
|
38
lib/state.ts
38
lib/state.ts
@ -1,16 +1,33 @@
|
||||
import logger from './util/logger';
|
||||
import data from './util/data';
|
||||
import * as settings from './util/settings';
|
||||
import utils from './util/utils';
|
||||
import fs from 'fs';
|
||||
import objectAssignDeep from 'object-assign-deep';
|
||||
|
||||
import data from './util/data';
|
||||
import logger from './util/logger';
|
||||
import * as settings from './util/settings';
|
||||
import utils from './util/utils';
|
||||
|
||||
const saveInterval = 1000 * 60 * 5; // 5 minutes
|
||||
|
||||
const dontCacheProperties = [
|
||||
'action', 'action_.*', 'button', 'button_left', 'button_right', 'click', 'forgotten', 'keyerror',
|
||||
'step_size', 'transition_time', 'group_list', 'group_capacity', 'no_occupancy_since',
|
||||
'step_mode', 'transition_time', 'duration', 'elapsed', 'from_side', 'to_side',
|
||||
'action',
|
||||
'action_.*',
|
||||
'button',
|
||||
'button_left',
|
||||
'button_right',
|
||||
'click',
|
||||
'forgotten',
|
||||
'keyerror',
|
||||
'step_size',
|
||||
'transition_time',
|
||||
'group_list',
|
||||
'group_capacity',
|
||||
'no_occupancy_since',
|
||||
'step_mode',
|
||||
'transition_time',
|
||||
'duration',
|
||||
'elapsed',
|
||||
'from_side',
|
||||
'to_side',
|
||||
];
|
||||
|
||||
class State {
|
||||
@ -18,7 +35,10 @@ class State {
|
||||
private file = data.joinPath('state.json');
|
||||
private timer: NodeJS.Timeout = null;
|
||||
|
||||
constructor(private readonly eventBus: EventBus, private readonly zigbee: Zigbee) {
|
||||
constructor(
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly zigbee: Zigbee,
|
||||
) {
|
||||
this.eventBus = eventBus;
|
||||
this.zigbee = zigbee;
|
||||
}
|
||||
@ -75,7 +95,7 @@ class State {
|
||||
return this.state[entity.ID] || {};
|
||||
}
|
||||
|
||||
set(entity: Group | Device, update: KeyValue, reason: string=null): KeyValue {
|
||||
set(entity: Group | Device, update: KeyValue, reason: string = null): KeyValue {
|
||||
const fromState = this.state[entity.ID] || {};
|
||||
const toState = objectAssignDeep({}, fromState, update);
|
||||
const newCache = {...toState};
|
||||
|
357
lib/types/types.d.ts
vendored
357
lib/types/types.d.ts
vendored
@ -1,11 +1,12 @@
|
||||
/* eslint-disable camelcase */
|
||||
import {LogLevel} from 'lib/util/settings';
|
||||
import type {
|
||||
Device as ZHDevice,
|
||||
Group as ZHGroup,
|
||||
Endpoint as ZHEndpoint,
|
||||
} from 'zigbee-herdsman/dist/controller/model';
|
||||
|
||||
import type TypeEventBus from 'lib/eventBus';
|
||||
import type TypeExtension from 'lib/extension/extension';
|
||||
import type TypeDevice from 'lib/model/device';
|
||||
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 {
|
||||
NetworkParameters as ZHNetworkParameters,
|
||||
CoordinatorVersion as ZHCoordinatorVersion,
|
||||
@ -13,25 +14,12 @@ import type {
|
||||
RoutingTable as ZHRoutingTable,
|
||||
RoutingTableEntry as ZHRoutingTableEntry,
|
||||
} from 'zigbee-herdsman/dist/adapter/tstype';
|
||||
|
||||
import type {
|
||||
Cluster as ZHCluster,
|
||||
FrameControl as ZHFrameControl,
|
||||
} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
|
||||
|
||||
import type * as ZHEvents from 'zigbee-herdsman/dist/controller/events';
|
||||
import type {Device as ZHDevice, Group as ZHGroup, Endpoint as ZHEndpoint} from 'zigbee-herdsman/dist/controller/model';
|
||||
import type {Cluster as ZHCluster, FrameControl as ZHFrameControl} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
|
||||
import type * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import type * as ZHEvents from 'zigbee-herdsman/dist/controller/events';
|
||||
|
||||
import type TypeEventBus from 'lib/eventBus';
|
||||
import type TypeMQTT from 'lib/mqtt';
|
||||
import type TypeState from 'lib/state';
|
||||
import type TypeZigbee from 'lib/zigbee';
|
||||
import type TypeDevice from 'lib/model/device';
|
||||
import type TypeGroup from 'lib/model/group';
|
||||
import type TypeExtension from 'lib/extension/extension';
|
||||
|
||||
import type {QoS} from 'mqtt-packet';
|
||||
import {LogLevel} from 'lib/util/settings';
|
||||
|
||||
declare global {
|
||||
// Define some class types as global
|
||||
@ -46,15 +34,25 @@ declare global {
|
||||
// Types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ExternalDefinition = zhc.Definition & {homeassistant: any};
|
||||
interface MQTTResponse {data: KeyValue, status: 'error' | 'ok', error?: string, transaction?: string}
|
||||
interface MQTTOptions {qos?: QoS, retain?: boolean, properties?: {messageExpiryInterval: number}}
|
||||
type Scene = {id: number, name: string};
|
||||
interface MQTTResponse {
|
||||
data: KeyValue;
|
||||
status: 'error' | 'ok';
|
||||
error?: string;
|
||||
transaction?: string;
|
||||
}
|
||||
interface MQTTOptions {
|
||||
qos?: QoS;
|
||||
retain?: boolean;
|
||||
properties?: {messageExpiryInterval: number};
|
||||
}
|
||||
type Scene = {id: number; name: string};
|
||||
type StateChangeReason = 'publishDebounce' | 'groupOptimistic' | 'lastSeenChanged' | 'publishCached';
|
||||
type PublishEntityState = (entity: Device | Group, payload: KeyValue,
|
||||
stateChangeReason?: StateChangeReason) => Promise<void>;
|
||||
type RecursivePartial<T> = {[P in keyof T]?: RecursivePartial<T[P]>;};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
interface KeyValue {[s: string]: any}
|
||||
type PublishEntityState = (entity: Device | Group, payload: KeyValue, stateChangeReason?: StateChangeReason) => Promise<void>;
|
||||
type RecursivePartial<T> = {[P in keyof T]?: RecursivePartial<T[P]>};
|
||||
interface KeyValue {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[s: string]: any;
|
||||
}
|
||||
|
||||
// zigbee-herdsman
|
||||
namespace zh {
|
||||
@ -74,27 +72,32 @@ declare global {
|
||||
}
|
||||
|
||||
namespace eventdata {
|
||||
type EntityRenamed = { entity: Device | Group, homeAssisantRename: boolean, from: string, to: string };
|
||||
type DeviceRemoved = { ieeeAddr: string, name: string };
|
||||
type MQTTMessage = { topic: string, message: string };
|
||||
type MQTTMessagePublished = { topic: string, payload: string, options: {retain: boolean, qos: number} };
|
||||
type EntityRenamed = {entity: Device | Group; homeAssisantRename: boolean; from: string; to: string};
|
||||
type DeviceRemoved = {ieeeAddr: string; name: string};
|
||||
type MQTTMessage = {topic: string; message: string};
|
||||
type MQTTMessagePublished = {topic: string; payload: string; options: {retain: boolean; qos: number}};
|
||||
type StateChange = {
|
||||
entity: Device | Group, from: KeyValue, to: KeyValue, reason: string | null, update: KeyValue };
|
||||
entity: Device | Group;
|
||||
from: KeyValue;
|
||||
to: KeyValue;
|
||||
reason: string | null;
|
||||
update: KeyValue;
|
||||
};
|
||||
type PermitJoinChanged = ZHEvents.PermitJoinChangedPayload;
|
||||
type LastSeenChanged = { device: Device,
|
||||
reason: 'deviceAnnounce' | 'networkAddress' | 'deviceJoined' | 'messageEmitted' | 'messageNonEmitted'; };
|
||||
type DeviceNetworkAddressChanged = { device: Device };
|
||||
type DeviceAnnounce = { device: Device };
|
||||
type DeviceInterview = { device: Device, status: 'started' | 'successful' | 'failed' };
|
||||
type DeviceJoined = { device: Device };
|
||||
type EntityOptionsChanged = { entity: Device | Group, from: KeyValue, to: KeyValue };
|
||||
type ExposesChanged = { device: Device };
|
||||
type Reconfigure = { device: Device };
|
||||
type DeviceLeave = { ieeeAddr: string, name: string };
|
||||
type GroupMembersChanged = {group: Group, action: 'remove' | 'add' | 'remove_all',
|
||||
endpoint: zh.Endpoint, skipDisableReporting: boolean };
|
||||
type PublishEntityState = {entity: Group | Device, message: KeyValue, stateChangeReason: StateChangeReason,
|
||||
payload: KeyValue};
|
||||
type LastSeenChanged = {
|
||||
device: Device;
|
||||
reason: 'deviceAnnounce' | 'networkAddress' | 'deviceJoined' | 'messageEmitted' | 'messageNonEmitted';
|
||||
};
|
||||
type DeviceNetworkAddressChanged = {device: Device};
|
||||
type DeviceAnnounce = {device: Device};
|
||||
type DeviceInterview = {device: Device; status: 'started' | 'successful' | 'failed'};
|
||||
type DeviceJoined = {device: Device};
|
||||
type EntityOptionsChanged = {entity: Device | Group; from: KeyValue; to: KeyValue};
|
||||
type ExposesChanged = {device: Device};
|
||||
type Reconfigure = {device: Device};
|
||||
type DeviceLeave = {ieeeAddr: string; name: string};
|
||||
type GroupMembersChanged = {group: Group; action: 'remove' | 'add' | 'remove_all'; endpoint: zh.Endpoint; skipDisableReporting: boolean};
|
||||
type PublishEntityState = {entity: Group | Device; message: KeyValue; stateChangeReason: StateChangeReason; payload: KeyValue};
|
||||
type DeviceMessage = {
|
||||
type: ZHEvents.MessagePayloadType;
|
||||
device: Device;
|
||||
@ -103,157 +106,157 @@ declare global {
|
||||
groupID: number;
|
||||
cluster: string | number;
|
||||
data: KeyValue | Array<string | number>;
|
||||
meta: {zclTransactionSequenceNumber?: number; manufacturerCode?: number; frameControl?: ZHFrameControl;};
|
||||
meta: {zclTransactionSequenceNumber?: number; manufacturerCode?: number; frameControl?: ZHFrameControl};
|
||||
};
|
||||
type ScenesChanged = { entity: Device | Group };
|
||||
type ScenesChanged = {entity: Device | Group};
|
||||
}
|
||||
|
||||
// Settings
|
||||
// eslint-disable camelcase
|
||||
interface Settings {
|
||||
homeassistant?: {
|
||||
discovery_topic: string,
|
||||
status_topic: string,
|
||||
legacy_entity_attributes: boolean,
|
||||
legacy_triggers: boolean,
|
||||
},
|
||||
permit_join?: boolean,
|
||||
discovery_topic: string;
|
||||
status_topic: string;
|
||||
legacy_entity_attributes: boolean;
|
||||
legacy_triggers: boolean;
|
||||
};
|
||||
permit_join?: boolean;
|
||||
availability?: {
|
||||
active: {timeout: number},
|
||||
passive: {timeout: number}
|
||||
},
|
||||
external_converters: string[],
|
||||
active: {timeout: number};
|
||||
passive: {timeout: number};
|
||||
};
|
||||
external_converters: string[];
|
||||
mqtt: {
|
||||
base_topic: string,
|
||||
include_device_information: boolean,
|
||||
force_disable_retain: boolean
|
||||
version?: 3 | 4 | 5,
|
||||
user?: string,
|
||||
password?: string,
|
||||
server: string,
|
||||
ca?: string,
|
||||
keepalive?: number,
|
||||
key?: string,
|
||||
cert?: string,
|
||||
client_id?: string,
|
||||
reject_unauthorized?: boolean,
|
||||
},
|
||||
base_topic: string;
|
||||
include_device_information: boolean;
|
||||
force_disable_retain: boolean;
|
||||
version?: 3 | 4 | 5;
|
||||
user?: string;
|
||||
password?: string;
|
||||
server: string;
|
||||
ca?: string;
|
||||
keepalive?: number;
|
||||
key?: string;
|
||||
cert?: string;
|
||||
client_id?: string;
|
||||
reject_unauthorized?: boolean;
|
||||
};
|
||||
serial: {
|
||||
disable_led: boolean,
|
||||
port?: string,
|
||||
adapter?: 'deconz' | 'zstack' | 'ezsp' | 'zigate' | 'ember',
|
||||
baudrate?: number,
|
||||
rtscts?: boolean,
|
||||
},
|
||||
passlist: string[],
|
||||
blocklist: string[],
|
||||
disable_led: boolean;
|
||||
port?: string;
|
||||
adapter?: 'deconz' | 'zstack' | 'ezsp' | 'zigate' | 'ember';
|
||||
baudrate?: number;
|
||||
rtscts?: boolean;
|
||||
};
|
||||
passlist: string[];
|
||||
blocklist: string[];
|
||||
map_options: {
|
||||
graphviz: {
|
||||
colors: {
|
||||
fill: {
|
||||
enddevice: string,
|
||||
coordinator: string,
|
||||
router: string,
|
||||
},
|
||||
enddevice: string;
|
||||
coordinator: string;
|
||||
router: string;
|
||||
};
|
||||
font: {
|
||||
coordinator: string,
|
||||
router: string,
|
||||
enddevice: string,
|
||||
},
|
||||
coordinator: string;
|
||||
router: string;
|
||||
enddevice: string;
|
||||
};
|
||||
line: {
|
||||
active: string,
|
||||
inactive: string,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
active: string;
|
||||
inactive: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
ota: {
|
||||
update_check_interval: number,
|
||||
disable_automatic_update_check: boolean,
|
||||
zigbee_ota_override_index_location?: string,
|
||||
ikea_ota_use_test_url?: boolean,
|
||||
},
|
||||
update_check_interval: number;
|
||||
disable_automatic_update_check: boolean;
|
||||
zigbee_ota_override_index_location?: string;
|
||||
ikea_ota_use_test_url?: boolean;
|
||||
};
|
||||
frontend?: {
|
||||
auth_token?: string,
|
||||
host?: string,
|
||||
port?: number,
|
||||
url?: string,
|
||||
ssl_cert?: string,
|
||||
ssl_key?: string,
|
||||
},
|
||||
devices?: {[s: string]: DeviceOptions},
|
||||
groups?: {[s: string]: GroupOptions},
|
||||
device_options: KeyValue,
|
||||
auth_token?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
url?: string;
|
||||
ssl_cert?: string;
|
||||
ssl_key?: string;
|
||||
};
|
||||
devices?: {[s: string]: DeviceOptions};
|
||||
groups?: {[s: string]: GroupOptions};
|
||||
device_options: KeyValue;
|
||||
advanced: {
|
||||
legacy_api: boolean,
|
||||
legacy_availability_payload: boolean,
|
||||
log_rotation: boolean,
|
||||
log_symlink_current: boolean,
|
||||
log_output: ('console' | 'file' | 'syslog')[],
|
||||
log_directory: string,
|
||||
log_file: string,
|
||||
log_level: LogLevel,
|
||||
log_namespaced_levels: Record<string, LogLevel>,
|
||||
log_syslog: KeyValue,
|
||||
log_debug_to_mqtt_frontend: boolean,
|
||||
log_debug_namespace_ignore: string,
|
||||
pan_id: number | 'GENERATE',
|
||||
ext_pan_id: number[] | 'GENERATE',
|
||||
channel: number,
|
||||
adapter_concurrent: number | null,
|
||||
adapter_delay: number | null,
|
||||
cache_state: boolean,
|
||||
cache_state_persistent: boolean,
|
||||
cache_state_send_on_startup: boolean,
|
||||
last_seen: 'disable' | 'ISO_8601' | 'ISO_8601_local' | 'epoch',
|
||||
elapsed: boolean,
|
||||
network_key: number[] | 'GENERATE',
|
||||
timestamp_format: string,
|
||||
output: 'json' | 'attribute' | 'attribute_and_json',
|
||||
transmit_power?: number,
|
||||
legacy_api: boolean;
|
||||
legacy_availability_payload: boolean;
|
||||
log_rotation: boolean;
|
||||
log_symlink_current: boolean;
|
||||
log_output: ('console' | 'file' | 'syslog')[];
|
||||
log_directory: string;
|
||||
log_file: string;
|
||||
log_level: LogLevel;
|
||||
log_namespaced_levels: Record<string, LogLevel>;
|
||||
log_syslog: KeyValue;
|
||||
log_debug_to_mqtt_frontend: boolean;
|
||||
log_debug_namespace_ignore: string;
|
||||
pan_id: number | 'GENERATE';
|
||||
ext_pan_id: number[] | 'GENERATE';
|
||||
channel: number;
|
||||
adapter_concurrent: number | null;
|
||||
adapter_delay: number | null;
|
||||
cache_state: boolean;
|
||||
cache_state_persistent: boolean;
|
||||
cache_state_send_on_startup: boolean;
|
||||
last_seen: 'disable' | 'ISO_8601' | 'ISO_8601_local' | 'epoch';
|
||||
elapsed: boolean;
|
||||
network_key: number[] | 'GENERATE';
|
||||
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,
|
||||
},
|
||||
availability_timeout?: number;
|
||||
availability_blocklist?: string[];
|
||||
availability_passlist?: string[];
|
||||
availability_blacklist?: string[];
|
||||
availability_whitelist?: string[];
|
||||
soft_reset_timeout: number;
|
||||
report: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeviceOptions {
|
||||
ID?: string,
|
||||
disabled?: boolean,
|
||||
retention?: number,
|
||||
availability?: boolean | {timeout: number},
|
||||
optimistic?: boolean,
|
||||
retrieve_state?: boolean,
|
||||
debounce?: number,
|
||||
debounce_ignore?: string[],
|
||||
filtered_attributes?: string[],
|
||||
filtered_cache?: string[],
|
||||
filtered_optimistic?: string[],
|
||||
icon?: string,
|
||||
homeassistant?: KeyValue,
|
||||
legacy?: boolean,
|
||||
friendly_name: string,
|
||||
description?: string,
|
||||
qos?: 0 | 1 | 2,
|
||||
ID?: string;
|
||||
disabled?: boolean;
|
||||
retention?: number;
|
||||
availability?: boolean | {timeout: number};
|
||||
optimistic?: boolean;
|
||||
retrieve_state?: boolean;
|
||||
debounce?: number;
|
||||
debounce_ignore?: string[];
|
||||
filtered_attributes?: string[];
|
||||
filtered_cache?: string[];
|
||||
filtered_optimistic?: string[];
|
||||
icon?: string;
|
||||
homeassistant?: KeyValue;
|
||||
legacy?: boolean;
|
||||
friendly_name: string;
|
||||
description?: string;
|
||||
qos?: 0 | 1 | 2;
|
||||
}
|
||||
|
||||
interface GroupOptions {
|
||||
devices?: string[],
|
||||
ID?: number,
|
||||
optimistic?: boolean,
|
||||
off_state?: 'all_members_off' | 'last_member_state'
|
||||
filtered_attributes?: string[],
|
||||
filtered_cache?: string[],
|
||||
filtered_optimistic?: string[],
|
||||
retrieve_state?: boolean,
|
||||
homeassistant?: KeyValue,
|
||||
friendly_name: string,
|
||||
description?: string,
|
||||
qos?: 0 | 1 | 2,
|
||||
devices?: string[];
|
||||
ID?: number;
|
||||
optimistic?: boolean;
|
||||
off_state?: 'all_members_off' | 'last_member_state';
|
||||
filtered_attributes?: string[];
|
||||
filtered_cache?: string[];
|
||||
filtered_optimistic?: string[];
|
||||
retrieve_state?: boolean;
|
||||
homeassistant?: KeyValue;
|
||||
friendly_name: string;
|
||||
description?: string;
|
||||
qos?: 0 | 1 | 2;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import winston from 'winston';
|
||||
import moment from 'moment';
|
||||
import * as settings from './settings';
|
||||
import path from 'path';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs';
|
||||
import fx from 'mkdir-recursive';
|
||||
import moment from 'moment';
|
||||
import path from 'path';
|
||||
import {rimrafSync} from 'rimraf';
|
||||
import assert from 'assert';
|
||||
import winston from 'winston';
|
||||
|
||||
import * as settings from './settings';
|
||||
|
||||
const NAMESPACE_SEPARATOR = ':';
|
||||
|
||||
@ -30,19 +31,13 @@ class Logger {
|
||||
this.namespacedLevels = settings.get().advanced.log_namespaced_levels;
|
||||
this.cachedNamespacedLevels = Object.assign({}, this.namespacedLevels);
|
||||
|
||||
assert(
|
||||
settings.LOG_LEVELS.includes(this.level),
|
||||
`'${this.level}' is not valid log_level, use one of '${settings.LOG_LEVELS.join(', ')}'`,
|
||||
);
|
||||
assert(settings.LOG_LEVELS.includes(this.level), `'${this.level}' is not valid log_level, use one of '${settings.LOG_LEVELS.join(', ')}'`);
|
||||
|
||||
const timestampFormat = (): string => moment().format(settings.get().advanced.timestamp_format);
|
||||
|
||||
this.logger = winston.createLogger({
|
||||
level: 'debug',
|
||||
format: winston.format.combine(
|
||||
winston.format.errors({stack: true}),
|
||||
winston.format.timestamp({format: timestampFormat}),
|
||||
),
|
||||
format: winston.format.combine(winston.format.errors({stack: true}), winston.format.timestamp({format: timestampFormat})),
|
||||
levels: winston.config.syslog.levels,
|
||||
});
|
||||
|
||||
@ -51,16 +46,20 @@ class Logger {
|
||||
let logging = `Logging to console${consoleSilenced ? ' (silenced)' : ''}`;
|
||||
|
||||
// Setup default console logger
|
||||
this.logger.add(new winston.transports.Console({
|
||||
silent: consoleSilenced,
|
||||
// winston.config.syslog.levels sets 'warning' as 'red'
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize({colors: {debug: 'blue', info: 'green', warning: 'yellow', error: 'red'}}),
|
||||
winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
return `[${info.timestamp}] ${info.level}: \t${info.message}`;
|
||||
}),
|
||||
),
|
||||
}));
|
||||
this.logger.add(
|
||||
new winston.transports.Console({
|
||||
silent: consoleSilenced,
|
||||
// winston.config.syslog.levels sets 'warning' as 'red'
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize({colors: {debug: 'blue', info: 'green', warning: 'yellow', error: 'red'}}),
|
||||
winston.format.printf(
|
||||
/* istanbul ignore next */ (info) => {
|
||||
return `[${info.timestamp}] ${info.level}: \t${info.message}`;
|
||||
},
|
||||
),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
if (this.output.includes('file')) {
|
||||
logging += `, file (filename: ${logFilename})`;
|
||||
@ -79,13 +78,14 @@ class Logger {
|
||||
}
|
||||
|
||||
// Add file logger when enabled
|
||||
// eslint-disable-next-line max-len
|
||||
// NOTE: the initiation of the logger even when not added as transport tries to create the logging directory
|
||||
const transportFileOptions: winston.transports.FileTransportOptions = {
|
||||
filename: path.join(this.directory, logFilename),
|
||||
format: winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
return `[${info.timestamp}] ${info.level}: \t${info.message}`;
|
||||
}),
|
||||
format: winston.format.printf(
|
||||
/* istanbul ignore next */ (info) => {
|
||||
return `[${info.timestamp}] ${info.level}: \t${info.message}`;
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
if (settings.get().advanced.log_rotation) {
|
||||
@ -135,7 +135,7 @@ class Logger {
|
||||
}
|
||||
|
||||
public getDebugNamespaceIgnore(): string {
|
||||
return this.debugNamespaceIgnoreRegex?.toString().slice(1, -1)/* remove slashes */ ?? '';
|
||||
return this.debugNamespaceIgnoreRegex?.toString().slice(1, -1) /* remove slashes */ ?? '';
|
||||
}
|
||||
|
||||
public setDebugNamespaceIgnore(value: string): void {
|
||||
@ -164,14 +164,14 @@ class Logger {
|
||||
this.cachedNamespacedLevels = Object.assign({}, this.namespacedLevels);
|
||||
}
|
||||
|
||||
private cacheNamespacedLevel(namespace: string) : string {
|
||||
private cacheNamespacedLevel(namespace: string): string {
|
||||
let cached = namespace;
|
||||
|
||||
while (this.cachedNamespacedLevels[namespace] == undefined) {
|
||||
const sep = cached.lastIndexOf(NAMESPACE_SEPARATOR);
|
||||
|
||||
if (sep === -1) {
|
||||
return this.cachedNamespacedLevels[namespace] = this.level;
|
||||
return (this.cachedNamespacedLevels[namespace] = this.level);
|
||||
}
|
||||
|
||||
cached = cached.slice(0, sep);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,11 @@
|
||||
import data from './data';
|
||||
import utils from './utils';
|
||||
import Ajv, {ValidateFunction} from 'ajv';
|
||||
import objectAssignDeep from 'object-assign-deep';
|
||||
import path from 'path';
|
||||
import yaml from './yaml';
|
||||
import Ajv, {ValidateFunction} from 'ajv';
|
||||
|
||||
import data from './data';
|
||||
import schemaJson from './settings.schema.json';
|
||||
import utils from './utils';
|
||||
import yaml from './yaml';
|
||||
export let schema = schemaJson;
|
||||
// @ts-ignore
|
||||
schema = {};
|
||||
@ -28,18 +29,19 @@ objectAssignDeep(schema, schemaJson);
|
||||
|
||||
/** NOTE: by order of priority, lower index is lower level (more important) */
|
||||
export const LOG_LEVELS: readonly string[] = ['error', 'warning', 'info', 'debug'] as const;
|
||||
export type LogLevel = typeof LOG_LEVELS[number];
|
||||
export type LogLevel = (typeof LOG_LEVELS)[number];
|
||||
|
||||
// DEPRECATED ZIGBEE2MQTT_CONFIG: https://github.com/Koenkk/zigbee2mqtt/issues/4697
|
||||
const file = process.env.ZIGBEE2MQTT_CONFIG ?? data.joinPath('configuration.yaml');
|
||||
const NULLABLE_SETTINGS = ['homeassistant'];
|
||||
const ajvSetting = new Ajv({allErrors: true}).addKeyword('requiresRestart').compile(schemaJson);
|
||||
const ajvRestartRequired = new Ajv({allErrors: true})
|
||||
.addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson);
|
||||
const ajvRestartRequired = new Ajv({allErrors: true}).addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson);
|
||||
const ajvRestartRequiredDeviceOptions = new Ajv({allErrors: true})
|
||||
.addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson.definitions.device);
|
||||
.addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s})
|
||||
.compile(schemaJson.definitions.device);
|
||||
const ajvRestartRequiredGroupOptions = new Ajv({allErrors: true})
|
||||
.addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson.definitions.group);
|
||||
.addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s})
|
||||
.compile(schemaJson.definitions.group);
|
||||
const defaults: RecursivePartial<Settings> = {
|
||||
permit_join: false,
|
||||
external_converters: [],
|
||||
@ -92,7 +94,7 @@ const defaults: RecursivePartial<Settings> = {
|
||||
log_debug_to_mqtt_frontend: false,
|
||||
log_debug_namespace_ignore: '',
|
||||
pan_id: 0x1a62,
|
||||
ext_pan_id: [0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD],
|
||||
ext_pan_id: [0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd],
|
||||
channel: 11,
|
||||
adapter_concurrent: null,
|
||||
adapter_delay: null,
|
||||
@ -129,12 +131,15 @@ function loadSettingsWithDefaults(): void {
|
||||
}
|
||||
|
||||
if (_settingsWithDefaults.homeassistant) {
|
||||
const defaults = {discovery_topic: 'homeassistant', status_topic: 'hass/status',
|
||||
legacy_entity_attributes: true, legacy_triggers: true};
|
||||
const defaults = {discovery_topic: 'homeassistant', status_topic: 'hass/status', legacy_entity_attributes: true, legacy_triggers: true};
|
||||
const sLegacy = {};
|
||||
if (_settingsWithDefaults.advanced) {
|
||||
for (const key of ['homeassistant_legacy_triggers', 'homeassistant_discovery_topic',
|
||||
'homeassistant_legacy_entity_attributes', 'homeassistant_status_topic']) {
|
||||
for (const key of [
|
||||
'homeassistant_legacy_triggers',
|
||||
'homeassistant_discovery_topic',
|
||||
'homeassistant_legacy_entity_attributes',
|
||||
'homeassistant_status_topic',
|
||||
]) {
|
||||
// @ts-ignore
|
||||
if (_settingsWithDefaults.advanced[key] !== undefined) {
|
||||
// @ts-ignore
|
||||
@ -202,7 +207,7 @@ function loadSettingsWithDefaults(): void {
|
||||
_settingsWithDefaults.whitelist && _settingsWithDefaults.passlist.push(..._settingsWithDefaults.whitelist);
|
||||
}
|
||||
|
||||
function parseValueRef(text: string): {filename: string, key: string} | null {
|
||||
function parseValueRef(text: string): {filename: string; key: string} | null {
|
||||
const match = /!(.*) (.*)/g.exec(text);
|
||||
if (match) {
|
||||
let filename = match[1];
|
||||
@ -248,7 +253,8 @@ function write(): void {
|
||||
|
||||
// If an array, only write to first file and only devices which are not in the other files.
|
||||
if (Array.isArray(actual[type])) {
|
||||
actual[type].filter((f: string, i: number) => i !== 0)
|
||||
actual[type]
|
||||
.filter((f: string, i: number) => i !== 0)
|
||||
.map((f: string) => yaml.readIfExists(data.joinPath(f), {}))
|
||||
.map((c: KeyValue) => Object.keys(c))
|
||||
// @ts-ignore
|
||||
@ -274,10 +280,7 @@ export function validate(): string[] {
|
||||
getInternalSettings();
|
||||
} catch (error) {
|
||||
if (error.name === 'YAMLException') {
|
||||
return [
|
||||
`Your YAML file: '${error.file}' is invalid ` +
|
||||
`(use https://jsonformatter.org/yaml-validator to find and fix the issue)`,
|
||||
];
|
||||
return [`Your YAML file: '${error.file}' is invalid ` + `(use https://jsonformatter.org/yaml-validator to find and fix the issue)`];
|
||||
}
|
||||
|
||||
return [error.message];
|
||||
@ -288,18 +291,30 @@ export function validate(): string[] {
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
if (_settings.advanced && _settings.advanced.network_key && typeof _settings.advanced.network_key === 'string' &&
|
||||
_settings.advanced.network_key !== 'GENERATE') {
|
||||
if (
|
||||
_settings.advanced &&
|
||||
_settings.advanced.network_key &&
|
||||
typeof _settings.advanced.network_key === 'string' &&
|
||||
_settings.advanced.network_key !== 'GENERATE'
|
||||
) {
|
||||
errors.push(`advanced.network_key: should be array or 'GENERATE' (is '${_settings.advanced.network_key}')`);
|
||||
}
|
||||
|
||||
if (_settings.advanced && _settings.advanced.pan_id && typeof _settings.advanced.pan_id === 'string' &&
|
||||
_settings.advanced.pan_id !== 'GENERATE') {
|
||||
if (
|
||||
_settings.advanced &&
|
||||
_settings.advanced.pan_id &&
|
||||
typeof _settings.advanced.pan_id === 'string' &&
|
||||
_settings.advanced.pan_id !== 'GENERATE'
|
||||
) {
|
||||
errors.push(`advanced.pan_id: should be number or 'GENERATE' (is '${_settings.advanced.pan_id}')`);
|
||||
}
|
||||
|
||||
if (_settings.advanced && _settings.advanced.ext_pan_id && typeof _settings.advanced.ext_pan_id === 'string' &&
|
||||
_settings.advanced.ext_pan_id !== 'GENERATE') {
|
||||
if (
|
||||
_settings.advanced &&
|
||||
_settings.advanced.ext_pan_id &&
|
||||
typeof _settings.advanced.ext_pan_id === 'string' &&
|
||||
_settings.advanced.ext_pan_id !== 'GENERATE'
|
||||
) {
|
||||
errors.push(`advanced.ext_pan_id: should be array or 'GENERATE' (is '${_settings.advanced.ext_pan_id}')`);
|
||||
}
|
||||
|
||||
@ -404,7 +419,7 @@ function applyEnvironmentVariables(settings: Partial<Settings>): void {
|
||||
if (key !== 'properties' && obj[key]) {
|
||||
const type = (obj[key].type || 'object').toString();
|
||||
const envPart = path.reduce((acc, val) => `${acc}${val}_`, '');
|
||||
const envVariableName = (`ZIGBEE2MQTT_CONFIG_${envPart}${key}`).toUpperCase();
|
||||
const envVariableName = `ZIGBEE2MQTT_CONFIG_${envPart}${key}`.toUpperCase();
|
||||
if (process.env[envVariableName]) {
|
||||
const setting = path.reduce((acc, val) => {
|
||||
/* eslint-disable-line */ // @ts-ignore
|
||||
@ -498,8 +513,7 @@ export function apply(settings: Record<string, unknown>): boolean {
|
||||
write();
|
||||
|
||||
ajvRestartRequired(settings);
|
||||
const restartRequired = ajvRestartRequired.errors &&
|
||||
!!ajvRestartRequired.errors.find((e) => e.keyword === 'requiresRestart');
|
||||
const restartRequired = ajvRestartRequired.errors && !!ajvRestartRequired.errors.find((e) => e.keyword === 'requiresRestart');
|
||||
return restartRequired;
|
||||
}
|
||||
|
||||
@ -607,8 +621,7 @@ export function removeDevice(IDorName: string): void {
|
||||
|
||||
// Remove device from groups
|
||||
if (settings.groups) {
|
||||
const regex =
|
||||
new RegExp(`^(${device.friendly_name}|${device.ID})(/[^/]+)?$`);
|
||||
const regex = new RegExp(`^(${device.friendly_name}|${device.ID})(/[^/]+)?$`);
|
||||
for (const group of Object.values(settings.groups).filter((g) => g.devices)) {
|
||||
group.devices = group.devices.filter((device) => !device.match(regex));
|
||||
}
|
||||
@ -701,7 +714,7 @@ export function changeEntityOptions(IDorName: string, newOptions: KeyValue): boo
|
||||
validator = ajvRestartRequiredDeviceOptions;
|
||||
} else if (getGroup(IDorName)) {
|
||||
objectAssignDeep(settings.groups[getGroup(IDorName).ID], newOptions);
|
||||
utils.removeNullPropertiesFromObject(settings.groups[getGroup(IDorName).ID], NULLABLE_SETTINGS );
|
||||
utils.removeNullPropertiesFromObject(settings.groups[getGroup(IDorName).ID], NULLABLE_SETTINGS);
|
||||
validator = ajvRestartRequiredGroupOptions;
|
||||
} else {
|
||||
throw new Error(`Device or group '${IDorName}' does not exist`);
|
||||
|
@ -1,11 +1,13 @@
|
||||
import equals from 'fast-deep-equal/es6';
|
||||
import humanizeDuration from 'humanize-duration';
|
||||
import data from './data';
|
||||
import vm from 'vm';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type * as zhc from 'zigbee-herdsman-converters';
|
||||
|
||||
import equals from 'fast-deep-equal/es6';
|
||||
import fs from 'fs';
|
||||
import humanizeDuration from 'humanize-duration';
|
||||
import path from 'path';
|
||||
import vm from 'vm';
|
||||
|
||||
import data from './data';
|
||||
|
||||
// construct a local ISO8601 string (instead of UTC-based)
|
||||
// Example:
|
||||
// - ISO8601 (UTC) = 2019-03-01T15:32:45.941+0000
|
||||
@ -18,21 +20,30 @@ function toLocalISOString(date: Date): string {
|
||||
return (norm < 10 ? '0' : '') + norm;
|
||||
};
|
||||
|
||||
return date.getFullYear() +
|
||||
'-' + pad(date.getMonth() + 1) +
|
||||
'-' + pad(date.getDate()) +
|
||||
'T' + pad(date.getHours()) +
|
||||
':' + pad(date.getMinutes()) +
|
||||
':' + pad(date.getSeconds()) +
|
||||
plusOrMinus + pad(tzOffset / 60) +
|
||||
':' + pad(tzOffset % 60);
|
||||
return (
|
||||
date.getFullYear() +
|
||||
'-' +
|
||||
pad(date.getMonth() + 1) +
|
||||
'-' +
|
||||
pad(date.getDate()) +
|
||||
'T' +
|
||||
pad(date.getHours()) +
|
||||
':' +
|
||||
pad(date.getMinutes()) +
|
||||
':' +
|
||||
pad(date.getSeconds()) +
|
||||
plusOrMinus +
|
||||
pad(tzOffset / 60) +
|
||||
':' +
|
||||
pad(tzOffset % 60)
|
||||
);
|
||||
}
|
||||
|
||||
function capitalize(s: string): string {
|
||||
return s[0].toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
async function getZigbee2MQTTVersion(includeCommitHash=true): Promise<{commitHash: string, version: string}> {
|
||||
async function getZigbee2MQTTVersion(includeCommitHash = true): Promise<{commitHash: string; version: string}> {
|
||||
const git = await import('git-last-commit');
|
||||
const packageJSON = await import('../..' + '/package.json');
|
||||
|
||||
@ -75,7 +86,8 @@ function formatDate(time: number, type: 'ISO_8601' | 'ISO_8601_local' | 'epoch'
|
||||
if (type === 'ISO_8601') return new Date(time).toISOString();
|
||||
else if (type === 'ISO_8601_local') return toLocalISOString(new Date(time));
|
||||
else if (type === 'epoch') return time;
|
||||
else { // relative
|
||||
else {
|
||||
// relative
|
||||
return humanizeDuration(Date.now() - time, {language: 'en', largest: 2, round: true}) + ' ago';
|
||||
}
|
||||
}
|
||||
@ -168,7 +180,7 @@ export function* loadExternalConverter(moduleName: string): Generator<ExternalDe
|
||||
* @param {KeyValue} obj Object to process (in-place)
|
||||
* @param {string[]} [ignoreKeys] Recursively ignore these keys in the object (keep null/undefined values).
|
||||
*/
|
||||
function removeNullPropertiesFromObject(obj: KeyValue, ignoreKeys: string[] = [] ): void {
|
||||
function removeNullPropertiesFromObject(obj: KeyValue, ignoreKeys: string[] = []): void {
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (ignoreKeys.includes(key)) continue;
|
||||
const value = obj[key];
|
||||
@ -198,28 +210,27 @@ function toSnakeCase(value: string | KeyValue): any {
|
||||
}
|
||||
return value;
|
||||
} else {
|
||||
return value.replace(/\.?([A-Z])/g, (x, y) => '_' + y.toLowerCase()).replace(/^_/, '').replace('_i_d', '_id');
|
||||
return value
|
||||
.replace(/\.?([A-Z])/g, (x, y) => '_' + y.toLowerCase())
|
||||
.replace(/^_/, '')
|
||||
.replace('_i_d', '_id');
|
||||
}
|
||||
}
|
||||
|
||||
function charRange(start: string, stop: string): number[] {
|
||||
const result = [];
|
||||
for (let idx=start.charCodeAt(0), end=stop.charCodeAt(0); idx <=end; ++idx) {
|
||||
for (let idx = start.charCodeAt(0), end = stop.charCodeAt(0); idx <= end; ++idx) {
|
||||
result.push(idx);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const controlCharacters = [
|
||||
...charRange('\u0000', '\u001F'),
|
||||
...charRange('\u007f', '\u009F'),
|
||||
...charRange('\ufdd0', '\ufdef'),
|
||||
];
|
||||
const controlCharacters = [...charRange('\u0000', '\u001F'), ...charRange('\u007f', '\u009F'), ...charRange('\ufdd0', '\ufdef')];
|
||||
|
||||
function containsControlCharacter(str: string): boolean {
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const ch = str.charCodeAt(i);
|
||||
if (controlCharacters.includes(ch) || [0xFFFE, 0xFFFF].includes(ch & 0xFFFF)) {
|
||||
if (controlCharacters.includes(ch) || [0xfffe, 0xffff].includes(ch & 0xffff)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -239,7 +250,7 @@ function getAllFiles(path_: string): string[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateFriendlyName(name: string, throwFirstError=false): string[] {
|
||||
function validateFriendlyName(name: string, throwFirstError = false): string[] {
|
||||
const errors = [];
|
||||
|
||||
if (name.length === 0) errors.push(`friendly_name must be at least 1 char long`);
|
||||
@ -264,7 +275,7 @@ function sleep(seconds: number): Promise<void> {
|
||||
function sanitizeImageParameter(parameter: string): string {
|
||||
const replaceByDash = [/\?/g, /&/g, /[^a-z\d\- _./:]/gi];
|
||||
let sanitized = parameter;
|
||||
replaceByDash.forEach((r) => sanitized = sanitized.replace(r, '-'));
|
||||
replaceByDash.forEach((r) => (sanitized = sanitized.replace(r, '-')));
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
@ -325,8 +336,12 @@ const hours = (hours: number): number => 1000 * 60 * 60 * hours;
|
||||
const minutes = (minutes: number): number => 1000 * 60 * minutes;
|
||||
const seconds = (seconds: number): number => 1000 * seconds;
|
||||
|
||||
async function publishLastSeen(data: eventdata.LastSeenChanged, settings: Settings, allowMessageEmitted: boolean,
|
||||
publishEntityState: PublishEntityState): Promise<void> {
|
||||
async function publishLastSeen(
|
||||
data: eventdata.LastSeenChanged,
|
||||
settings: Settings,
|
||||
allowMessageEmitted: boolean,
|
||||
publishEntityState: PublishEntityState,
|
||||
): Promise<void> {
|
||||
/**
|
||||
* Prevent 2 MQTT publishes when 1 message event is received;
|
||||
* - In case reason == messageEmitted, receive.ts will only call this when it did not publish a
|
||||
@ -382,10 +397,34 @@ function getScenes(entity: zh.Endpoint | zh.Group): Scene[] {
|
||||
}
|
||||
|
||||
export default {
|
||||
capitalize, getZigbee2MQTTVersion, getDependencyVersion, formatDate, objectHasProperties,
|
||||
equalsPartial, getObjectProperty, getResponse, parseJSON, loadModuleFromText, loadModuleFromFile,
|
||||
removeNullPropertiesFromObject, toNetworkAddressHex, toSnakeCase,
|
||||
isEndpoint, isZHGroup, hours, minutes, seconds, validateFriendlyName, sleep,
|
||||
sanitizeImageParameter, isAvailabilityEnabledForEntity, publishLastSeen, availabilityPayload,
|
||||
getAllFiles, filterProperties, flatten, arrayUnique, getScenes,
|
||||
capitalize,
|
||||
getZigbee2MQTTVersion,
|
||||
getDependencyVersion,
|
||||
formatDate,
|
||||
objectHasProperties,
|
||||
equalsPartial,
|
||||
getObjectProperty,
|
||||
getResponse,
|
||||
parseJSON,
|
||||
loadModuleFromText,
|
||||
loadModuleFromFile,
|
||||
removeNullPropertiesFromObject,
|
||||
toNetworkAddressHex,
|
||||
toSnakeCase,
|
||||
isEndpoint,
|
||||
isZHGroup,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
validateFriendlyName,
|
||||
sleep,
|
||||
sanitizeImageParameter,
|
||||
isAvailabilityEnabledForEntity,
|
||||
publishLastSeen,
|
||||
availabilityPayload,
|
||||
getAllFiles,
|
||||
filterProperties,
|
||||
flatten,
|
||||
arrayUnique,
|
||||
getScenes,
|
||||
};
|
||||
|
@ -1,11 +1,11 @@
|
||||
import yaml from 'js-yaml';
|
||||
import fs from 'fs';
|
||||
import equals from 'fast-deep-equal/es6';
|
||||
import fs from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
function read(file: string): KeyValue {
|
||||
try {
|
||||
const result = yaml.load(fs.readFileSync(file, 'utf8'));
|
||||
return result as KeyValue ?? {};
|
||||
return (result as KeyValue) ?? {};
|
||||
} catch (error) {
|
||||
if (error.name === 'YAMLException') {
|
||||
error.file = file;
|
||||
|
@ -1,14 +1,15 @@
|
||||
import {Controller} from 'zigbee-herdsman';
|
||||
import logger from './util/logger';
|
||||
import * as settings from './util/settings';
|
||||
import data from './util/data';
|
||||
import utils from './util/utils';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import Device from './model/device';
|
||||
import Group from './model/group';
|
||||
import * as ZHEvents from 'zigbee-herdsman/dist/controller/events';
|
||||
import bind from 'bind-decorator';
|
||||
import {randomInt} from 'crypto';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import {Controller} from 'zigbee-herdsman';
|
||||
import * as ZHEvents from 'zigbee-herdsman/dist/controller/events';
|
||||
|
||||
import Device from './model/device';
|
||||
import Group from './model/group';
|
||||
import data from './util/data';
|
||||
import logger from './util/logger';
|
||||
import * as settings from './util/settings';
|
||||
import utils from './util/utils';
|
||||
|
||||
const entityIDRegex = new RegExp(`^(.+?)(?:/([^/]+))?$`);
|
||||
|
||||
@ -27,13 +28,14 @@ export default class Zigbee {
|
||||
logger.info(`Starting zigbee-herdsman (${infoHerdsman.version})`);
|
||||
const herdsmanSettings = {
|
||||
network: {
|
||||
panID: settings.get().advanced.pan_id === 'GENERATE' ?
|
||||
this.generatePanID() : settings.get().advanced.pan_id as number,
|
||||
extendedPanID: settings.get().advanced.ext_pan_id === 'GENERATE' ?
|
||||
this.generateExtPanID() : settings.get().advanced.ext_pan_id as number[],
|
||||
panID: settings.get().advanced.pan_id === 'GENERATE' ? this.generatePanID() : (settings.get().advanced.pan_id as number),
|
||||
extendedPanID:
|
||||
settings.get().advanced.ext_pan_id === 'GENERATE' ? this.generateExtPanID() : (settings.get().advanced.ext_pan_id as number[]),
|
||||
channelList: [settings.get().advanced.channel],
|
||||
networkKey: settings.get().advanced.network_key === 'GENERATE' ?
|
||||
this.generateNetworkKey() : settings.get().advanced.network_key as number[],
|
||||
networkKey:
|
||||
settings.get().advanced.network_key === 'GENERATE'
|
||||
? this.generateNetworkKey()
|
||||
: (settings.get().advanced.network_key as number[]),
|
||||
},
|
||||
databasePath: data.joinPath('database.db'),
|
||||
databaseBackupPath: data.joinPath('database.db.backup'),
|
||||
@ -108,10 +110,12 @@ export default class Zigbee {
|
||||
this.herdsman.on('message', async (data: ZHEvents.MessagePayload) => {
|
||||
const device = this.resolveDevice(data.device.ieeeAddr);
|
||||
await device.resolveDefinition();
|
||||
logger.debug(`Received Zigbee message from '${device.name}', type '${data.type}', ` +
|
||||
`cluster '${data.cluster}', data '${stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
|
||||
(data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``) +
|
||||
(device.zh.type === 'Coordinator' ? `, ignoring since it is from coordinator` : ``));
|
||||
logger.debug(
|
||||
`Received Zigbee message from '${device.name}', type '${data.type}', ` +
|
||||
`cluster '${data.cluster}', data '${stringify(data.data)}' from endpoint ${data.endpoint.ID}` +
|
||||
(data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``) +
|
||||
(device.zh.type === 'Coordinator' ? `, ignoring since it is from coordinator` : ``),
|
||||
);
|
||||
if (device.zh.type === 'Coordinator') return;
|
||||
this.eventBus.emitDeviceMessage({...data, device});
|
||||
});
|
||||
@ -161,14 +165,16 @@ export default class Zigbee {
|
||||
const {vendor, description, model} = data.device.definition;
|
||||
logger.info(`Device '${name}' is supported, identified as: ${vendor} ${description} (${model})`);
|
||||
} else {
|
||||
logger.warning(`Device '${name}' with Zigbee model '${data.device.zh.modelID}' and manufacturer name ` +
|
||||
`'${data.device.zh.manufacturerName}' is NOT supported, ` +
|
||||
// eslint-disable-next-line max-len
|
||||
`please follow https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`);
|
||||
logger.warning(
|
||||
`Device '${name}' with Zigbee model '${data.device.zh.modelID}' and manufacturer name ` +
|
||||
`'${data.device.zh.manufacturerName}' is NOT supported, ` +
|
||||
`please follow https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`,
|
||||
);
|
||||
}
|
||||
} else if (data.status === 'failed') {
|
||||
logger.error(`Failed to interview '${name}', device has not successfully been paired`);
|
||||
} else { // data.status === 'started'
|
||||
} else {
|
||||
// data.status === 'started'
|
||||
logger.info(`Starting interview of '${name}'`);
|
||||
}
|
||||
}
|
||||
@ -186,7 +192,7 @@ export default class Zigbee {
|
||||
}
|
||||
|
||||
private generatePanID(): number {
|
||||
const panID = randomInt(1, 0xFFFF - 1);
|
||||
const panID = randomInt(1, 0xffff - 1);
|
||||
settings.set(['advanced', 'pan_id'], panID);
|
||||
return panID;
|
||||
}
|
||||
@ -230,7 +236,7 @@ export default class Zigbee {
|
||||
return this.herdsman.getPermitJoinTimeout();
|
||||
}
|
||||
|
||||
async permitJoin(permit: boolean, device?: Device, time: number=undefined): Promise<void> {
|
||||
async permitJoin(permit: boolean, device?: Device, time: number = undefined): Promise<void> {
|
||||
if (permit) {
|
||||
logger.info(`Zigbee: allowing new devices to join${device ? ` via ${device.name}` : ''}.`);
|
||||
} else {
|
||||
@ -284,8 +290,7 @@ export default class Zigbee {
|
||||
}
|
||||
}
|
||||
|
||||
resolveEntityAndEndpoint(ID: string)
|
||||
: {ID: string, entity: Device | Group, endpointID: string, endpoint: zh.Endpoint} {
|
||||
resolveEntityAndEndpoint(ID: string): {ID: string; entity: Device | Group; endpointID: string; endpoint: zh.Endpoint} {
|
||||
// This function matches the following entity formats:
|
||||
// device_name (just device name)
|
||||
// device_name/ep_name (device name and endpoint numeric ID or name)
|
||||
@ -324,8 +329,9 @@ export default class Zigbee {
|
||||
return this.herdsman.getGroups().map((g) => this.resolveGroup(g.groupID));
|
||||
}
|
||||
|
||||
devices(includeCoordinator=true): Device[] {
|
||||
return this.herdsman.getDevices()
|
||||
devices(includeCoordinator = true): Device[] {
|
||||
return this.herdsman
|
||||
.getDevices()
|
||||
.map((d) => this.resolveDevice(d.ieeeAddr))
|
||||
.filter((d) => includeCoordinator || d.zh.type !== 'Coordinator');
|
||||
}
|
||||
@ -371,7 +377,7 @@ export default class Zigbee {
|
||||
await this.herdsman.touchlinkIdentify(ieeeAddr, channel);
|
||||
}
|
||||
|
||||
async touchlinkScan(): Promise<{ieeeAddr: string, channel: number}[]> {
|
||||
async touchlinkScan(): Promise<{ieeeAddr: string; channel: number}[]> {
|
||||
return this.herdsman.touchlinkScan();
|
||||
}
|
||||
|
||||
|
71
package-lock.json
generated
71
package-lock.json
generated
@ -56,9 +56,11 @@
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
"babel-jest": "^29.7.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jest": "^28.6.0",
|
||||
"eslint-plugin-perfectionist": "^2.11.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.2",
|
||||
"tmp": "^0.2.3",
|
||||
"typescript": "^5.5.2"
|
||||
},
|
||||
@ -4671,16 +4673,16 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-google": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz",
|
||||
"integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==",
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
|
||||
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=5.16.0"
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jest": {
|
||||
@ -4708,6 +4710,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-perfectionist": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-2.11.0.tgz",
|
||||
"integrity": "sha512-XrtBtiu5rbQv88gl+1e2RQud9te9luYNvKIgM9emttQ2zutHPzY/AQUucwxscDKV4qlTkvLTxjOFvxqeDpPorw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^6.13.0 || ^7.0.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"natural-compare-lite": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"astro-eslint-parser": "^1.0.2",
|
||||
"eslint": ">=8.0.0",
|
||||
"svelte": ">=3.0.0",
|
||||
"svelte-eslint-parser": "^0.37.0",
|
||||
"vue-eslint-parser": ">=9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"astro-eslint-parser": {
|
||||
"optional": true
|
||||
},
|
||||
"svelte": {
|
||||
"optional": true
|
||||
},
|
||||
"svelte-eslint-parser": {
|
||||
"optional": true
|
||||
},
|
||||
"vue-eslint-parser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||
@ -7939,6 +7973,12 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/natural-compare-lite": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
|
||||
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz",
|
||||
@ -8283,6 +8323,21 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
|
||||
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||
|
@ -23,6 +23,8 @@
|
||||
"build": "tsc && node index.js writehash",
|
||||
"build-watch": "tsc --watch",
|
||||
"eslint": "eslint lib/ --max-warnings=0",
|
||||
"pretty:write": "prettier --write lib test",
|
||||
"pretty:check": "prettier --check lib test",
|
||||
"start": "node index.js",
|
||||
"test-with-coverage": "jest test --silent --maxWorkers=50% --coverage",
|
||||
"test": "jest test --silent --maxWorkers=50%",
|
||||
@ -79,9 +81,11 @@
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
"babel-jest": "^29.7.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jest": "^28.6.0",
|
||||
"eslint-plugin-perfectionist": "^2.11.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.2",
|
||||
"tmp": "^0.2.3",
|
||||
"typescript": "^5.5.2"
|
||||
},
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint max-len: 0 */
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const process = require('process');
|
||||
|
@ -1,11 +1,11 @@
|
||||
class Example {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
this.mqtt = mqtt;
|
||||
this.mqtt.publish('example/extension', 'call from constructor')
|
||||
this.mqtt.publish('example/extension', 'call from constructor');
|
||||
}
|
||||
|
||||
start() {
|
||||
this.mqtt.publish('example/extension', 'test')
|
||||
this.mqtt.publish('example/extension', 'test');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,25 +9,28 @@ const homeassistantSwitch = {
|
||||
},
|
||||
};
|
||||
|
||||
const mockDevices = [{
|
||||
mock: 1,
|
||||
model: 'external_converters_device_1',
|
||||
homeassistant: [homeassistantSwitch],
|
||||
zigbeeModel: ['external_converter_device_1'],
|
||||
vendor: 'external_1',
|
||||
description: 'external_1',
|
||||
fromZigbee: [],
|
||||
toZigbee: [],
|
||||
exposes: [],
|
||||
}, {
|
||||
mock: 2,
|
||||
model: 'external_converters_device_2',
|
||||
zigbeeModel: ['external_converter_device_2'],
|
||||
vendor: 'external_2',
|
||||
description: 'external_2',
|
||||
fromZigbee: [],
|
||||
toZigbee: [],
|
||||
exposes: [],
|
||||
}];
|
||||
const mockDevices = [
|
||||
{
|
||||
mock: 1,
|
||||
model: 'external_converters_device_1',
|
||||
homeassistant: [homeassistantSwitch],
|
||||
zigbeeModel: ['external_converter_device_1'],
|
||||
vendor: 'external_1',
|
||||
description: 'external_1',
|
||||
fromZigbee: [],
|
||||
toZigbee: [],
|
||||
exposes: [],
|
||||
},
|
||||
{
|
||||
mock: 2,
|
||||
model: 'external_converters_device_2',
|
||||
zigbeeModel: ['external_converter_device_2'],
|
||||
vendor: 'external_2',
|
||||
description: 'external_2',
|
||||
fromZigbee: [],
|
||||
toZigbee: [],
|
||||
exposes: [],
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = mockDevices;
|
||||
module.exports = mockDevices;
|
||||
|
@ -9,4 +9,4 @@ const mockDevice = {
|
||||
exposes: [],
|
||||
};
|
||||
|
||||
module.exports = mockDevice;
|
||||
module.exports = mockDevice;
|
||||
|
@ -13,7 +13,8 @@ import stringify from 'json-stable-stringify-without-jsonify';
|
||||
const mocks = [MQTT.publish, logger.warning, logger.info];
|
||||
const devices = zigbeeHerdsman.devices;
|
||||
zigbeeHerdsman.returnDevices.push(
|
||||
...[devices.bulb_color.ieeeAddr, devices.bulb_color_2.ieeeAddr, devices.coordinator.ieeeAddr, devices.remote.ieeeAddr])
|
||||
...[devices.bulb_color.ieeeAddr, devices.bulb_color_2.ieeeAddr, devices.coordinator.ieeeAddr, devices.remote.ieeeAddr],
|
||||
);
|
||||
|
||||
describe('Availability', () => {
|
||||
let controller;
|
||||
@ -21,12 +22,12 @@ describe('Availability', () => {
|
||||
let resetExtension = async () => {
|
||||
await controller.enableDisableExtension(false, 'Availability');
|
||||
await controller.enableDisableExtension(true, 'Availability');
|
||||
}
|
||||
};
|
||||
|
||||
const setTimeAndAdvanceTimers = async (value) => {
|
||||
jest.setSystemTime(Date.now() + value);
|
||||
await jest.advanceTimersByTimeAsync(value);
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(utils, 'sleep').mockImplementation(async (seconds) => {});
|
||||
@ -44,24 +45,28 @@ describe('Availability', () => {
|
||||
settings.reRead();
|
||||
settings.set(['availability'], true);
|
||||
settings.set(['devices', devices.bulb_color_2.ieeeAddr, 'availability'], false);
|
||||
Object.values(devices).forEach(d => d.lastSeen = utils.minutes(1));
|
||||
Object.values(devices).forEach((d) => (d.lastSeen = utils.minutes(1)));
|
||||
mocks.forEach((m) => m.mockClear());
|
||||
await resetExtension();
|
||||
Object.values(devices).forEach((d) => d.ping.mockClear());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
})
|
||||
afterEach(async () => {});
|
||||
|
||||
afterAll(async () => {
|
||||
await controller.stop();
|
||||
jest.useRealTimers();
|
||||
})
|
||||
});
|
||||
|
||||
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).not.toHaveBeenCalledWith('zigbee2mqtt/bulb_color_2/availability', 'online', {retain: true, qos: 1}, expect.any(Function));
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color_2/availability',
|
||||
'online',
|
||||
{retain: true, qos: 1},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should ping on startup for enabled and unavailable devices', async () => {
|
||||
@ -71,8 +76,8 @@ describe('Availability', () => {
|
||||
await resetExtension();
|
||||
|
||||
await setTimeAndAdvanceTimers(utils.minutes(1));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);// enabled/unavailable
|
||||
expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(0);// enabled/available
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1); // enabled/unavailable
|
||||
expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(0); // enabled/available
|
||||
});
|
||||
|
||||
it('Should not ping on startup for available or disabled devices', async () => {
|
||||
@ -83,8 +88,8 @@ describe('Availability', () => {
|
||||
await resetExtension();
|
||||
|
||||
await setTimeAndAdvanceTimers(utils.minutes(1));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);// enabled/available
|
||||
expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(0);// disabled/unavailable
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); // enabled/available
|
||||
expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(0); // disabled/unavailable
|
||||
});
|
||||
|
||||
it('Should publish offline for active device when not seen for 10 minutes', async () => {
|
||||
@ -134,7 +139,9 @@ describe('Availability', () => {
|
||||
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color});
|
||||
|
||||
devices.bulb_color.ping.mockImplementationOnce(() => {throw new Error('failed')});
|
||||
devices.bulb_color.ping.mockImplementationOnce(() => {
|
||||
throw new Error('failed');
|
||||
});
|
||||
|
||||
await setTimeAndAdvanceTimers(utils.minutes(15));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(2);
|
||||
@ -218,7 +225,7 @@ describe('Availability', () => {
|
||||
await setTimeAndAdvanceTimers(utils.minutes(9));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: "bulb_color"}));
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb_color'}));
|
||||
await flushPromises();
|
||||
|
||||
await setTimeAndAdvanceTimers(utils.minutes(3));
|
||||
@ -249,7 +256,7 @@ describe('Availability', () => {
|
||||
//@ts-expect-error private
|
||||
const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr);
|
||||
//@ts-expect-error private
|
||||
controller.state.set(device, {state: 'OFF'})
|
||||
controller.state.set(device, {state: 'OFF'});
|
||||
|
||||
const endpoint = devices.bulb_color.getEndpoint(1);
|
||||
endpoint.read.mockClear();
|
||||
@ -269,7 +276,9 @@ describe('Availability', () => {
|
||||
endpoint.read.mockClear();
|
||||
await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color});
|
||||
await flushPromises();
|
||||
endpoint.read.mockImplementationOnce(() => {throw new Error('')});
|
||||
endpoint.read.mockImplementationOnce(() => {
|
||||
throw new Error('');
|
||||
});
|
||||
await setTimeAndAdvanceTimers(utils.seconds(3));
|
||||
expect(endpoint.read).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@ -293,7 +302,12 @@ describe('Availability', () => {
|
||||
MQTT.publish.mockClear();
|
||||
await setTimeAndAdvanceTimers(utils.hours(26));
|
||||
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: '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('Deprecated - should allow to block via advanced.availability_blocklist', async () => {
|
||||
@ -332,15 +346,30 @@ describe('Availability', () => {
|
||||
await resetExtension();
|
||||
devices.bulb_color_2.ping.mockClear();
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', 'online', {retain: true, qos: 1}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/group_tradfri_remote/availability',
|
||||
'online',
|
||||
{retain: true, qos: 1},
|
||||
expect.any(Function),
|
||||
);
|
||||
MQTT.publish.mockClear();
|
||||
await setTimeAndAdvanceTimers(utils.minutes(12));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', 'offline', {retain: true, qos: 1}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/group_tradfri_remote/availability',
|
||||
'offline',
|
||||
{retain: true, qos: 1},
|
||||
expect.any(Function),
|
||||
);
|
||||
MQTT.publish.mockClear();
|
||||
devices.bulb_color_2.lastSeen = Date.now();
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color_2});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', 'online', {retain: true, qos: 1}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/group_tradfri_remote/availability',
|
||||
'online',
|
||||
{retain: true, qos: 1},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should clear the ping queue on stop', async () => {
|
||||
@ -358,7 +387,7 @@ describe('Availability', () => {
|
||||
expect(availability.pingQueue).toEqual([]);
|
||||
// Validate the stop-interrupt implicitly by checking that it prevents further function invocations
|
||||
expect(publishAvailabilitySpy).not.toHaveBeenCalled();
|
||||
devices.bulb_color.ping = jest.fn();// ensure reset
|
||||
devices.bulb_color.ping = jest.fn(); // ensure reset
|
||||
});
|
||||
|
||||
it('Should prevent instance restart', async () => {
|
||||
|
@ -6,7 +6,7 @@ const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
const flushPromises = require('./lib/flushPromises');
|
||||
const stringify = require('json-stable-stringify-without-jsonify');
|
||||
jest.mock('debounce', () => jest.fn(fn => fn));
|
||||
jest.mock('debounce', () => jest.fn((fn) => fn));
|
||||
const debounce = require('debounce');
|
||||
|
||||
describe('Bind', () => {
|
||||
@ -21,12 +21,12 @@ describe('Bind', () => {
|
||||
endpoint.bind.mockClear();
|
||||
endpoint.unbind.mockClear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let resetExtension = async () => {
|
||||
await controller.enableDisableExtension(false, 'Bind');
|
||||
await controller.enableDisableExtension(true, 'Bind');
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers();
|
||||
@ -49,7 +49,7 @@ describe('Bind', () => {
|
||||
|
||||
afterAll(async () => {
|
||||
jest.useRealTimers();
|
||||
})
|
||||
});
|
||||
|
||||
it('Should bind to device and configure reporting', async () => {
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
@ -61,29 +61,44 @@ describe('Bind', () => {
|
||||
device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768];
|
||||
const originalTargetBinds = target.binds;
|
||||
target.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}];
|
||||
target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined);
|
||||
target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined);
|
||||
mockClear(device);
|
||||
target.configureReporting.mockImplementationOnce(() => {throw new Error("timeout")});
|
||||
target.configureReporting.mockImplementationOnce(() => {
|
||||
throw new Error('timeout');
|
||||
});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: "1234", from: 'remote', to: 'bulb_color'}));
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'}));
|
||||
await flushPromises();
|
||||
expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', [ 'colorCapabilities' ]);
|
||||
expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']);
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(4);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("lightingColorCtrl", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target);
|
||||
expect(target.configureReporting).toHaveBeenCalledTimes(3);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 3600, "minimumReportInterval": 0, "reportableChange": 0}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("lightingColorCtrl",[{"attribute":"colorTemperature","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentX","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentY","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [
|
||||
{attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0},
|
||||
]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [
|
||||
{attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1},
|
||||
]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [
|
||||
{attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
|
||||
{attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
|
||||
{attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
|
||||
]);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"transaction": "1234","data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl", "lightingColorCtrl"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
transaction: '1234',
|
||||
data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []},
|
||||
status: 'ok',
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function));
|
||||
|
||||
// Teardown
|
||||
target.binds = originalTargetBinds;
|
||||
@ -102,35 +117,48 @@ describe('Bind', () => {
|
||||
device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768];
|
||||
const originalTargetInputClusters = target.inputClusters;
|
||||
target.inputClusters = [...originalTargetInputClusters];
|
||||
target.inputClusters.splice(originalTargetInputClusters.indexOf(8), 1);// remove genLevelCtrl
|
||||
target.inputClusters.splice(originalTargetInputClusters.indexOf(8), 1); // remove genLevelCtrl
|
||||
const originalTargetOutputClusters = target.outputClusters;
|
||||
target.outputClusters = [...target.outputClusters, 8];
|
||||
const originalTargetBinds = target.binds;
|
||||
target.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}];
|
||||
target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined);
|
||||
target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined);
|
||||
mockClear(device);
|
||||
target.configureReporting.mockImplementationOnce(() => {throw new Error("timeout")});
|
||||
target.configureReporting.mockImplementationOnce(() => {
|
||||
throw new Error('timeout');
|
||||
});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: "1234", from: 'remote', to: 'bulb_color'}));
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'}));
|
||||
await flushPromises();
|
||||
|
||||
expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', [ 'colorCapabilities' ]);
|
||||
expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']);
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(4);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("lightingColorCtrl", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target);
|
||||
expect(target.configureReporting).toHaveBeenCalledTimes(2);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 3600, "minimumReportInterval": 0, "reportableChange": 0}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [
|
||||
{attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0},
|
||||
]);
|
||||
// expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("lightingColorCtrl",[{"attribute":"colorTemperature","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentX","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentY","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [
|
||||
{attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
|
||||
{attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
|
||||
{attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
|
||||
]);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"transaction": "1234","data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl","lightingColorCtrl"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
transaction: '1234',
|
||||
data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []},
|
||||
status: 'ok',
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function));
|
||||
|
||||
// Teardown
|
||||
target.binds = originalTargetBinds;
|
||||
@ -150,9 +178,11 @@ describe('Bind', () => {
|
||||
device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768];
|
||||
const originalTargetBinds = target.binds;
|
||||
target.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}];
|
||||
target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined);
|
||||
target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined);
|
||||
mockClear(device);
|
||||
target.configureReporting.mockImplementationOnce(() => {throw new Error("timeout")});
|
||||
target.configureReporting.mockImplementationOnce(() => {
|
||||
throw new Error('timeout');
|
||||
});
|
||||
const originalTargetCR = target.configuredReportings;
|
||||
target.configuredReportings = [
|
||||
{
|
||||
@ -161,28 +191,39 @@ describe('Bind', () => {
|
||||
minimumReportInterval: 0,
|
||||
maximumReportInterval: 3600,
|
||||
reportableChange: 0,
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: "1234", from: 'remote', to: 'bulb_color'}));
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'}));
|
||||
await flushPromises();
|
||||
expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', [ 'colorCapabilities' ]);
|
||||
expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']);
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(4);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("lightingColorCtrl", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target);
|
||||
expect(target.configureReporting).toHaveBeenCalledTimes(2);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 3600, "minimumReportInterval": 0, "reportableChange": 0}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [
|
||||
{attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0},
|
||||
]);
|
||||
// expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("lightingColorCtrl",[{"attribute":"colorTemperature","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentX","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1},{"attribute":"currentY","minimumReportInterval":5,"maximumReportInterval":3600,"reportableChange":1}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [
|
||||
{attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
|
||||
{attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
|
||||
{attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
|
||||
]);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"transaction": "1234","data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl", "lightingColorCtrl"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
transaction: '1234',
|
||||
data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []},
|
||||
status: 'ok',
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function));
|
||||
|
||||
// Teardown
|
||||
target.configuredReportings = originalTargetCR;
|
||||
@ -195,14 +236,15 @@ describe('Bind', () => {
|
||||
const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
|
||||
const endpoint = device.getEndpoint(1);
|
||||
mockClear(device);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color', clusters: ["genOnOff"]}));
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color', clusters: ['genOnOff']}));
|
||||
await flushPromises();
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"remote","to":"bulb_color","clusters":["genOnOff"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {from: 'remote', to: 'bulb_color', clusters: ['genOnOff'], failed: []}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -216,8 +258,9 @@ describe('Bind', () => {
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(0);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"remote","to":"button","clusters":[],"failed":[]},"status":"error","error":"Nothing to bind"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {from: 'remote', to: 'button', clusters: [], failed: []}, status: 'error', error: 'Nothing to bind'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -226,14 +269,16 @@ describe('Bind', () => {
|
||||
const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
|
||||
|
||||
// setup
|
||||
target.configureReporting.mockImplementationOnce(() => {throw new Error("timeout")});
|
||||
target.configureReporting.mockImplementationOnce(() => {
|
||||
throw new Error('timeout');
|
||||
});
|
||||
const originalRemoteBinds = device.getEndpoint(1).binds;
|
||||
device.getEndpoint(1).binds = [];
|
||||
const originalTargetBinds = target.binds;
|
||||
target.binds = [
|
||||
{cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)},
|
||||
{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)},
|
||||
{cluster: {name: 'lightingColorCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}
|
||||
{cluster: {name: 'lightingColorCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)},
|
||||
];
|
||||
|
||||
const endpoint = device.getEndpoint(1);
|
||||
@ -243,20 +288,29 @@ describe('Bind', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: '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(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target);
|
||||
|
||||
// Disable reporting
|
||||
expect(target.configureReporting).toHaveBeenCalledTimes(3);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 0, "reportableChange": 0}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 5, "reportableChange": 1}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith("lightingColorCtrl",[{"attribute":"colorTemperature","minimumReportInterval":5,"maximumReportInterval":0xFFFF,"reportableChange":1},{"attribute":"currentX","minimumReportInterval":5,"maximumReportInterval":0xFFFF,"reportableChange":1},{"attribute":"currentY","minimumReportInterval":5,"maximumReportInterval":0xFFFF,"reportableChange":1}]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [
|
||||
{attribute: 'onOff', maximumReportInterval: 0xffff, minimumReportInterval: 0, reportableChange: 0},
|
||||
]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [
|
||||
{attribute: 'currentLevel', maximumReportInterval: 0xffff, minimumReportInterval: 5, reportableChange: 1},
|
||||
]);
|
||||
expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [
|
||||
{attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1},
|
||||
{attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1},
|
||||
{attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1},
|
||||
]);
|
||||
expect(zigbeeHerdsman.devices.bulb_color.meta.configured).toBe(332242049);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/unbind',
|
||||
stringify({"data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// Teardown
|
||||
@ -273,13 +327,14 @@ describe('Bind', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: '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(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/unbind',
|
||||
stringify({"data":{"from":"remote","to":"Coordinator","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {from: 'remote', to: 'Coordinator', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -294,16 +349,21 @@ describe('Bind', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: '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(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target);
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledTimes(2);
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 3600, "minimumReportInterval": 0, "reportableChange": 0}]);
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]);
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [
|
||||
{attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0},
|
||||
]);
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [
|
||||
{attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1},
|
||||
]);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"remote","to":"group_1","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {from: 'remote', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// Should configure reproting for device added to group
|
||||
@ -311,8 +371,12 @@ describe('Bind', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb');
|
||||
await flushPromises();
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledTimes(2);
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith("genOnOff",[{"attribute": "onOff", "maximumReportInterval": 3600, "minimumReportInterval": 0, "reportableChange": 0}]);
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]);
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [
|
||||
{attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0},
|
||||
]);
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [
|
||||
{attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should unbind from group', async () => {
|
||||
@ -326,13 +390,14 @@ describe('Bind', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1'}));
|
||||
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(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/unbind',
|
||||
stringify({"data":{"from":"remote","to":"group_1","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {from: 'remote', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -347,7 +412,10 @@ describe('Bind', () => {
|
||||
const originalBinds = endpoint.binds;
|
||||
endpoint.binds = [];
|
||||
|
||||
target1Member.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}];
|
||||
target1Member.binds = [
|
||||
{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)},
|
||||
{cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)},
|
||||
];
|
||||
target1Member.configureReporting.mockClear();
|
||||
mockClear(device);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1', skip_disable_reporting: true}));
|
||||
@ -369,7 +437,10 @@ describe('Bind', () => {
|
||||
const originalBinds = endpoint.binds;
|
||||
endpoint.binds = [];
|
||||
|
||||
target1Member.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}];
|
||||
target1Member.binds = [
|
||||
{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)},
|
||||
{cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)},
|
||||
];
|
||||
target1Member.configureReporting.mockClear();
|
||||
mockClear(device);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1', skip_disable_reporting: false}));
|
||||
@ -377,8 +448,12 @@ describe('Bind', () => {
|
||||
expect(endpoint.unbind).toHaveBeenCalledTimes(3);
|
||||
// with skip_disable_reporting set, we expect it to reconfigure reporting
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledTimes(2);
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 65535, "minimumReportInterval": 5, "reportableChange": 1}])
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [{"attribute": "onOff", "maximumReportInterval": 65535, "minimumReportInterval": 0, "reportableChange": 0}])
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [
|
||||
{attribute: 'currentLevel', maximumReportInterval: 65535, minimumReportInterval: 5, reportableChange: 1},
|
||||
]);
|
||||
expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [
|
||||
{attribute: 'onOff', maximumReportInterval: 65535, minimumReportInterval: 0, reportableChange: 0},
|
||||
]);
|
||||
endpoint.binds = originalBinds;
|
||||
});
|
||||
|
||||
@ -390,13 +465,14 @@ describe('Bind', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: '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(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"remote","to":"1","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {from: 'remote', to: '1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -405,14 +481,21 @@ describe('Bind', () => {
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
mockClear(device);
|
||||
endpoint.bind.mockImplementation(() => {throw new Error('failed')});
|
||||
endpoint.bind.mockImplementation(() => {
|
||||
throw new Error('failed');
|
||||
});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"remote","to":"bulb_color","clusters":[],"failed":["genScenes","genOnOff","genLevelCtrl"]},"status":"error","error":"Failed to bind"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
data: {from: 'remote', to: 'bulb_color', clusters: [], failed: ['genScenes', 'genOnOff', 'genLevelCtrl']},
|
||||
status: 'error',
|
||||
error: 'Failed to bind',
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -424,11 +507,12 @@ describe('Bind', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch_double/right'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"remote/ep2","to":"wall_switch_double/right","clusters":["genOnOff"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {from: 'remote/ep2', to: 'wall_switch_double/right', clusters: ['genOnOff'], failed: []}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -440,11 +524,12 @@ describe('Bind', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'temperature_sensor', to: 'heating_actuator'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("msTemperatureMeasurement", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('msTemperatureMeasurement', target);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"temperature_sensor","to":"heating_actuator","clusters":["msTemperatureMeasurement"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {from: 'temperature_sensor', to: 'heating_actuator', clusters: ['msTemperatureMeasurement'], failed: []}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -456,11 +541,12 @@ describe('Bind', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"remote/ep2","to":"wall_switch","clusters":["genOnOff"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {from: 'remote/ep2', to: 'wall_switch', clusters: ['genOnOff'], failed: []}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -472,13 +558,17 @@ describe('Bind', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 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(endpoint.unbind).toHaveBeenCalledWith('genOnOff', 901);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', 901);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', 901);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/unbind',
|
||||
stringify({"data":{"from":"remote","to":"default_bind_group","clusters":["genScenes","genOnOff","genLevelCtrl"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
data: {from: 'remote', to: 'default_bind_group', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []},
|
||||
status: 'ok',
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -491,8 +581,13 @@ describe('Bind', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"remote_not_existing","to":"bulb_color"},"status":"error","error":"Source device 'remote_not_existing' does not exist"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
data: {from: 'remote_not_existing', to: 'bulb_color'},
|
||||
status: 'error',
|
||||
error: "Source device 'remote_not_existing' does not exist",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -505,8 +600,13 @@ describe('Bind', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"remote/not_existing_endpoint","to":"bulb_color"},"status":"error","error":"Source device 'remote' does not have endpoint 'not_existing_endpoint'"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
data: {from: 'remote/not_existing_endpoint', to: 'bulb_color'},
|
||||
status: 'error',
|
||||
error: "Source device 'remote' does not have endpoint 'not_existing_endpoint'",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -519,8 +619,13 @@ describe('Bind', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"remote","to":"bulb_color_not_existing"},"status":"error","error":"Target device or group 'bulb_color_not_existing' does not exist"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
data: {from: 'remote', to: 'bulb_color_not_existing'},
|
||||
status: 'error',
|
||||
error: "Target device or group 'bulb_color_not_existing' does not exist",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -533,8 +638,13 @@ describe('Bind', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
stringify({"data":{"from":"remote","to":"bulb_color/not_existing_endpoint"},"status":"error","error":"Target device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
data: {from: 'remote', to: 'bulb_color/not_existing_endpoint'},
|
||||
status: 'error',
|
||||
error: "Target device 'bulb_color' does not have endpoint 'not_existing_endpoint'",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -546,15 +656,24 @@ describe('Bind', () => {
|
||||
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(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(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(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'}});
|
||||
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 () => {
|
||||
@ -576,15 +695,24 @@ describe('Bind', () => {
|
||||
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(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(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(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'}});
|
||||
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 () => {
|
||||
@ -596,15 +724,24 @@ describe('Bind', () => {
|
||||
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(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(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(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'}});
|
||||
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 () => {
|
||||
@ -615,15 +752,24 @@ describe('Bind', () => {
|
||||
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(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(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(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'}});
|
||||
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 () => {
|
||||
@ -634,15 +780,24 @@ describe('Bind', () => {
|
||||
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(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(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(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'}});
|
||||
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 () => {
|
||||
@ -650,7 +805,9 @@ describe('Bind', () => {
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
mockClear(device);
|
||||
endpoint.bind.mockImplementationOnce(() => {throw new Error('failed')});
|
||||
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)");
|
||||
@ -665,7 +822,7 @@ describe('Bind', () => {
|
||||
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);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
|
||||
});
|
||||
|
||||
it('Legacy api: Should bind to default endpoint returned by endpoints()', async () => {
|
||||
@ -676,7 +833,7 @@ describe('Bind', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/bind/remote/ep2', 'wall_switch');
|
||||
await flushPromises();
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
|
||||
});
|
||||
|
||||
it('Legacy api: Should unbind from default_bind_group', async () => {
|
||||
@ -687,53 +844,92 @@ describe('Bind', () => {
|
||||
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(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(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(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'}});
|
||||
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};
|
||||
const payload = {data, cluster: 'manuSpecificPhilips', device: remote, endpoint: remote.getEndpoint(2), type: 'commandHueNotification', linkquality: 10, groupID: 0};
|
||||
const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'manuSpecificPhilips',
|
||||
device: remote,
|
||||
endpoint: remote.getEndpoint(2),
|
||||
type: 'commandHueNotification',
|
||||
linkquality: 10,
|
||||
groupID: 0,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(debounce).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.devices.bulb_color.getEndpoint(1).read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]);
|
||||
expect(zigbeeHerdsman.devices.bulb_color.getEndpoint(1).read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']);
|
||||
});
|
||||
|
||||
it('Should poll bounded Hue bulb when receiving message from scene controller', async () => {
|
||||
const remote = zigbeeHerdsman.devices.bj_scene_switch;
|
||||
const data = {"action": "recall_2_row_1"};
|
||||
zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockImplementationOnce(() => {throw new Error('failed')});
|
||||
const payload = {data, cluster: 'genScenes', device: remote, endpoint: remote.getEndpoint(10), type: 'commandRecall', linkquality: 10, groupID: 0};
|
||||
const data = {action: 'recall_2_row_1'};
|
||||
zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockImplementationOnce(() => {
|
||||
throw new Error('failed');
|
||||
});
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'genScenes',
|
||||
device: remote,
|
||||
endpoint: remote.getEndpoint(10),
|
||||
type: 'commandRecall',
|
||||
linkquality: 10,
|
||||
groupID: 0,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
// Calls to three clusters are expected in this case
|
||||
expect(debounce).toHaveBeenCalledTimes(3);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genOnOff", ["onOff"]);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("lightingColorCtrl", ["currentX", "currentY", "colorTemperature"]);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genOnOff', ['onOff']);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('lightingColorCtrl', [
|
||||
'currentX',
|
||||
'currentY',
|
||||
'colorTemperature',
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should poll grouped Hue bulb when receiving message from TRADFRI remote', async () => {
|
||||
zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockClear();
|
||||
zigbeeHerdsman.devices.bulb_2.getEndpoint(1).read.mockClear();
|
||||
const remote = zigbeeHerdsman.devices.tradfri_remote;
|
||||
const data = {"stepmode":0,"stepsize":43,"transtime":5};
|
||||
const payload = {data, cluster: 'genLevelCtrl', device: remote, endpoint: remote.getEndpoint(1), type: 'commandStepWithOnOff', linkquality: 10, groupID: 15071};
|
||||
const data = {stepmode: 0, stepsize: 43, transtime: 5};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'genLevelCtrl',
|
||||
device: remote,
|
||||
endpoint: remote.getEndpoint(1),
|
||||
type: 'commandStepWithOnOff',
|
||||
linkquality: 10,
|
||||
groupID: 15071,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(debounce).toHaveBeenCalledTimes(2);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledTimes(2);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genOnOff", ["onOff"]);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genOnOff', ['onOff']);
|
||||
|
||||
// Should also only debounce once
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
|
3205
test/bridge.test.js
3205
test/bridge.test.js
File diff suppressed because one or more lines are too long
@ -23,29 +23,29 @@ describe('Configure', () => {
|
||||
|
||||
const endpoint2 = device.getEndpoint(2);
|
||||
expect(endpoint2.write).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint2.write).toHaveBeenCalledWith("genBasic", {"49": {"type": 25, "value": 11}}, {"disableDefaultResponse": true, "manufacturerCode": 4107});
|
||||
expect(endpoint2.write).toHaveBeenCalledWith('genBasic', {49: {type: 25, value: 11}}, {disableDefaultResponse: true, manufacturerCode: 4107});
|
||||
expect(device.meta.configured).toBe(332242049);
|
||||
}
|
||||
};
|
||||
|
||||
const expectBulbConfigured = () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint1 = device.getEndpoint(1);
|
||||
expect(endpoint1.read).toHaveBeenCalledTimes(2);
|
||||
expect(endpoint1.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']);
|
||||
expect(endpoint1.read).toHaveBeenCalledWith('lightingColorCtrl', [ 'colorTempPhysicalMin', 'colorTempPhysicalMax' ]);
|
||||
}
|
||||
expect(endpoint1.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorTempPhysicalMin', 'colorTempPhysicalMax']);
|
||||
};
|
||||
|
||||
const expectBulbNotConfigured = () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint1 = device.getEndpoint(1);
|
||||
expect(endpoint1.read).toHaveBeenCalledTimes(0);
|
||||
}
|
||||
};
|
||||
|
||||
const expectRemoteNotConfigured = () => {
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
const endpoint1 = device.getEndpoint(1);
|
||||
expect(endpoint1.bind).toHaveBeenCalledTimes(0);
|
||||
}
|
||||
};
|
||||
|
||||
const mockClear = (device) => {
|
||||
for (const endpoint of device.endpoints) {
|
||||
@ -54,12 +54,12 @@ describe('Configure', () => {
|
||||
endpoint.configureReporting.mockClear();
|
||||
endpoint.bind.mockClear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let resetExtension = async () => {
|
||||
await controller.enableDisableExtension(false, 'Configure');
|
||||
await controller.enableDisableExtension(true, 'Configure');
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers();
|
||||
@ -80,7 +80,7 @@ describe('Configure', () => {
|
||||
|
||||
afterAll(async () => {
|
||||
jest.useRealTimers();
|
||||
})
|
||||
});
|
||||
|
||||
it('Should configure Router on startup', async () => {
|
||||
expectBulbConfigured();
|
||||
@ -149,39 +149,45 @@ describe('Configure', () => {
|
||||
expectRemoteConfigured();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/configure',
|
||||
stringify({"data":{"id": "remote"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {id: 'remote'}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Fail to configure via MQTT when device does not exist', async () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: "not_existing_device"}));
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'not_existing_device'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/configure',
|
||||
stringify({"data":{"id": "not_existing_device"},"status":"error","error": "Device 'not_existing_device' does not exist"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {id: 'not_existing_device'}, status: 'error', error: "Device 'not_existing_device' does not exist"}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Fail to configure via MQTT when configure fails', async () => {
|
||||
zigbeeHerdsman.devices.remote.getEndpoint(1).bind.mockImplementationOnce(async () => {throw new Error('Bind timeout after 10s')});
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: "remote"}));
|
||||
zigbeeHerdsman.devices.remote.getEndpoint(1).bind.mockImplementationOnce(async () => {
|
||||
throw new Error('Bind timeout after 10s');
|
||||
});
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'remote'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/configure',
|
||||
stringify({"data":{"id": "remote"},"status":"error","error": "Failed to configure (Bind timeout after 10s)"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {id: 'remote'}, status: 'error', error: 'Failed to configure (Bind timeout after 10s)'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Fail to configure via MQTT when device has no configure', async () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: "0x0017882104a44559", transaction: 20}));
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: '0x0017882104a44559', transaction: 20}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/configure',
|
||||
stringify({"data":{"id": "0x0017882104a44559"},"status":"error","error": "Device 'TS0601_thermostat' cannot be configured","transaction":20}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {id: '0x0017882104a44559'}, status: 'error', error: "Device 'TS0601_thermostat' cannot be configured", transaction: 20}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -202,7 +208,7 @@ describe('Configure', () => {
|
||||
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.`)
|
||||
expect(logger.warning).toHaveBeenCalledWith(`Skipping configure of 'TS0601_thermostat', device does not require this.`);
|
||||
});
|
||||
|
||||
it('Should not configure when interview not completed', async () => {
|
||||
@ -237,19 +243,29 @@ describe('Configure', () => {
|
||||
delete device.meta.configured;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
mockClear(device);
|
||||
endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')});
|
||||
endpoint.bind.mockImplementationOnce(async () => {
|
||||
throw new Error('BLA');
|
||||
});
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device});
|
||||
await flushPromises();
|
||||
endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')});
|
||||
endpoint.bind.mockImplementationOnce(async () => {
|
||||
throw new Error('BLA');
|
||||
});
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device});
|
||||
await flushPromises();
|
||||
endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')});
|
||||
endpoint.bind.mockImplementationOnce(async () => {
|
||||
throw new Error('BLA');
|
||||
});
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device});
|
||||
await flushPromises();
|
||||
endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')});
|
||||
endpoint.bind.mockImplementationOnce(async () => {
|
||||
throw new Error('BLA');
|
||||
});
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device});
|
||||
await flushPromises();
|
||||
endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')});
|
||||
endpoint.bind.mockImplementationOnce(async () => {
|
||||
throw new Error('BLA');
|
||||
});
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device});
|
||||
await flushPromises();
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(3);
|
||||
|
@ -1,4 +1,4 @@
|
||||
process.env.NOTIFY_SOCKET = "mocked";
|
||||
process.env.NOTIFY_SOCKET = 'mocked';
|
||||
const data = require('./stub/data');
|
||||
const logger = require('./stub/logger');
|
||||
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
@ -10,24 +10,36 @@ const stringify = require('json-stable-stringify-without-jsonify');
|
||||
const flushPromises = require('./lib/flushPromises');
|
||||
const tmp = require('tmp');
|
||||
const mocksClear = [
|
||||
zigbeeHerdsman.permitJoin, MQTT.end, zigbeeHerdsman.stop, logger.debug,
|
||||
MQTT.publish, MQTT.connect, zigbeeHerdsman.devices.bulb_color.removeFromNetwork,
|
||||
zigbeeHerdsman.devices.bulb.removeFromNetwork, logger.error,
|
||||
zigbeeHerdsman.permitJoin,
|
||||
MQTT.end,
|
||||
zigbeeHerdsman.stop,
|
||||
logger.debug,
|
||||
MQTT.publish,
|
||||
MQTT.connect,
|
||||
zigbeeHerdsman.devices.bulb_color.removeFromNetwork,
|
||||
zigbeeHerdsman.devices.bulb.removeFromNetwork,
|
||||
logger.error,
|
||||
];
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const LOG_MQTT_NS = 'z2m:mqtt';
|
||||
|
||||
jest.mock('sd-notify', () => {
|
||||
return {
|
||||
watchdogInterval: () => {return 3000;},
|
||||
startWatchdogMode: (interval) => {},
|
||||
stopWatchdogMode: () => {},
|
||||
ready: () => {},
|
||||
stopping: () => {},
|
||||
};
|
||||
}, {virtual: true});
|
||||
jest.mock(
|
||||
'sd-notify',
|
||||
() => {
|
||||
return {
|
||||
watchdogInterval: () => {
|
||||
return 3000;
|
||||
},
|
||||
startWatchdogMode: (interval) => {},
|
||||
stopWatchdogMode: () => {},
|
||||
ready: () => {},
|
||||
stopping: () => {},
|
||||
};
|
||||
},
|
||||
{virtual: true},
|
||||
);
|
||||
|
||||
describe('Controller', () => {
|
||||
let controller;
|
||||
@ -51,27 +63,51 @@ describe('Controller', () => {
|
||||
|
||||
afterAll(async () => {
|
||||
jest.useRealTimers();
|
||||
})
|
||||
});
|
||||
|
||||
it('Start controller', async () => {
|
||||
await controller.start();
|
||||
expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({"network":{"panID":6754,"extendedPanID":[221,221,221,221,221,221,221,221],"channelList":[11],"networkKey":[1,3,5,7,9,11,13,15,0,2,4,6,8,10,12,13]},"databasePath":path.join(data.mockDir, "database.db"), "databaseBackupPath":path.join(data.mockDir, "database.db.backup"),"backupPath":path.join(data.mockDir, "coordinator_backup.json"),"acceptJoiningDeviceHandler": expect.any(Function),adapter: {concurrent: null, delay: null, disableLED: false}, "serialPort":{"baudRate":undefined,"rtscts":undefined,"path":"/dev/dummy"}});
|
||||
expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({
|
||||
network: {
|
||||
panID: 6754,
|
||||
extendedPanID: [221, 221, 221, 221, 221, 221, 221, 221],
|
||||
channelList: [11],
|
||||
networkKey: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
|
||||
},
|
||||
databasePath: path.join(data.mockDir, 'database.db'),
|
||||
databaseBackupPath: path.join(data.mockDir, 'database.db.backup'),
|
||||
backupPath: path.join(data.mockDir, 'coordinator_backup.json'),
|
||||
acceptJoiningDeviceHandler: expect.any(Function),
|
||||
adapter: {concurrent: null, delay: null, disableLED: false},
|
||||
serialPort: {baudRate: undefined, rtscts: undefined, path: '/dev/dummy'},
|
||||
});
|
||||
expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0);
|
||||
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined:`)
|
||||
expect(logger.info).toHaveBeenCalledWith('bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (Router)');
|
||||
expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined:`);
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
'bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (Router)',
|
||||
);
|
||||
expect(logger.info).toHaveBeenCalledWith('remote (0x0017880104e45517): 324131092621 - Philips Hue dimmer switch (EndDevice)');
|
||||
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}});
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99}),{ retain: true, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({"brightness":255}), { retain: true, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', {
|
||||
will: {payload: Buffer.from('offline'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1},
|
||||
});
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {retain: true, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Start controller when permit join fails', async () => {
|
||||
zigbeeHerdsman.permitJoin.mockImplementationOnce(() => {throw new Error("failed!")});
|
||||
zigbeeHerdsman.permitJoin.mockImplementationOnce(() => {
|
||||
throw new Error('failed!');
|
||||
});
|
||||
await controller.start();
|
||||
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.connect).toHaveBeenCalledTimes(1);
|
||||
@ -79,29 +115,31 @@ describe('Controller', () => {
|
||||
|
||||
it('Start controller with specific MQTT settings', async () => {
|
||||
const ca = tmp.fileSync().name;
|
||||
fs.writeFileSync(ca, "ca");
|
||||
fs.writeFileSync(ca, 'ca');
|
||||
const key = tmp.fileSync().name;
|
||||
fs.writeFileSync(key, "key");
|
||||
fs.writeFileSync(key, 'key');
|
||||
const cert = tmp.fileSync().name;
|
||||
fs.writeFileSync(cert, "cert");
|
||||
fs.writeFileSync(cert, 'cert');
|
||||
|
||||
const configuration = {
|
||||
base_topic: "zigbee2mqtt",
|
||||
server: "mqtt://localhost",
|
||||
base_topic: 'zigbee2mqtt',
|
||||
server: 'mqtt://localhost',
|
||||
keepalive: 30,
|
||||
ca, cert, key,
|
||||
ca,
|
||||
cert,
|
||||
key,
|
||||
password: 'pass',
|
||||
user: 'user1',
|
||||
client_id: 'my_client_id',
|
||||
reject_unauthorized: false,
|
||||
version: 5,
|
||||
}
|
||||
settings.set(['mqtt'], configuration)
|
||||
};
|
||||
settings.set(['mqtt'], configuration);
|
||||
await controller.start();
|
||||
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('offline'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1},
|
||||
keepalive: 30,
|
||||
ca: Buffer.from([99, 97]),
|
||||
key: Buffer.from([107, 101, 121]),
|
||||
@ -111,9 +149,8 @@ describe('Controller', () => {
|
||||
clientId: 'my_client_id',
|
||||
rejectUnauthorized: false,
|
||||
protocolVersion: 5,
|
||||
|
||||
}
|
||||
expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", expected);
|
||||
};
|
||||
expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected);
|
||||
});
|
||||
|
||||
it('Should generate network_key, pan_id and ext_pan_id when set to GENERATE', async () => {
|
||||
@ -134,9 +171,14 @@ describe('Controller', () => {
|
||||
data.writeDefaultState();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/remote", stringify({"brightness":255}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":'ON'}), {"qos": 0, "retain":false}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}),
|
||||
{qos: 0, retain: true},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {qos: 0, retain: false}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Start controller should not publish cached states when disabled', async () => {
|
||||
@ -144,8 +186,8 @@ describe('Controller', () => {
|
||||
data.writeDefaultState();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
const publishedTopics = MQTT.publish.mock.calls.map(m => m[0]);
|
||||
expect(publishedTopics).toEqual(expect.not.arrayContaining(["zigbee2mqtt/bulb", "zigbee2mqtt/remote"]));
|
||||
const publishedTopics = MQTT.publish.mock.calls.map((m) => m[0]);
|
||||
expect(publishedTopics).toEqual(expect.not.arrayContaining(['zigbee2mqtt/bulb', 'zigbee2mqtt/remote']));
|
||||
});
|
||||
|
||||
it('Start controller should not publish cached states when cache_state is false', async () => {
|
||||
@ -153,8 +195,13 @@ describe('Controller', () => {
|
||||
data.writeDefaultState();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith("zigbee2mqtt/bulb", `{"state":"ON","brightness":50,"color_temp":370,"linkquality":99}`, {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith("zigbee2mqtt/remote", `{"brightness":255}`, {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
`{"state":"ON","brightness":50,"color_temp":370,"linkquality":99}`,
|
||||
{qos: 0, retain: true},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/remote', `{"brightness":255}`, {qos: 0, retain: true}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Log when MQTT client is unavailable', async () => {
|
||||
@ -163,7 +210,7 @@ describe('Controller', () => {
|
||||
logger.error.mockClear();
|
||||
controller.mqtt.client.reconnecting = true;
|
||||
jest.advanceTimersByTime(11 * 1000);
|
||||
expect(logger.error).toHaveBeenCalledWith("Not connected to MQTT server!");
|
||||
expect(logger.error).toHaveBeenCalledWith('Not connected to MQTT server!');
|
||||
controller.mqtt.client.reconnecting = false;
|
||||
});
|
||||
|
||||
@ -173,11 +220,19 @@ describe('Controller', () => {
|
||||
logger.error.mockClear();
|
||||
controller.mqtt.client.reconnecting = true;
|
||||
const device = controller.zigbee.resolveEntity('bulb');
|
||||
await controller.publishEntityState(device, {state: 'ON', brightness: 50, color_temp: 370, color: {r: 100, g: 50, b: 10}, dummy: {1: 'yes', 2: 'no'}});
|
||||
await controller.publishEntityState(device, {
|
||||
state: 'ON',
|
||||
brightness: 50,
|
||||
color_temp: 370,
|
||||
color: {r: 100, g: 50, b: 10},
|
||||
dummy: {1: 'yes', 2: 'no'},
|
||||
});
|
||||
await flushPromises();
|
||||
expect(logger.error).toHaveBeenCalledTimes(2);
|
||||
expect(logger.error).toHaveBeenCalledWith("Not connected to MQTT server!");
|
||||
expect(logger.error).toHaveBeenCalledWith("Cannot send message: topic: 'zigbee2mqtt/bulb', payload: '{\"brightness\":50,\"color\":{\"b\":10,\"g\":50,\"r\":100},\"color_temp\":370,\"dummy\":{\"1\":\"yes\",\"2\":\"no\"},\"linkquality\":99,\"state\":\"ON\"}");
|
||||
expect(logger.error).toHaveBeenCalledWith('Not connected to MQTT server!');
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'Cannot send message: topic: \'zigbee2mqtt/bulb\', payload: \'{"brightness":50,"color":{"b":10,"g":50,"r":100},"color_temp":370,"dummy":{"1":"yes","2":"no"},"linkquality":99,"state":"ON"}',
|
||||
);
|
||||
controller.mqtt.client.reconnecting = false;
|
||||
});
|
||||
|
||||
@ -190,7 +245,9 @@ describe('Controller', () => {
|
||||
|
||||
it('Should remove device not on passlist on startup', async () => {
|
||||
settings.set(['passlist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]);
|
||||
zigbeeHerdsman.devices.bulb.removeFromNetwork.mockImplementationOnce(() => {throw new Error("dummy")});
|
||||
zigbeeHerdsman.devices.bulb.removeFromNetwork.mockImplementationOnce(() => {
|
||||
throw new Error('dummy');
|
||||
});
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(0);
|
||||
@ -206,7 +263,9 @@ describe('Controller', () => {
|
||||
});
|
||||
|
||||
it('Start controller fails', async () => {
|
||||
zigbeeHerdsman.start.mockImplementationOnce(() => {throw new Error('failed')});
|
||||
zigbeeHerdsman.start.mockImplementationOnce(() => {
|
||||
throw new Error('failed');
|
||||
});
|
||||
await controller.start();
|
||||
expect(mockExit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@ -247,7 +306,9 @@ describe('Controller', () => {
|
||||
});
|
||||
|
||||
it('Start controller and stop', async () => {
|
||||
zigbeeHerdsman.stop.mockImplementationOnce(() => {throw new Error('failed')})
|
||||
zigbeeHerdsman.stop.mockImplementationOnce(() => {
|
||||
throw new Error('failed');
|
||||
});
|
||||
await controller.start();
|
||||
await controller.stop();
|
||||
expect(MQTT.end).toHaveBeenCalledTimes(1);
|
||||
@ -257,7 +318,9 @@ describe('Controller', () => {
|
||||
});
|
||||
|
||||
it('Start controller adapter disconnects', async () => {
|
||||
zigbeeHerdsman.stop.mockImplementationOnce(() => {throw new Error('failed')})
|
||||
zigbeeHerdsman.stop.mockImplementationOnce(() => {
|
||||
throw new Error('failed');
|
||||
});
|
||||
await controller.start();
|
||||
await zigbeeHerdsman.events.adapterDisconnected();
|
||||
await flushPromises();
|
||||
@ -271,14 +334,14 @@ describe('Controller', () => {
|
||||
await controller.start();
|
||||
logger.debug.mockClear();
|
||||
await MQTT.events.message('dummytopic', 'dummymessage');
|
||||
expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'dummytopic' with data 'dummymessage'", LOG_MQTT_NS)
|
||||
expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'dummytopic' with data 'dummymessage'", LOG_MQTT_NS);
|
||||
});
|
||||
|
||||
it('Skip MQTT messages on topic we published to', async () => {
|
||||
await controller.start();
|
||||
logger.debug.mockClear();
|
||||
await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped');
|
||||
expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'zigbee2mqtt/skip-this-topic' with data 'skipped'", LOG_MQTT_NS)
|
||||
expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'zigbee2mqtt/skip-this-topic' with data 'skipped'", LOG_MQTT_NS);
|
||||
logger.debug.mockClear();
|
||||
await controller.mqtt.publish('skip-this-topic', '', {});
|
||||
await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped');
|
||||
@ -288,19 +351,38 @@ describe('Controller', () => {
|
||||
it('On zigbee event message', async () => {
|
||||
await controller.start();
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, cluster: 'genBasic', data: {modelId: device.modelID}};
|
||||
const payload = {
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
cluster: 'genBasic',
|
||||
data: {modelId: device.modelID},
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(logger.debug).toHaveBeenCalledWith(`Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1`);
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
`Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1`,
|
||||
);
|
||||
});
|
||||
|
||||
it('On zigbee event message with group ID', async () => {
|
||||
await controller.start();
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, groupID: 0, cluster: 'genBasic', data: {modelId: device.modelID}};
|
||||
const payload = {
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
groupID: 0,
|
||||
cluster: 'genBasic',
|
||||
data: {modelId: device.modelID},
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(logger.debug).toHaveBeenCalledWith(`Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1 with groupID 0`);
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
`Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1 with groupID 0`,
|
||||
);
|
||||
});
|
||||
|
||||
it('Should add entities which are missing from configuration but are in database to configuration', async () => {
|
||||
@ -315,7 +397,12 @@ describe('Controller', () => {
|
||||
const payload = {device};
|
||||
await zigbeeHerdsman.events.deviceJoined(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bridge/log", stringify({"type":"device_connected","message":{"friendly_name":"bulb"}}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'device_connected', message: {friendly_name: 'bulb'}}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('acceptJoiningDeviceHandler reject device on blocklist', async () => {
|
||||
@ -373,7 +460,12 @@ describe('Controller', () => {
|
||||
zigbeeHerdsman.events.deviceJoined(payload);
|
||||
zigbeeHerdsman.events.deviceJoined(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bridge/log", stringify({"type":"device_connected","message":{"friendly_name":"bulb"}}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'device_connected', message: {friendly_name: 'bulb'}}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('On zigbee deviceInterview started', async () => {
|
||||
@ -382,7 +474,12 @@ describe('Controller', () => {
|
||||
const payload = {device, status: 'started'};
|
||||
await zigbeeHerdsman.events.deviceInterview(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', stringify({"type":"pairing","message":"interview_started","meta":{"friendly_name":"bulb"}}), { retain: false, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'pairing', message: 'interview_started', meta: {friendly_name: 'bulb'}}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('On zigbee deviceInterview failed', async () => {
|
||||
@ -391,7 +488,12 @@ describe('Controller', () => {
|
||||
const payload = {device, status: 'failed'};
|
||||
await zigbeeHerdsman.events.deviceInterview(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', stringify({"type":"pairing","message":"interview_failed","meta":{"friendly_name":"bulb"}}), { retain: false, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'pairing', message: 'interview_failed', meta: {friendly_name: 'bulb'}}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('On zigbee deviceInterview successful supported', async () => {
|
||||
@ -400,7 +502,22 @@ describe('Controller', () => {
|
||||
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).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),
|
||||
);
|
||||
});
|
||||
|
||||
it('On zigbee deviceInterview successful not supported', async () => {
|
||||
@ -409,7 +526,12 @@ describe('Controller', () => {
|
||||
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).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'pairing', message: 'interview_successful', meta: {friendly_name: '0x0017880104e45518', supported: false}}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('On zigbee event device announce', async () => {
|
||||
@ -419,19 +541,29 @@ describe('Controller', () => {
|
||||
await zigbeeHerdsman.events.deviceAnnounce(payload);
|
||||
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"}}), { retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'device_announced', message: 'announce', meta: {friendly_name: 'bulb'}}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('On zigbee event device leave (removed from database and settings)', async () => {
|
||||
await controller.start();
|
||||
zigbeeHerdsman.returnDevices.push('0x00124b00120144ae');
|
||||
settings.set(['devices'], {})
|
||||
settings.set(['devices'], {});
|
||||
MQTT.publish.mockClear();
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const payload = {ieeeAddr: device.ieeeAddr};
|
||||
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"}}), { retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'device_removed', message: 'left_network', meta: {friendly_name: '0x000b57fffec6a5b2'}}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('On zigbee event device leave (removed from database and NOT settings)', async () => {
|
||||
@ -442,7 +574,12 @@ describe('Controller', () => {
|
||||
const payload = {ieeeAddr: device.ieeeAddr};
|
||||
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"}}), { retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'device_removed', message: 'left_network', meta: {friendly_name: '0x000b57fffec6a5b2'}}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Publish entity state attribute output', async () => {
|
||||
@ -450,16 +587,24 @@ describe('Controller', () => {
|
||||
settings.set(['experimental', 'output'], 'attribute');
|
||||
MQTT.publish.mockClear();
|
||||
const device = controller.zigbee.resolveEntity('bulb');
|
||||
await controller.publishEntityState(device, {dummy: {1: 'yes', 2: 'no'}, color: {r: 100, g: 50, b: 10}, state: 'ON', test: undefined, test1: null, color_temp: 370, brightness: 50});
|
||||
await controller.publishEntityState(device, {
|
||||
dummy: {1: 'yes', 2: 'no'},
|
||||
color: {r: 100, g: 50, b: 10},
|
||||
state: 'ON',
|
||||
test: undefined,
|
||||
test1: null,
|
||||
color_temp: 370,
|
||||
brightness: 50,
|
||||
});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "50", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/color_temp", "370", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/color", '100,50,10', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/dummy-1", "yes", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/dummy-2", "no", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/test1", '', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/test", '', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '50', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color', '100,50,10', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-1', 'yes', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-2', 'no', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/test1', '', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/test', '', {qos: 0, retain: true}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Publish entity state attribute_json output', async () => {
|
||||
@ -470,14 +615,18 @@ describe('Controller', () => {
|
||||
await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(5);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/color_temp", "370", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "99", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200,"color_temp":370,"linkquality":99}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '99', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}),
|
||||
{qos: 0, retain: true},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('Publish entity state attribute_json output filtered', async () => {
|
||||
await controller.start();
|
||||
settings.set(['experimental', 'output'], 'attribute_and_json');
|
||||
@ -487,9 +636,14 @@ describe('Controller', () => {
|
||||
await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({state: 'ON', brightness: 200}),
|
||||
{qos: 0, retain: true},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Publish entity state attribute_json output filtered (device_options)', async () => {
|
||||
@ -501,9 +655,14 @@ describe('Controller', () => {
|
||||
await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({state: 'ON', brightness: 200}),
|
||||
{qos: 0, retain: true},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Publish entity state attribute_json output filtered cache', async () => {
|
||||
@ -513,17 +672,22 @@ describe('Controller', () => {
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
const device = controller.zigbee.resolveEntity('bulb');
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":50,"color_temp":370,"linkquality":99,"state":"ON"});
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: 'ON'});
|
||||
|
||||
await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 87});
|
||||
await flushPromises();
|
||||
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":200,"color_temp":370,"state":"ON"});
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 200, color_temp: 370, state: 'ON'});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(5);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "87", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200,"color_temp":370,"linkquality":87}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}),
|
||||
{qos: 0, retain: true},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Publish entity state attribute_json output filtered cache (device_options)', async () => {
|
||||
@ -533,17 +697,22 @@ describe('Controller', () => {
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
const device = controller.zigbee.resolveEntity('bulb');
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":50,"color_temp":370,"linkquality":99,"state":"ON"});
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: 'ON'});
|
||||
|
||||
await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 87});
|
||||
await flushPromises();
|
||||
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":200,"color_temp":370,"state":"ON"});
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 200, color_temp: 370, state: 'ON'});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(5);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "87", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200,"color_temp":370,"linkquality":87}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}),
|
||||
{qos: 0, retain: true},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Publish entity state with device information', async () => {
|
||||
@ -553,13 +722,52 @@ describe('Controller', () => {
|
||||
let device = controller.zigbee.resolveEntity('bulb');
|
||||
await controller.publishEntityState(device, {state: 'ON'});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"device":{"friendlyName":"bulb","model":"LED1545G12","ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369,"type":"Router","manufacturerID":4476,"powerSource":"Mains (single phase)","dateCode":null, "softwareBuildID": null}}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({
|
||||
state: 'ON',
|
||||
brightness: 50,
|
||||
color_temp: 370,
|
||||
linkquality: 99,
|
||||
device: {
|
||||
friendlyName: 'bulb',
|
||||
model: 'LED1545G12',
|
||||
ieeeAddr: '0x000b57fffec6a5b2',
|
||||
networkAddress: 40369,
|
||||
type: 'Router',
|
||||
manufacturerID: 4476,
|
||||
powerSource: 'Mains (single phase)',
|
||||
dateCode: null,
|
||||
softwareBuildID: null,
|
||||
},
|
||||
}),
|
||||
{qos: 0, retain: true},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// Unsupported device should have model "unknown"
|
||||
device = controller.zigbee.resolveEntity('unsupported2');
|
||||
await controller.publishEntityState(device, {state: 'ON'});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/unsupported2', stringify({"state":"ON","device":{"friendlyName":"unsupported2","model":"notSupportedModelID","ieeeAddr":"0x0017880104e45529","networkAddress":6536,"type":"EndDevice","manufacturerID":0,"powerSource":"Battery","dateCode":null, "softwareBuildID": null}}), {"qos": 0, "retain": false}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/unsupported2',
|
||||
stringify({
|
||||
state: 'ON',
|
||||
device: {
|
||||
friendlyName: 'unsupported2',
|
||||
model: 'notSupportedModelID',
|
||||
ieeeAddr: '0x0017880104e45529',
|
||||
networkAddress: 6536,
|
||||
type: 'EndDevice',
|
||||
manufacturerID: 0,
|
||||
powerSource: 'Battery',
|
||||
dateCode: null,
|
||||
softwareBuildID: null,
|
||||
},
|
||||
}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should publish entity state without retain', async () => {
|
||||
@ -569,7 +777,12 @@ describe('Controller', () => {
|
||||
const device = controller.zigbee.resolveEntity('bulb');
|
||||
await controller.publishEntityState(device, {state: 'ON'});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99}), {"qos": 0, "retain": false}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should publish entity state with retain', async () => {
|
||||
@ -579,7 +792,12 @@ describe('Controller', () => {
|
||||
const device = controller.zigbee.resolveEntity('bulb');
|
||||
await controller.publishEntityState(device, {state: 'ON'});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}),
|
||||
{qos: 0, retain: true},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should publish entity state with expiring retention', async () => {
|
||||
@ -591,7 +809,12 @@ describe('Controller', () => {
|
||||
const device = controller.zigbee.resolveEntity('bulb');
|
||||
await controller.publishEntityState(device, {state: 'ON'});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99}), {"qos": 0, "retain": true, "properties": {messageExpiryInterval: 37}}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}),
|
||||
{qos: 0, retain: true, properties: {messageExpiryInterval: 37}},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Publish entity state no empty messages', async () => {
|
||||
@ -614,8 +837,13 @@ describe('Controller', () => {
|
||||
await controller.publishEntityState(device, {brightness: 200});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({state: "ON"}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({state: "ON", brightness: 200}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({state: 'ON', brightness: 200}),
|
||||
{qos: 0, retain: true},
|
||||
expect.any(Function),
|
||||
);
|
||||
await controller.stop();
|
||||
expect(data.stateExists()).toBeFalsy();
|
||||
});
|
||||
@ -624,7 +852,7 @@ describe('Controller', () => {
|
||||
data.removeState();
|
||||
await controller.start();
|
||||
logger.error.mockClear();
|
||||
controller.state.file = "/";
|
||||
controller.state.file = '/';
|
||||
await controller.state.save();
|
||||
expect(logger.error).toHaveBeenCalledWith(expect.stringMatching(/Failed to write state to \'\/\'/));
|
||||
});
|
||||
@ -639,8 +867,8 @@ describe('Controller', () => {
|
||||
await controller.publishEntityState(device, {brightness: 200});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON"}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"brightness":200}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({brightness: 200}), {qos: 0, retain: true}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should start when state is corrupted', async () => {
|
||||
@ -656,18 +884,18 @@ describe('Controller', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.connect).toHaveBeenCalledTimes(1);
|
||||
const expected = {
|
||||
"will": { "payload": Buffer.from("offline"), "retain": false, "topic": "zigbee2mqtt/bridge/state", "qos": 1 },
|
||||
}
|
||||
expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", expected);
|
||||
will: {payload: Buffer.from('offline'), retain: false, topic: 'zigbee2mqtt/bridge/state', qos: 1},
|
||||
};
|
||||
expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected);
|
||||
});
|
||||
|
||||
it('Should republish retained messages on MQTT reconnect', async () => {
|
||||
await controller.start();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events['connect']();
|
||||
await jest.advanceTimersByTimeAsync(2500);// before any startup configure triggers
|
||||
await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(14);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should not republish retained messages on MQTT reconnect when retained message are sent', async () => {
|
||||
@ -676,19 +904,19 @@ describe('Controller', () => {
|
||||
MQTT.events['connect']();
|
||||
await flushPromises();
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/info', 'dummy');
|
||||
await jest.advanceTimersByTimeAsync(2500);// before any startup configure triggers
|
||||
await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/state', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/state', expect.any(String), {retain: true, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should prevent any message being published with retain flag when force_disable_retain is set', async () => {
|
||||
settings.set(['mqtt', 'force_disable_retain'], true);
|
||||
await controller.mqtt.connect()
|
||||
await controller.mqtt.connect();
|
||||
MQTT.publish.mockClear();
|
||||
await controller.mqtt.publish('fo', 'bar', { retain: true })
|
||||
await controller.mqtt.publish('fo', 'bar', {retain: true});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', { retain: false, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should disable legacy options on new network start', async () => {
|
||||
@ -711,7 +939,11 @@ describe('Controller', () => {
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device, reason: 'deviceAnnounce'});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/remote', stringify({"brightness":255,"last_seen":1000}), { qos: 0, retain: true }, expect.any(Function));
|
||||
'zigbee2mqtt/remote',
|
||||
stringify({brightness: 255, last_seen: 1000}),
|
||||
{qos: 0, retain: true},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should not publish last seen changes when reason is messageEmitted', async () => {
|
||||
@ -728,16 +960,25 @@ describe('Controller', () => {
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/9218
|
||||
await controller.start();
|
||||
const device = zigbeeHerdsman.devices.coordinator;
|
||||
const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, cluster: 'genBasic', data: {modelId: device.modelID}};
|
||||
const payload = {
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
cluster: 'genBasic',
|
||||
data: {modelId: device.modelID},
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(logger.debug).toHaveBeenCalledWith(`Received Zigbee message from 'Coordinator', type 'attributeReport', cluster 'genBasic', data '{"modelId":null}' from endpoint 1, ignoring since it is from coordinator`);
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
`Received Zigbee message from 'Coordinator', type 'attributeReport', cluster 'genBasic', data '{"modelId":null}' from endpoint 1, ignoring since it is from coordinator`,
|
||||
);
|
||||
});
|
||||
|
||||
it('Should remove state of removed device when stopped', async () => {
|
||||
await controller.start();
|
||||
const device = controller.zigbee.resolveEntity('bulb');
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":50,"color_temp":370,"linkquality":99,"state":"ON"});
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: 'ON'});
|
||||
device.zh.isDeleted = true;
|
||||
await controller.stop();
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual(undefined);
|
||||
@ -745,7 +986,9 @@ describe('Controller', () => {
|
||||
|
||||
it('EventBus should handle errors', async () => {
|
||||
const eventbus = controller.eventBus;
|
||||
const callback = jest.fn().mockImplementation(async () => {throw new Error('Whoops!')});
|
||||
const callback = jest.fn().mockImplementation(async () => {
|
||||
throw new Error('Whoops!');
|
||||
});
|
||||
eventbus.onStateChange('test', callback);
|
||||
eventbus.emitStateChange({});
|
||||
await flushPromises();
|
||||
|
@ -12,31 +12,48 @@ const fs = require('fs');
|
||||
|
||||
zigbeeHerdsmanConverters.addDefinition = jest.fn();
|
||||
|
||||
const mocksClear = [zigbeeHerdsmanConverters.addDefinition, zigbeeHerdsman.permitJoin,
|
||||
mockExit, MQTT.end, zigbeeHerdsman.stop, logger.debug,
|
||||
MQTT.publish, MQTT.connect, zigbeeHerdsman.devices.bulb_color.removeFromNetwork,
|
||||
zigbeeHerdsman.devices.bulb.removeFromNetwork, logger.error,
|
||||
const mocksClear = [
|
||||
zigbeeHerdsmanConverters.addDefinition,
|
||||
zigbeeHerdsman.permitJoin,
|
||||
mockExit,
|
||||
MQTT.end,
|
||||
zigbeeHerdsman.stop,
|
||||
logger.debug,
|
||||
MQTT.publish,
|
||||
MQTT.connect,
|
||||
zigbeeHerdsman.devices.bulb_color.removeFromNetwork,
|
||||
zigbeeHerdsman.devices.bulb.removeFromNetwork,
|
||||
logger.error,
|
||||
];
|
||||
|
||||
jest.mock(
|
||||
'mock-external-converter-module', () => {
|
||||
'mock-external-converter-module',
|
||||
() => {
|
||||
return {
|
||||
mock: true
|
||||
mock: true,
|
||||
};
|
||||
}, {
|
||||
virtual: true
|
||||
});
|
||||
},
|
||||
{
|
||||
virtual: true,
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'mock-multiple-external-converter-module', () => {
|
||||
return [{
|
||||
mock: 1
|
||||
}, {
|
||||
mock: 2
|
||||
}];
|
||||
}, {
|
||||
virtual: true
|
||||
});
|
||||
'mock-multiple-external-converter-module',
|
||||
() => {
|
||||
return [
|
||||
{
|
||||
mock: 1,
|
||||
},
|
||||
{
|
||||
mock: 2,
|
||||
},
|
||||
];
|
||||
},
|
||||
{
|
||||
virtual: true,
|
||||
},
|
||||
);
|
||||
|
||||
describe('Loads external converters', () => {
|
||||
let controller;
|
||||
@ -44,7 +61,7 @@ describe('Loads external converters', () => {
|
||||
let resetExtension = async () => {
|
||||
await controller.enableDisableExtension(false, 'ExternalConverters');
|
||||
await controller.enableDisableExtension(true, 'ExternalConverters');
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers();
|
||||
@ -83,12 +100,15 @@ describe('Loads external converters', () => {
|
||||
description: 'external',
|
||||
fromZigbee: [],
|
||||
toZigbee: [],
|
||||
exposes: []
|
||||
exposes: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('Loads multiple external converters', async () => {
|
||||
fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'), path.join(data.mockDir, 'mock-external-converter-multiple.js'));
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'),
|
||||
path.join(data.mockDir, 'mock-external-converter-multiple.js'),
|
||||
);
|
||||
settings.set(['external_converters'], ['mock-external-converter-multiple.js']);
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(2);
|
||||
@ -100,7 +120,7 @@ describe('Loads external converters', () => {
|
||||
description: 'external_1',
|
||||
fromZigbee: [],
|
||||
toZigbee: [],
|
||||
exposes: []
|
||||
exposes: [],
|
||||
});
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(2, {
|
||||
mock: 2,
|
||||
@ -110,7 +130,7 @@ describe('Loads external converters', () => {
|
||||
description: 'external_2',
|
||||
fromZigbee: [],
|
||||
toZigbee: [],
|
||||
exposes: []
|
||||
exposes: [],
|
||||
});
|
||||
});
|
||||
|
||||
@ -119,7 +139,7 @@ describe('Loads external converters', () => {
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledWith({
|
||||
mock: true
|
||||
mock: true,
|
||||
});
|
||||
});
|
||||
|
||||
@ -128,18 +148,20 @@ describe('Loads external converters', () => {
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(2);
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(1, {
|
||||
mock: 1
|
||||
mock: 1,
|
||||
});
|
||||
expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(2, {
|
||||
mock: 2
|
||||
mock: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('Loads external converters with error', async () => {
|
||||
fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js'));
|
||||
settings.set(['external_converters'], ['mock-external-converter.js']);
|
||||
zigbeeHerdsmanConverters.addDefinition.mockImplementationOnce(() => {throw new Error('Invalid definition!')});
|
||||
zigbeeHerdsmanConverters.addDefinition.mockImplementationOnce(() => {
|
||||
throw new Error('Invalid definition!');
|
||||
});
|
||||
await resetExtension();
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to load external converter file 'mock-external-converter.js' (Invalid definition!)`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -9,9 +9,15 @@ const Controller = require('../lib/controller');
|
||||
const stringify = require('json-stable-stringify-without-jsonify');
|
||||
const flushPromises = require('./lib/flushPromises');
|
||||
const mocksClear = [
|
||||
zigbeeHerdsman.permitJoin, MQTT.end, zigbeeHerdsman.stop, logger.debug,
|
||||
MQTT.publish, MQTT.connect, zigbeeHerdsman.devices.bulb_color.removeFromNetwork,
|
||||
zigbeeHerdsman.devices.bulb.removeFromNetwork, logger.error,
|
||||
zigbeeHerdsman.permitJoin,
|
||||
MQTT.end,
|
||||
zigbeeHerdsman.stop,
|
||||
logger.debug,
|
||||
MQTT.publish,
|
||||
MQTT.connect,
|
||||
zigbeeHerdsman.devices.bulb_color.removeFromNetwork,
|
||||
zigbeeHerdsman.devices.bulb.removeFromNetwork,
|
||||
logger.error,
|
||||
];
|
||||
|
||||
const fs = require('fs');
|
||||
@ -30,7 +36,7 @@ describe('User extensions', () => {
|
||||
settings.reRead();
|
||||
mocksClear.forEach((m) => m.mockClear());
|
||||
});
|
||||
|
||||
|
||||
afterAll(async () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
@ -46,18 +52,23 @@ describe('User extensions', () => {
|
||||
afterEach(() => {
|
||||
const extensionPath = path.join(data.mockDir, 'extension');
|
||||
rimrafSync(extensionPath);
|
||||
})
|
||||
});
|
||||
|
||||
it('Load user extension', async () => {
|
||||
const extensionPath = path.join(data.mockDir, 'extension');
|
||||
const extensionCode = fs.readFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), 'utf-8');
|
||||
fs.mkdirSync(extensionPath);
|
||||
fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), path.join(extensionPath, 'exampleExtension.js'))
|
||||
fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), path.join(extensionPath, 'exampleExtension.js'));
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', { retain: false, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([{"name": "exampleExtension.js", "code": extensionCode}]), { retain: true, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/extensions',
|
||||
stringify([{name: 'exampleExtension.js', code: extensionCode}]),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Load user extension from api call', async () => {
|
||||
@ -67,53 +78,72 @@ describe('User extensions', () => {
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/extension/save', stringify({"name": "foo.js", "code": extensionCode}));
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'foo.js', code: extensionCode}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([{"name": "foo.js", "code": extensionCode}]), { retain: true, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from constructor', { retain: false, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/extensions',
|
||||
stringify([{name: 'foo.js', code: extensionCode}]),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/example/extension',
|
||||
'call from constructor',
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mkdirSyncSpy).toHaveBeenCalledWith(extensionPath);
|
||||
});
|
||||
|
||||
it('Do not load corrupted extensions', async () => {
|
||||
const extensionPath = path.join(data.mockDir, 'extension');
|
||||
const extensionCode = "definetly not a correct javascript code";
|
||||
const extensionCode = 'definetly not a correct javascript code';
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/extension/save', stringify({"name": "foo.js", "code": extensionCode}));
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'foo.js', code: extensionCode}));
|
||||
await flushPromises();
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/extension/save', expect.any(String), { retain: false, qos: 0 }, expect.any(Function));
|
||||
const payload = JSON.parse(MQTT.publish.mock.calls[0][1]);
|
||||
expect(payload).toEqual(
|
||||
expect.objectContaining({"data":{},"status":"error"})
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/extension/save',
|
||||
expect.any(String),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(payload.error).toMatch("Unexpected identifier");
|
||||
const payload = JSON.parse(MQTT.publish.mock.calls[0][1]);
|
||||
expect(payload).toEqual(expect.objectContaining({data: {}, status: 'error'}));
|
||||
expect(payload.error).toMatch('Unexpected identifier');
|
||||
});
|
||||
|
||||
it('Removes user extension', async () => {
|
||||
const extensionPath = path.join(data.mockDir, 'extension');
|
||||
const extensionCode = fs.readFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), 'utf-8');
|
||||
fs.mkdirSync(extensionPath);
|
||||
const extensionFilePath = path.join(extensionPath, 'exampleExtension.js')
|
||||
fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), extensionFilePath)
|
||||
const extensionFilePath = path.join(extensionPath, 'exampleExtension.js');
|
||||
fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), extensionFilePath);
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', { retain: false, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([{"name": "exampleExtension.js", "code": extensionCode}]), { retain: true, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/extensions',
|
||||
stringify([{name: 'exampleExtension.js', code: extensionCode}]),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({"name": "exampleExtension.js"}));
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: 'exampleExtension.js'}));
|
||||
await flushPromises();
|
||||
expect(unlinkSyncSpy).toHaveBeenCalledWith(extensionFilePath);
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({"name": "non existing.js"}));
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: 'non existing.js'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/extension/remove',
|
||||
stringify({"data":{},"status":"error","error":"Extension non existing.js doesn't exists"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {}, status: 'error', error: "Extension non existing.js doesn't exists"}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -7,13 +7,15 @@ const Controller = require('../lib/controller');
|
||||
const stringify = require('json-stable-stringify-without-jsonify');
|
||||
const flushPromises = require('./lib/flushPromises');
|
||||
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
const path = require("path");
|
||||
const path = require('path');
|
||||
jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||
|
||||
const mockHTTP = {
|
||||
implementation: {
|
||||
listen: jest.fn(),
|
||||
on: (event, handler) => {mockHTTP.events[event] = handler},
|
||||
on: (event, handler) => {
|
||||
mockHTTP.events[event] = handler;
|
||||
},
|
||||
close: jest.fn().mockImplementation((cb) => cb()),
|
||||
},
|
||||
variables: {},
|
||||
@ -23,7 +25,9 @@ const mockHTTP = {
|
||||
const mockHTTPS = {
|
||||
implementation: {
|
||||
listen: jest.fn(),
|
||||
on: (event, handler) => {mockHTTPS.events[event] = handler},
|
||||
on: (event, handler) => {
|
||||
mockHTTPS.events[event] = handler;
|
||||
},
|
||||
close: jest.fn().mockImplementation((cb) => cb()),
|
||||
},
|
||||
variables: {},
|
||||
@ -37,9 +41,11 @@ const mockWSocket = {
|
||||
const mockWS = {
|
||||
implementation: {
|
||||
clients: [],
|
||||
on: (event, handler) => {mockWS.events[event] = handler},
|
||||
on: (event, handler) => {
|
||||
mockWS.events[event] = handler;
|
||||
},
|
||||
handleUpgrade: jest.fn().mockImplementation((request, socket, head, cb) => {
|
||||
cb(mockWSocket)
|
||||
cb(mockWSocket);
|
||||
}),
|
||||
emit: jest.fn(),
|
||||
close: jest.fn(),
|
||||
@ -70,11 +76,11 @@ jest.mock('https', () => ({
|
||||
Agent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("connect-gzip-static", () =>
|
||||
jest.mock('connect-gzip-static', () =>
|
||||
jest.fn().mockImplementation((path) => {
|
||||
mockNodeStatic.variables.path = path
|
||||
return mockNodeStatic.implementation
|
||||
})
|
||||
mockNodeStatic.variables.path = path;
|
||||
return mockNodeStatic.implementation;
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('zigbee2mqtt-frontend', () => ({
|
||||
@ -100,7 +106,7 @@ describe('Frontend', () => {
|
||||
data.writeDefaultConfiguration();
|
||||
data.writeDefaultState();
|
||||
settings.reRead();
|
||||
settings.set(['frontend'], {port: 8081, host: "127.0.0.1"});
|
||||
settings.set(['frontend'], {port: 8081, host: '127.0.0.1'});
|
||||
settings.set(['homeassistant'], true);
|
||||
zigbeeHerdsman.devices.bulb.linkquality = 10;
|
||||
});
|
||||
@ -109,15 +115,15 @@ describe('Frontend', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
afterEach(async() => {
|
||||
afterEach(async () => {
|
||||
delete zigbeeHerdsman.devices.bulb.linkquality;
|
||||
});
|
||||
|
||||
it('Start/stop', async () => {
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockNodeStatic.variables.path).toBe("my/dummy/path");
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
expect(mockNodeStatic.variables.path).toBe('my/dummy/path');
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1');
|
||||
const mockWSClient = {
|
||||
implementation: {
|
||||
terminate: jest.fn(),
|
||||
@ -140,7 +146,7 @@ describe('Frontend', () => {
|
||||
settings.set(['frontend'], {port: 8081});
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockNodeStatic.variables.path).toBe("my/dummy/path");
|
||||
expect(mockNodeStatic.variables.path).toBe('my/dummy/path');
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081);
|
||||
const mockWSClient = {
|
||||
implementation: {
|
||||
@ -161,11 +167,11 @@ describe('Frontend', () => {
|
||||
});
|
||||
|
||||
it('Start/stop unix socket', async () => {
|
||||
settings.set(['frontend'], {host: "/tmp/zigbee2mqtt.sock"});
|
||||
settings.set(['frontend'], {host: '/tmp/zigbee2mqtt.sock'});
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockNodeStatic.variables.path).toBe("my/dummy/path");
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith("/tmp/zigbee2mqtt.sock");
|
||||
expect(mockNodeStatic.variables.path).toBe('my/dummy/path');
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith('/tmp/zigbee2mqtt.sock');
|
||||
const mockWSClient = {
|
||||
implementation: {
|
||||
terminate: jest.fn(),
|
||||
@ -184,42 +190,39 @@ describe('Frontend', () => {
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
});
|
||||
|
||||
|
||||
it('Start/stop HTTPS valid', async () => {
|
||||
settings.set(['frontend','ssl_cert'], path.join(__dirname,'assets','certs','dummy.crt'));
|
||||
settings.set(['frontend','ssl_key'], path.join(__dirname,'assets','certs','dummy.key'));
|
||||
settings.set(['frontend', 'ssl_cert'], path.join(__dirname, 'assets', 'certs', 'dummy.crt'));
|
||||
settings.set(['frontend', 'ssl_key'], path.join(__dirname, 'assets', 'certs', 'dummy.key'));
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockHTTP.implementation.listen).not.toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
expect(mockHTTPS.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
expect(mockHTTP.implementation.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1');
|
||||
expect(mockHTTPS.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1');
|
||||
await controller.stop();
|
||||
mockHTTP.implementation.listen.mockClear();
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
});
|
||||
|
||||
it('Start/stop HTTPS invalid : missing config', async () => {
|
||||
settings.set(['frontend','ssl_cert'], path.join(__dirname,'assets','certs','dummy.crt'));
|
||||
settings.set(['frontend', 'ssl_cert'], path.join(__dirname, 'assets', 'certs', 'dummy.crt'));
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1');
|
||||
expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1');
|
||||
await controller.stop();
|
||||
mockHTTP.implementation.listen.mockClear();
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
|
||||
});
|
||||
|
||||
it('Start/stop HTTPS invalid : missing file', async () => {
|
||||
settings.set(['frontend','ssl_cert'], 'filesNotExists.crt');
|
||||
settings.set(['frontend','ssl_key'], path.join(__dirname,'assets','certs','dummy.key'));
|
||||
settings.set(['frontend', 'ssl_cert'], 'filesNotExists.crt');
|
||||
settings.set(['frontend', 'ssl_key'], path.join(__dirname, 'assets', 'certs', 'dummy.key'));
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1');
|
||||
expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1');
|
||||
await controller.stop();
|
||||
mockHTTP.implementation.listen.mockClear();
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
|
||||
});
|
||||
|
||||
it('Websocket interaction', async () => {
|
||||
@ -229,7 +232,9 @@ describe('Frontend', () => {
|
||||
// Connect
|
||||
const mockWSClient = {
|
||||
implementation: {
|
||||
on: (event, handler) => {mockWSClient.events[event] = handler},
|
||||
on: (event, handler) => {
|
||||
mockWSClient.events[event] = handler;
|
||||
},
|
||||
send: jest.fn(),
|
||||
readyState: 'open',
|
||||
},
|
||||
@ -239,22 +244,28 @@ describe('Frontend', () => {
|
||||
await mockWS.events.connection(mockWSClient.implementation);
|
||||
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: 'online'}));
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic:"remote", payload:{brightness:255}}));
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'remote', payload: {brightness: 255}}));
|
||||
|
||||
// Message
|
||||
MQTT.publish.mockClear();
|
||||
mockWSClient.implementation.send.mockClear();
|
||||
mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false)
|
||||
mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({state: 'ON', power_on_behavior:null, linkquality: null, update_available: null, update: {state: null, installed_version: -1, latest_version: -1}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function)
|
||||
stringify({
|
||||
state: 'ON',
|
||||
power_on_behavior: null,
|
||||
linkquality: null,
|
||||
update_available: null,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
mockWSClient.events.message(undefined, false);
|
||||
mockWSClient.events.message("", false);
|
||||
mockWSClient.events.message('', false);
|
||||
mockWSClient.events.message(null, false);
|
||||
await flushPromises();
|
||||
|
||||
@ -264,12 +275,23 @@ describe('Frontend', () => {
|
||||
|
||||
// Received message on socket
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(1);
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', power_on_behavior:null, linkquality: null, update_available: null, update: {state: null, installed_version: -1, latest_version: -1}}}));
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(
|
||||
stringify({
|
||||
topic: 'bulb_color',
|
||||
payload: {
|
||||
state: 'ON',
|
||||
power_on_behavior: null,
|
||||
linkquality: null,
|
||||
update_available: null,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Shouldnt set when not ready
|
||||
mockWSClient.implementation.send.mockClear();
|
||||
mockWSClient.implementation.readyState = 'close';
|
||||
mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false)
|
||||
mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false);
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Send last seen on connect
|
||||
@ -278,7 +300,9 @@ describe('Frontend', () => {
|
||||
settings.set(['advanced'], {last_seen: 'ISO_8601'});
|
||||
mockWS.implementation.clients.push(mockWSClient.implementation);
|
||||
await mockWS.events.connection(mockWSClient.implementation);
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic:"remote", payload:{brightness:255, last_seen: "1970-01-01T00:00:01.000Z"}}));
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(
|
||||
stringify({topic: 'remote', payload: {brightness: 255, last_seen: '1970-01-01T00:00:01.000Z'}}),
|
||||
);
|
||||
});
|
||||
|
||||
it('onReques/onUpgrade', async () => {
|
||||
@ -290,9 +314,9 @@ describe('Frontend', () => {
|
||||
mockHTTP.events.upgrade({url: 'http://localhost:8080/api'}, mockSocket, 3);
|
||||
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1);
|
||||
expect(mockSocket.destroy).toHaveBeenCalledTimes(0);
|
||||
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({"url": "http://localhost:8080/api"}, mockSocket, 3, expect.any(Function));
|
||||
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({url: 'http://localhost:8080/api'}, mockSocket, 3, expect.any(Function));
|
||||
mockWS.implementation.handleUpgrade.mock.calls[0][3](99);
|
||||
expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', 99, {"url": "http://localhost:8080/api"});
|
||||
expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', 99, {url: 'http://localhost:8080/api'});
|
||||
|
||||
mockHTTP.variables.onRequest(1, 2);
|
||||
expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1);
|
||||
@ -303,11 +327,11 @@ describe('Frontend', () => {
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1');
|
||||
});
|
||||
|
||||
it('Authentification', async () => {
|
||||
const authToken = 'sample-secure-token'
|
||||
const authToken = 'sample-secure-token';
|
||||
settings.set(['frontend'], {auth_token: authToken});
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
@ -317,8 +341,8 @@ describe('Frontend', () => {
|
||||
mockHTTP.events.upgrade({url: '/api'}, mockSocket, mockWSocket);
|
||||
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1);
|
||||
expect(mockSocket.destroy).toHaveBeenCalledTimes(0);
|
||||
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({"url": "/api"}, mockSocket, mockWSocket, expect.any(Function));
|
||||
expect(mockWSocket.close).toHaveBeenCalledWith(4401, "Unauthorized");
|
||||
expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({url: '/api'}, mockSocket, mockWSocket, expect.any(Function));
|
||||
expect(mockWSocket.close).toHaveBeenCalledWith(4401, 'Unauthorized');
|
||||
|
||||
mockWSocket.close.mockClear();
|
||||
mockWS.implementation.emit.mockClear();
|
||||
@ -332,6 +356,5 @@ describe('Frontend', () => {
|
||||
expect(mockWSocket.close).toHaveBeenCalledTimes(0);
|
||||
mockWS.implementation.handleUpgrade.mock.calls[0][3](mockWSocket);
|
||||
expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', mockWSocket, {url});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -22,7 +22,7 @@ describe('Groups', () => {
|
||||
let resetExtension = async () => {
|
||||
await controller.enableDisableExtension(false, 'Groups');
|
||||
await controller.enableDisableExtension(true, 'Groups');
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers();
|
||||
@ -36,22 +36,22 @@ describe('Groups', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
Object.values(zigbeeHerdsman.groups).forEach((g) => g.members = []);
|
||||
Object.values(zigbeeHerdsman.groups).forEach((g) => (g.members = []));
|
||||
data.writeDefaultConfiguration();
|
||||
settings.reRead();
|
||||
MQTT.publish.mockClear();
|
||||
zigbeeHerdsman.groups.gledopto_group.command.mockClear();
|
||||
zigbeeHerdsmanConverters.toZigbee.__clearStore__();
|
||||
controller.state.state = {};
|
||||
})
|
||||
});
|
||||
|
||||
it('Apply group updates add', async () => {
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['bulb', 'bulb_color']}});
|
||||
zigbeeHerdsman.groups.group_1.members.push(zigbeeHerdsman.devices.bulb.getEndpoint(1))
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['bulb', 'bulb_color']}});
|
||||
zigbeeHerdsman.groups.group_1.members.push(zigbeeHerdsman.devices.bulb.getEndpoint(1));
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([
|
||||
zigbeeHerdsman.devices.bulb.getEndpoint(1),
|
||||
zigbeeHerdsman.devices.bulb_color.getEndpoint(1)
|
||||
zigbeeHerdsman.devices.bulb_color.getEndpoint(1),
|
||||
]);
|
||||
});
|
||||
|
||||
@ -59,17 +59,19 @@ describe('Groups', () => {
|
||||
const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
|
||||
const group = zigbeeHerdsman.groups.group_1;
|
||||
group.members.push(endpoint);
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false,}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false}});
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('Apply group updates remove handle fail', async () => {
|
||||
const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
|
||||
endpoint.removeFromGroup.mockImplementationOnce(() => {throw new Error("failed!")});
|
||||
endpoint.removeFromGroup.mockImplementationOnce(() => {
|
||||
throw new Error('failed!');
|
||||
});
|
||||
const group = zigbeeHerdsman.groups.group_1;
|
||||
group.members.push(endpoint);
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false,}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false}});
|
||||
logger.error.mockClear();
|
||||
await resetExtension();
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to remove 'bulb_color' from 'group_1'`);
|
||||
@ -81,28 +83,28 @@ describe('Groups', () => {
|
||||
const endpoint = device.getEndpoint(1);
|
||||
const group = zigbeeHerdsman.groups.group_1;
|
||||
group.members.push(endpoint);
|
||||
settings.set(['groups'], {'3': {friendly_name: 'group_3', retain: false, devices: [device.ieeeAddr]}});
|
||||
settings.set(['groups'], {3: {friendly_name: 'group_3', retain: false, devices: [device.ieeeAddr]}});
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('Add non standard endpoint to group with name', async () => {
|
||||
const QBKG03LM = zigbeeHerdsman.devices.QBKG03LM;
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['0x0017880104e45542/right']}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['0x0017880104e45542/right']}});
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(3)]);
|
||||
});
|
||||
|
||||
it('Add non standard endpoint to group with number', async () => {
|
||||
const QBKG03LM = zigbeeHerdsman.devices.QBKG03LM;
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['wall_switch_double/2']}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['wall_switch_double/2']}});
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(2)]);
|
||||
});
|
||||
|
||||
it('Shouldnt crash on non-existing devices', async () => {
|
||||
logger.error.mockClear();
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['not_existing_bla']}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['not_existing_bla']}});
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]);
|
||||
expect(logger.error).toHaveBeenCalledWith("Cannot find 'not_existing_bla' of group 'group_1'");
|
||||
@ -110,11 +112,11 @@ describe('Groups', () => {
|
||||
|
||||
it('Should resolve device friendly names', async () => {
|
||||
settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'friendly_name'], 'bulb_friendly_name');
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: ['bulb_friendly_name', 'bulb_color']}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['bulb_friendly_name', 'bulb_color']}});
|
||||
await resetExtension();
|
||||
expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([
|
||||
zigbeeHerdsman.devices.bulb.getEndpoint(1),
|
||||
zigbeeHerdsman.devices.bulb_color.getEndpoint(1)
|
||||
zigbeeHerdsman.devices.bulb_color.getEndpoint(1),
|
||||
]);
|
||||
});
|
||||
|
||||
@ -122,28 +124,38 @@ describe('Groups', () => {
|
||||
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: []}});
|
||||
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));
|
||||
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: []}});
|
||||
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));
|
||||
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 () => {
|
||||
@ -177,13 +189,18 @@ describe('Groups', () => {
|
||||
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]}});
|
||||
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));
|
||||
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 () => {
|
||||
@ -191,7 +208,7 @@ describe('Groups', () => {
|
||||
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']}});
|
||||
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();
|
||||
@ -204,7 +221,7 @@ describe('Groups', () => {
|
||||
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`]}});
|
||||
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();
|
||||
@ -217,7 +234,7 @@ describe('Groups', () => {
|
||||
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`]}});
|
||||
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();
|
||||
@ -230,7 +247,7 @@ describe('Groups', () => {
|
||||
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`]}});
|
||||
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();
|
||||
@ -243,13 +260,18 @@ describe('Groups', () => {
|
||||
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`]}});
|
||||
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));
|
||||
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 () => {
|
||||
@ -257,7 +279,7 @@ describe('Groups', () => {
|
||||
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`]}});
|
||||
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();
|
||||
@ -294,7 +316,7 @@ describe('Groups', () => {
|
||||
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]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}});
|
||||
await resetExtension();
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
@ -303,8 +325,8 @@ describe('Groups', () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should not republish identical optimistic group states', async () => {
|
||||
@ -314,16 +336,55 @@ describe('Groups', () => {
|
||||
await resetExtension();
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
await zigbeeHerdsman.events.message({data: {onOff: 1}, cluster: 'genOnOff', device: device1, endpoint: device1.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await zigbeeHerdsman.events.message({data: {onOff: 1}, cluster: 'genOnOff', device: device2, endpoint: device2.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await zigbeeHerdsman.events.message({
|
||||
data: {onOff: 1},
|
||||
cluster: 'genOnOff',
|
||||
device: device1,
|
||||
endpoint: device1.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
});
|
||||
await zigbeeHerdsman.events.message({
|
||||
data: {onOff: 1},
|
||||
cluster: 'genOnOff',
|
||||
device: device2,
|
||||
endpoint: device2.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(6);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_tradfri_remote", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_2", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color_2", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_with_tradfri", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/ha_discovery_group", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/switch_group", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/group_tradfri_remote',
|
||||
stringify({state: 'ON'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_2', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color_2',
|
||||
stringify({state: 'ON'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/group_with_tradfri',
|
||||
stringify({state: 'ON'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/ha_discovery_group',
|
||||
stringify({state: 'ON'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/switch_group',
|
||||
stringify({state: 'ON'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should publish state change of all members when a group changes its state', async () => {
|
||||
@ -331,15 +392,15 @@ describe('Groups', () => {
|
||||
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]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}});
|
||||
await resetExtension();
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should not publish state change when group changes state and device is disabled', async () => {
|
||||
@ -348,14 +409,14 @@ describe('Groups', () => {
|
||||
const group = zigbeeHerdsman.groups.group_1;
|
||||
group.members.push(endpoint);
|
||||
settings.set(['devices', device.ieeeAddr, 'disabled'], true);
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}});
|
||||
await resetExtension();
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should publish state change for group when members state change', async () => {
|
||||
@ -364,29 +425,29 @@ describe('Groups', () => {
|
||||
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]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}});
|
||||
await resetExtension();
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should publish state of device with endpoint name', async () => {
|
||||
@ -397,10 +458,20 @@ describe('Groups', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/gledopto_group/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/GLEDOPTO_2ID", stringify({"state_cct":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/gledopto_group", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/GLEDOPTO_2ID',
|
||||
stringify({state_cct: 'ON'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/gledopto_group',
|
||||
stringify({state: 'ON'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(group.command).toHaveBeenCalledTimes(1);
|
||||
expect(group.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
|
||||
expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {});
|
||||
});
|
||||
|
||||
it('Should publish state of group when specific state of specific endpoint is changed', async () => {
|
||||
@ -411,8 +482,18 @@ describe('Groups', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/GLEDOPTO_2ID/set', stringify({state_cct: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/GLEDOPTO_2ID", stringify({"state_cct":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/gledopto_group", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/GLEDOPTO_2ID',
|
||||
stringify({state_cct: 'ON'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/gledopto_group',
|
||||
stringify({state: 'ON'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(group.command).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
@ -421,15 +502,20 @@ describe('Groups', () => {
|
||||
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, filtered_attributes: ['brightness'], devices: [device.ieeeAddr]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, filtered_attributes: ['brightness'], devices: [device.ieeeAddr]}});
|
||||
await resetExtension();
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 100}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON","brightness":100}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({state: 'ON', brightness: 100}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Shouldnt publish group state change when a group is not optimistic', async () => {
|
||||
@ -437,7 +523,7 @@ describe('Groups', () => {
|
||||
const endpoint = device.getEndpoint(1);
|
||||
const group = zigbeeHerdsman.groups.group_1;
|
||||
group.members.push(endpoint);
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: [device.ieeeAddr], optimistic: false, retain: false}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', devices: [device.ieeeAddr], optimistic: false, retain: false}});
|
||||
await resetExtension();
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
@ -446,7 +532,7 @@ describe('Groups', () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should publish state change of another group with shared device when a group changes its state', async () => {
|
||||
@ -455,9 +541,9 @@ describe('Groups', () => {
|
||||
const group = zigbeeHerdsman.groups.group_1;
|
||||
group.members.push(endpoint);
|
||||
settings.set(['groups'], {
|
||||
'1': {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]},
|
||||
'2': {friendly_name: 'group_2', retain: false, devices: [device.ieeeAddr]},
|
||||
'3': {friendly_name: 'group_3', retain: false, devices: []}
|
||||
1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]},
|
||||
2: {friendly_name: 'group_2', retain: false, devices: [device.ieeeAddr]},
|
||||
3: {friendly_name: 'group_3', retain: false, devices: []},
|
||||
});
|
||||
await resetExtension();
|
||||
|
||||
@ -465,9 +551,9 @@ describe('Groups', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_2", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should not publish state change off if any lights within are still on when changed via device', async () => {
|
||||
@ -479,7 +565,7 @@ describe('Groups', () => {
|
||||
group.members.push(endpoint_1);
|
||||
group.members.push(endpoint_2);
|
||||
settings.set(['groups'], {
|
||||
'1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}
|
||||
1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false},
|
||||
});
|
||||
await resetExtension();
|
||||
|
||||
@ -490,7 +576,7 @@ describe('Groups', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should publish state change off if any lights within are still on when changed via device when off_state: last_member_state is used', async () => {
|
||||
@ -502,7 +588,7 @@ describe('Groups', () => {
|
||||
group.members.push(endpoint_1);
|
||||
group.members.push(endpoint_2);
|
||||
settings.set(['groups'], {
|
||||
'1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false, off_state: 'last_member_state'}
|
||||
1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false, off_state: 'last_member_state'},
|
||||
});
|
||||
await resetExtension();
|
||||
|
||||
@ -513,8 +599,20 @@ describe('Groups', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(1, "zigbee2mqtt/group_1", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(2, "zigbee2mqtt/bulb_color", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'zigbee2mqtt/group_1',
|
||||
stringify({state: 'OFF'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({state: 'OFF'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should not publish state change off if any lights within are still on when changed via shared group', async () => {
|
||||
@ -526,8 +624,8 @@ describe('Groups', () => {
|
||||
group.members.push(endpoint_1);
|
||||
group.members.push(endpoint_2);
|
||||
settings.set(['groups'], {
|
||||
'1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false},
|
||||
'2': {friendly_name: 'group_2', retain: false, devices: [device_1.ieeeAddr]},
|
||||
1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false},
|
||||
2: {friendly_name: 'group_2', retain: false, devices: [device_1.ieeeAddr]},
|
||||
});
|
||||
await resetExtension();
|
||||
|
||||
@ -538,8 +636,8 @@ describe('Groups', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/group_2/set', stringify({state: 'OFF'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_2", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should publish state change off if all lights within turn off', async () => {
|
||||
@ -551,7 +649,7 @@ describe('Groups', () => {
|
||||
group.members.push(endpoint_1);
|
||||
group.members.push(endpoint_2);
|
||||
settings.set(['groups'], {
|
||||
'1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}
|
||||
1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false},
|
||||
});
|
||||
await resetExtension();
|
||||
|
||||
@ -563,9 +661,9 @@ describe('Groups', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bulb/set', stringify({state: 'OFF'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"OFF"}), {"retain": true, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'OFF'}), {retain: true, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should only update group state with changed properties', async () => {
|
||||
@ -577,7 +675,7 @@ describe('Groups', () => {
|
||||
group.members.push(endpoint_1);
|
||||
group.members.push(endpoint_2);
|
||||
settings.set(['groups'], {
|
||||
'1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}
|
||||
1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false},
|
||||
});
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
@ -590,9 +688,24 @@ describe('Groups', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 300}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"color_mode": "color_temp","color_temp":300,"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"color_mode": "color_temp","color_temp":300,"state":"ON"}), {"retain": true, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"color_mode": "color_temp","color_temp":300,"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({color_mode: 'color_temp', color_temp: 300, state: 'OFF'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/group_1',
|
||||
stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should publish state change off even when missing current state', async () => {
|
||||
@ -604,7 +717,7 @@ describe('Groups', () => {
|
||||
group.members.push(endpoint_1);
|
||||
group.members.push(endpoint_2);
|
||||
settings.set(['groups'], {
|
||||
'1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}
|
||||
1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false},
|
||||
});
|
||||
await resetExtension();
|
||||
|
||||
@ -617,27 +730,28 @@ describe('Groups', () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('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: []}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}});
|
||||
expect(group.members.length).toBe(0);
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({transaction: "123", group: 'group_1', device: 'bulb_color'}));
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({transaction: '123', group: 'group_1', device: '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/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/add',
|
||||
stringify({"data":{"device":"bulb_color","group":"group_1"},"transaction": "123", "status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: 'bulb_color', group: 'group_1'}, transaction: '123', status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -645,10 +759,12 @@ describe('Groups', () => {
|
||||
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: []}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}});
|
||||
expect(group.members.length).toBe(0);
|
||||
await resetExtension();
|
||||
endpoint.addToGroup.mockImplementationOnce(() => {throw new Error('timeout')});
|
||||
endpoint.addToGroup.mockImplementationOnce(() => {
|
||||
throw new Error('timeout');
|
||||
});
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color'}));
|
||||
@ -658,16 +774,17 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/add',
|
||||
stringify({"data":{"device":"bulb_color","group":"group_1"},"status":"error","error":"Failed to add from group (timeout)"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'error', error: 'Failed to add from group (timeout)'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('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: []}});
|
||||
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.publish.mockClear();
|
||||
@ -678,8 +795,9 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/add',
|
||||
stringify({"data":{"device":"bulb_color","group":"group/with/slashes"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: 'bulb_color', group: 'group/with/slashes'}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -697,8 +815,9 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/add',
|
||||
stringify({"data":{"device":"wall_switch_double/right","group":"group_1"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: 'wall_switch_double/right', group: 'group_1'}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -718,8 +837,9 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/add',
|
||||
stringify({"data":{"device":"wall_switch_double/right","group":"group_1"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: 'wall_switch_double/right', group: 'group_1'}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -728,7 +848,7 @@ describe('Groups', () => {
|
||||
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]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}});
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'}));
|
||||
@ -738,8 +858,9 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/remove',
|
||||
stringify({"data":{"device":"bulb_color","group":"group_1"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -748,18 +869,22 @@ describe('Groups', () => {
|
||||
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]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}});
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color', skip_disable_reporting: true}));
|
||||
MQTT.events.message(
|
||||
'zigbee2mqtt/bridge/request/group/members/remove',
|
||||
stringify({group: 'group_1', device: 'bulb_color', skip_disable_reporting: true}),
|
||||
);
|
||||
await flushPromises();
|
||||
expect(group.members).toStrictEqual([]);
|
||||
expect(settings.getGroup('group_1').devices).toStrictEqual([]);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/remove',
|
||||
stringify({"data":{"device":"bulb_color","group":"group_1"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -768,7 +893,7 @@ describe('Groups', () => {
|
||||
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']}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['dummy']}});
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'}));
|
||||
@ -778,8 +903,9 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/remove',
|
||||
stringify({"data":{"device":"bulb_color","group":"group_1"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -788,7 +914,7 @@ describe('Groups', () => {
|
||||
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`]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/right`]}});
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/3'}));
|
||||
@ -798,8 +924,9 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/remove',
|
||||
stringify({"data":{"device":"0x0017880104e45542/3","group":"group_1"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: '0x0017880104e45542/3', group: 'group_1'}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -808,7 +935,7 @@ describe('Groups', () => {
|
||||
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`]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`0x0017880104e45542/right`]}});
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'wall_switch_double/3'}));
|
||||
@ -818,8 +945,9 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/remove',
|
||||
stringify({"data":{"device":"wall_switch_double/3","group":"group_1"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: 'wall_switch_double/3', group: 'group_1'}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -828,7 +956,7 @@ describe('Groups', () => {
|
||||
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`]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}});
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/right'}));
|
||||
@ -838,8 +966,9 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/remove',
|
||||
stringify({"data":{"device":"0x0017880104e45542/right","group":"group_1"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: '0x0017880104e45542/right', group: 'group_1'}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -848,7 +977,7 @@ describe('Groups', () => {
|
||||
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`]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}});
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove_all', stringify({device: '0x0017880104e45542/right'}));
|
||||
@ -858,8 +987,9 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/remove_all',
|
||||
stringify({"data":{"device":"0x0017880104e45542/right"},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {device: '0x0017880104e45542/right'}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -872,8 +1002,13 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/remove',
|
||||
stringify({"data":{"device":"bulb_color","group":"group_1_not_existing"},"status":"error","error":"Group 'group_1_not_existing' does not exist"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
data: {device: 'bulb_color', group: 'group_1_not_existing'},
|
||||
status: 'error',
|
||||
error: "Group 'group_1_not_existing' does not exist",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -886,8 +1021,13 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/add',
|
||||
stringify({"data":{"device":"bulb_color_not_existing","group":"group_1"},"status":"error","error":"Device 'bulb_color_not_existing' does not exist"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
data: {device: 'bulb_color_not_existing', group: 'group_1'},
|
||||
status: 'error',
|
||||
error: "Device 'bulb_color_not_existing' does not exist",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -895,13 +1035,21 @@ describe('Groups', () => {
|
||||
await resetExtension();
|
||||
logger.error.mockClear();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color/not_existing_endpoint'}));
|
||||
MQTT.events.message(
|
||||
'zigbee2mqtt/bridge/request/group/members/add',
|
||||
stringify({group: 'group_1', device: 'bulb_color/not_existing_endpoint'}),
|
||||
);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/group/members/add',
|
||||
stringify({"data":{"device":"bulb_color/not_existing_endpoint","group":"group_1"},"status":"error","error":"Device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
data: {device: 'bulb_color/not_existing_endpoint', group: 'group_1'},
|
||||
status: 'error',
|
||||
error: "Device 'bulb_color' does not have endpoint 'not_existing_endpoint'",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -911,23 +1059,53 @@ describe('Groups', () => {
|
||||
const group = zigbeeHerdsman.groups.group_1;
|
||||
group.members.push(bulbColor.getEndpoint(1));
|
||||
group.members.push(bulbColorTemp.getEndpoint(1));
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [bulbColor.ieeeAddr, bulbColorTemp.ieeeAddr]}});
|
||||
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [bulbColor.ieeeAddr, bulbColorTemp.ieeeAddr]}});
|
||||
await resetExtension();
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 50}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"color_mode":"color_temp","color_temp":50}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"color_mode":"color_temp","color_temp":50}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"color_mode":"color_temp","color_temp":50}), {"retain": true, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({color_mode: 'color_temp', color_temp: 50}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/group_1',
|
||||
stringify({color_mode: 'color_temp', color_temp: 50}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({color_mode: 'color_temp', color_temp: 50}),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.5, y: 0.3}}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({"color":{"x":0.5,"y":0.3},"color_mode":"xy","color_temp":548}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"color":{"x":0.5,"y":0.3},"color_mode":"xy","color_temp":548}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"color_mode":"color_temp","color_temp":548}), {"retain": true, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({color: {x: 0.5, y: 0.3}, color_mode: 'xy', color_temp: 548}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/group_1',
|
||||
stringify({color: {x: 0.5, y: 0.3}, color_mode: 'xy', color_temp: 548}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({color_mode: 'color_temp', color_temp: 548}),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,6 @@ const settings = require('../../lib/util/settings');
|
||||
const Controller = require('../../lib/controller');
|
||||
const flushPromises = require('../lib/flushPromises');
|
||||
|
||||
|
||||
describe('Bridge legacy', () => {
|
||||
let controller;
|
||||
let version;
|
||||
@ -19,7 +18,7 @@ describe('Bridge legacy', () => {
|
||||
version = await require('../../lib/util/utils').default.getZigbee2MQTTVersion();
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
})
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
data.writeDefaultConfiguration();
|
||||
@ -35,9 +34,16 @@ describe('Bridge legacy', () => {
|
||||
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)
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
||||
@ -62,23 +68,23 @@ describe('Bridge legacy', () => {
|
||||
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)
|
||||
stringify({type: 'device_whitelisted', message: {friendly_name: 'bulb_color'}}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
MQTT.publish.mockClear()
|
||||
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)
|
||||
stringify({type: 'device_whitelisted', message: {friendly_name: 'bulb'}}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
MQTT.publish.mockClear()
|
||||
MQTT.publish.mockClear();
|
||||
expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb');
|
||||
await flushPromises();
|
||||
@ -88,39 +94,48 @@ describe('Bridge legacy', () => {
|
||||
|
||||
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}
|
||||
);
|
||||
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}
|
||||
);
|
||||
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}
|
||||
);
|
||||
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}
|
||||
);
|
||||
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}
|
||||
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}}}),
|
||||
);
|
||||
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}}
|
||||
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}}}),
|
||||
);
|
||||
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}}
|
||||
);
|
||||
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 () => {
|
||||
@ -142,7 +157,9 @@ describe('Bridge legacy', () => {
|
||||
await flushPromises();
|
||||
expect(zigbeeHerdsman.reset).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.reset).toHaveBeenCalledWith('soft');
|
||||
zigbeeHerdsman.reset.mockImplementationOnce(() => {throw new Error('')});
|
||||
zigbeeHerdsman.reset.mockImplementationOnce(() => {
|
||||
throw new Error('');
|
||||
});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/reset', '');
|
||||
await flushPromises();
|
||||
expect(zigbeeHerdsman.reset).toHaveBeenCalledTimes(2);
|
||||
@ -182,8 +199,30 @@ describe('Bridge legacy', () => {
|
||||
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"});
|
||||
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;
|
||||
});
|
||||
|
||||
@ -193,13 +232,25 @@ describe('Bridge legacy', () => {
|
||||
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"});
|
||||
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};
|
||||
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});
|
||||
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')).toStrictEqual(null);
|
||||
@ -208,7 +259,7 @@ describe('Bridge legacy', () => {
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'device_renamed', message: {from: 'bulb_color', to: 'bulb_color2'}}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'bulb_color2', newmalformed: 'bulb_color3'}));
|
||||
@ -226,15 +277,15 @@ describe('Bridge legacy', () => {
|
||||
|
||||
it('Should allow rename groups', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
expect(settings.getGroup(1)).toStrictEqual({"ID": 1, devices: [], "friendly_name": "group_1", retain: false});
|
||||
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(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)
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -251,7 +302,7 @@ describe('Bridge legacy', () => {
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'device_renamed', message: {from: 'bulb', to: 'bulb_new_name'}}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -273,9 +324,9 @@ describe('Bridge legacy', () => {
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'group_added', message: 'new_group'}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(settings.getGroup('new_group')).toStrictEqual({"ID": 3, "friendly_name": "new_group", devices: []});
|
||||
expect(settings.getGroup('new_group')).toStrictEqual({ID: 3, friendly_name: 'new_group', devices: []});
|
||||
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3);
|
||||
});
|
||||
@ -284,14 +335,14 @@ describe('Bridge legacy', () => {
|
||||
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(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)
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -299,14 +350,14 @@ describe('Bridge legacy', () => {
|
||||
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(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)
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -314,9 +365,9 @@ describe('Bridge legacy', () => {
|
||||
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(settings.getGroup('group_42')).toStrictEqual({ID: 42, friendly_name: 'group_42', devices: []});
|
||||
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(42)
|
||||
expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(42);
|
||||
});
|
||||
|
||||
it('Should allow to remove groups', async () => {
|
||||
@ -329,7 +380,7 @@ describe('Bridge legacy', () => {
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'group_removed', message: 'group_1'}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -343,7 +394,7 @@ describe('Bridge legacy', () => {
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'group_removed', message: 'group_1'}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -378,7 +429,7 @@ describe('Bridge legacy', () => {
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'device_removed', message: 'bulb_color'}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(controller.state.state).toStrictEqual({});
|
||||
expect(settings.get().blocklist.length).toBe(0);
|
||||
@ -400,7 +451,7 @@ describe('Bridge legacy', () => {
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'device_force_removed', message: 'bulb_color'}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(controller.state.state).toStrictEqual({});
|
||||
expect(settings.get().blocklist.length).toBe(0);
|
||||
@ -421,7 +472,7 @@ describe('Bridge legacy', () => {
|
||||
'zigbee2mqtt/bridge/log',
|
||||
stringify({type: 'device_banned', message: 'bulb_color'}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function)
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(settings.get().blocklist).toStrictEqual(['0x000b57fffec6a5b3']);
|
||||
});
|
||||
@ -436,28 +487,32 @@ describe('Bridge legacy', () => {
|
||||
it('Should handle when remove fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
device.removeFromNetwork.mockClear();
|
||||
device.removeFromNetwork.mockImplementationOnce(() => {throw new Error('')})
|
||||
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(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('')})
|
||||
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(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
@ -32,12 +32,22 @@ describe('Report', () => {
|
||||
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}]);
|
||||
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}]);
|
||||
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}]);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [
|
||||
{attribute: 'colorTemperature', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,12 +61,22 @@ describe('Report', () => {
|
||||
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}]);
|
||||
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}]);
|
||||
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}]);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [
|
||||
{attribute: 'colorTemperature', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +88,7 @@ describe('Report', () => {
|
||||
endpoint.bind.mockClear();
|
||||
endpoint.unbind.mockClear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers();
|
||||
@ -130,7 +150,7 @@ describe('Report', () => {
|
||||
device.save.mockClear();
|
||||
mockClear(device);
|
||||
delete device.meta.reporting;
|
||||
const data = {onOff: 1}
|
||||
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();
|
||||
@ -155,7 +175,9 @@ describe('Report', () => {
|
||||
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')});
|
||||
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};
|
||||
@ -183,7 +205,7 @@ describe('Report', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
mockClear(device);
|
||||
const data = {onOff: 1}
|
||||
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();
|
||||
@ -233,7 +255,9 @@ describe('Report', () => {
|
||||
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')});
|
||||
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};
|
||||
@ -281,8 +305,10 @@ describe('Report', () => {
|
||||
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.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;
|
||||
});
|
||||
|
@ -30,8 +30,7 @@ describe('Logger', () => {
|
||||
consoleWriteSpy.mockClear();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
});
|
||||
afterEach(async () => {});
|
||||
|
||||
it('Create log directory', () => {
|
||||
const dirs = fs.readdirSync(dir.name);
|
||||
@ -50,7 +49,7 @@ describe('Logger', () => {
|
||||
expect(fs.readdirSync(dir.name).length).toBe(21);
|
||||
logger.cleanup();
|
||||
expect(fs.readdirSync(dir.name).length).toBe(10);
|
||||
})
|
||||
});
|
||||
|
||||
it('Should not cleanup when there is no timestamp set', () => {
|
||||
for (let i = 30; i < 40; i++) {
|
||||
@ -61,7 +60,7 @@ describe('Logger', () => {
|
||||
expect(fs.readdirSync(dir.name).length).toBe(21);
|
||||
logger.cleanup();
|
||||
expect(fs.readdirSync(dir.name).length).toBe(21);
|
||||
})
|
||||
});
|
||||
|
||||
it('Set and get log level', () => {
|
||||
logger.setLevel('debug');
|
||||
@ -82,8 +81,7 @@ describe('Logger', () => {
|
||||
|
||||
it('Add/remove transport', () => {
|
||||
class DummyTransport extends Transport {
|
||||
log(info, callback) {
|
||||
}
|
||||
log(info, callback) {}
|
||||
}
|
||||
|
||||
expect(logger.winston.transports.length).toBe(2);
|
||||
@ -143,7 +141,7 @@ describe('Logger', () => {
|
||||
it('Should allow to symlink logs to current directory', () => {
|
||||
settings.set(['advanced', 'log_symlink_current'], true);
|
||||
logger.init();
|
||||
expect(fs.readdirSync(dir.name).includes('current')).toBeTruthy()
|
||||
expect(fs.readdirSync(dir.name).includes('current')).toBeTruthy();
|
||||
|
||||
jest.resetModules();
|
||||
});
|
||||
@ -192,51 +190,51 @@ describe('Logger', () => {
|
||||
it('Logs Error object', () => {
|
||||
const logSpy = jest.spyOn(logger.winston, 'log');
|
||||
|
||||
logger.error(new Error('msg'));// test for stack=true
|
||||
logger.error(new Error('msg')); // test for stack=true
|
||||
expect(logSpy).toHaveBeenLastCalledWith('error', `z2m: ${new Error('msg')}`);
|
||||
expect(consoleWriteSpy).toHaveBeenCalledTimes(1);
|
||||
})
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
'^zhc:legacy:fz:(tuya|moes)',
|
||||
new RegExp(/^zhc:legacy:fz:(tuya|moes)/),
|
||||
[
|
||||
{ ns: 'zhc:legacy:fz:tuya_device12', match: true },
|
||||
{ ns: 'zhc:legacy:fz:moes_dimmer', match: true },
|
||||
{ ns: 'zhc:legacy:fz:not_moes', match: false },
|
||||
{ ns: 'zhc:legacy:fz', match: false },
|
||||
{ ns: 'zhc:legacy:fz:', match: false },
|
||||
{ ns: '1zhc:legacy:fz:tuya_device12', match: false },
|
||||
]
|
||||
{ns: 'zhc:legacy:fz:tuya_device12', match: true},
|
||||
{ns: 'zhc:legacy:fz:moes_dimmer', match: true},
|
||||
{ns: 'zhc:legacy:fz:not_moes', match: false},
|
||||
{ns: 'zhc:legacy:fz', match: false},
|
||||
{ns: 'zhc:legacy:fz:', match: false},
|
||||
{ns: '1zhc:legacy:fz:tuya_device12', match: false},
|
||||
],
|
||||
],
|
||||
[
|
||||
'^zhc:legacy:fz:(tuya|moes)|^zh:ember:uart:|^zh:controller',
|
||||
new RegExp(/^zhc:legacy:fz:(tuya|moes)|^zh:ember:uart:|^zh:controller/),
|
||||
[
|
||||
{ ns: 'zh:ember:uart:ash', match: true },
|
||||
{ ns: 'zh:ember:uart', match: false },
|
||||
{ ns: 'zh:controller', match: true },
|
||||
{ ns: 'zh:controller:', match: true },
|
||||
{ ns: 'azh:controller:', match: false },
|
||||
]
|
||||
{ns: 'zh:ember:uart:ash', match: true},
|
||||
{ns: 'zh:ember:uart', match: false},
|
||||
{ns: 'zh:controller', match: true},
|
||||
{ns: 'zh:controller:', match: true},
|
||||
{ns: 'azh:controller:', match: false},
|
||||
],
|
||||
],
|
||||
[
|
||||
'',
|
||||
undefined,
|
||||
[
|
||||
{ ns: 'zhc:legacy:fz:tuya_device12', match: false },
|
||||
{ ns: 'zhc:legacy:fz:moes_dimmer', match: false },
|
||||
{ ns: 'zhc:legacy:fz:not_moes', match: false },
|
||||
{ ns: 'zhc:legacy:fz', match: false },
|
||||
{ ns: 'zhc:legacy:fz:', match: false },
|
||||
{ ns: '1zhc:legacy:fz:tuya_device12', match: false },
|
||||
{ ns: 'zh:ember:uart:ash', match: false },
|
||||
{ ns: 'zh:ember:uart', match: false },
|
||||
{ ns: 'zh:controller', match: false },
|
||||
{ ns: 'zh:controller:', match: false },
|
||||
{ ns: 'azh:controller:', match: false },
|
||||
]
|
||||
{ns: 'zhc:legacy:fz:tuya_device12', match: false},
|
||||
{ns: 'zhc:legacy:fz:moes_dimmer', match: false},
|
||||
{ns: 'zhc:legacy:fz:not_moes', match: false},
|
||||
{ns: 'zhc:legacy:fz', match: false},
|
||||
{ns: 'zhc:legacy:fz:', match: false},
|
||||
{ns: '1zhc:legacy:fz:tuya_device12', match: false},
|
||||
{ns: 'zh:ember:uart:ash', match: false},
|
||||
{ns: 'zh:ember:uart', match: false},
|
||||
{ns: 'zh:controller', match: false},
|
||||
{ns: 'zh:controller:', match: false},
|
||||
{ns: 'azh:controller:', match: false},
|
||||
],
|
||||
],
|
||||
])('Sets namespace ignore for debug level %s', (ignore, expected, tests) => {
|
||||
logger.setLevel('debug');
|
||||
@ -264,7 +262,7 @@ describe('Logger', () => {
|
||||
});
|
||||
logger.init();
|
||||
logger.setLevel('debug');
|
||||
expect(logger.getNamespacedLevels()).toStrictEqual({"z2m:mqtt": 'warning'});
|
||||
expect(logger.getNamespacedLevels()).toStrictEqual({'z2m:mqtt': 'warning'});
|
||||
expect(logger.getLevel()).toStrictEqual('debug');
|
||||
|
||||
const logSpy = jest.spyOn(logger.winston, 'log');
|
||||
@ -283,9 +281,9 @@ describe('Logger', () => {
|
||||
|
||||
it('Logs with namespaced levels or default - lower', () => {
|
||||
expect(logger.getNamespacedLevels()).toStrictEqual({});
|
||||
logger.setNamespacedLevels({'z2m:mqtt': 'info'})
|
||||
logger.setNamespacedLevels({'z2m:mqtt': 'info'});
|
||||
logger.setLevel('warning');
|
||||
expect(logger.getNamespacedLevels()).toStrictEqual({"z2m:mqtt": 'info'});
|
||||
expect(logger.getNamespacedLevels()).toStrictEqual({'z2m:mqtt': 'info'});
|
||||
expect(logger.getLevel()).toStrictEqual('warning');
|
||||
|
||||
const logSpy = jest.spyOn(logger.winston, 'log');
|
||||
@ -332,7 +330,7 @@ describe('Logger', () => {
|
||||
expect(consoleWriteSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
logger.setLevel('info');
|
||||
expect(logger.cachedNamespacedLevels).toStrictEqual(cachedNSLevels = Object.assign({}, nsLevels));
|
||||
expect(logger.cachedNamespacedLevels).toStrictEqual((cachedNSLevels = Object.assign({}, nsLevels)));
|
||||
logger.info(`unconfigured namespace info should now pass after default level change and cache reset`, 'z2m:mqtt');
|
||||
expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'info'}));
|
||||
expect(consoleWriteSpy).toHaveBeenCalledTimes(4);
|
||||
@ -340,8 +338,8 @@ describe('Logger', () => {
|
||||
expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:another:sub:ns': 'error'}));
|
||||
expect(consoleWriteSpy).toHaveBeenCalledTimes(5);
|
||||
|
||||
logger.setNamespacedLevels({'zh:zstack': 'warning'})
|
||||
expect(logger.cachedNamespacedLevels).toStrictEqual(cachedNSLevels = {'zh:zstack': 'warning'});
|
||||
logger.setNamespacedLevels({'zh:zstack': 'warning'});
|
||||
expect(logger.cachedNamespacedLevels).toStrictEqual((cachedNSLevels = {'zh:zstack': 'warning'}));
|
||||
logger.error(`error logged`, 'zh:zstack:unpi:writer');
|
||||
expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer': 'warning'}));
|
||||
expect(consoleWriteSpy).toHaveBeenCalledTimes(6);
|
||||
|
@ -12,7 +12,7 @@ zigbeeHerdsman.returnDevices.push(bulb_color.ieeeAddr);
|
||||
zigbeeHerdsman.returnDevices.push(WXKG02LM_rev1.ieeeAddr);
|
||||
zigbeeHerdsman.returnDevices.push(CC2530_ROUTER.ieeeAddr);
|
||||
zigbeeHerdsman.returnDevices.push(unsupported_router.ieeeAddr);
|
||||
zigbeeHerdsman.returnDevices.push(external_converter_device.ieeeAddr)
|
||||
zigbeeHerdsman.returnDevices.push(external_converter_device.ieeeAddr);
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
@ -25,7 +25,7 @@ describe('Networkmap', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers();
|
||||
Date.now = jest.fn()
|
||||
Date.now = jest.fn();
|
||||
Date.now.mockReturnValue(10000);
|
||||
data.writeDefaultConfiguration();
|
||||
settings.reRead();
|
||||
@ -66,32 +66,62 @@ describe('Networkmap', () => {
|
||||
* | -> CC2530_ROUTER -> WXKG02LM_rev1
|
||||
*
|
||||
*/
|
||||
coordinator.lqi = () => {return {neighbors: [
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120},
|
||||
{ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92},
|
||||
{ieeeAddr: external_converter_device.ieeeAddr, networkAddress: external_converter_device.networkAddress, relationship: 2, depth: 1, linkquality: 92}
|
||||
]}};
|
||||
coordinator.routingTable = () => {return {table: [
|
||||
{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress},
|
||||
]}};
|
||||
coordinator.lqi = () => {
|
||||
return {
|
||||
neighbors: [
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120},
|
||||
{ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92},
|
||||
{
|
||||
ieeeAddr: external_converter_device.ieeeAddr,
|
||||
networkAddress: external_converter_device.networkAddress,
|
||||
relationship: 2,
|
||||
depth: 1,
|
||||
linkquality: 92,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
coordinator.routingTable = () => {
|
||||
return {table: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}]};
|
||||
};
|
||||
|
||||
bulb.lqi = () => {return {neighbors: [
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110},
|
||||
{ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100}
|
||||
]}};
|
||||
bulb.routingTable = () => {return {table: []}};
|
||||
bulb.lqi = () => {
|
||||
return {
|
||||
neighbors: [
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110},
|
||||
{ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100},
|
||||
],
|
||||
};
|
||||
};
|
||||
bulb.routingTable = () => {
|
||||
return {table: []};
|
||||
};
|
||||
|
||||
bulb_color.lqi = () => {return {neighbors: []}}
|
||||
bulb_color.routingTable = () => {return {table: []}};
|
||||
bulb_color.lqi = () => {
|
||||
return {neighbors: []};
|
||||
};
|
||||
bulb_color.routingTable = () => {
|
||||
return {table: []};
|
||||
};
|
||||
|
||||
CC2530_ROUTER.lqi = () => {return {neighbors: [
|
||||
{ieeeAddr: '0x0000000000000000', networkAddress: WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130},
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130},
|
||||
]}};
|
||||
CC2530_ROUTER.routingTable = () => {return {table: []}};
|
||||
CC2530_ROUTER.lqi = () => {
|
||||
return {
|
||||
neighbors: [
|
||||
{ieeeAddr: '0x0000000000000000', networkAddress: WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130},
|
||||
{ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130},
|
||||
],
|
||||
};
|
||||
};
|
||||
CC2530_ROUTER.routingTable = () => {
|
||||
return {table: []};
|
||||
};
|
||||
|
||||
unsupported_router.lqi = () => {throw new Error('failed')};
|
||||
unsupported_router.routingTable = () => {throw new Error('failed')};
|
||||
unsupported_router.lqi = () => {
|
||||
throw new Error('failed');
|
||||
};
|
||||
unsupported_router.routingTable = () => {
|
||||
throw new Error('failed');
|
||||
};
|
||||
}
|
||||
|
||||
it('Output raw networkmap legacy api', async () => {
|
||||
@ -102,7 +132,175 @@ describe('Networkmap', () => {
|
||||
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), 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"}]};
|
||||
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), 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);
|
||||
|
||||
/**
|
||||
@ -122,7 +320,7 @@ describe('Networkmap', () => {
|
||||
}
|
||||
});
|
||||
|
||||
expected.links.forEach((l) => l.routes = [])
|
||||
expected.links.forEach((l) => (l.routes = []));
|
||||
expect(JSON.parse(call[1])).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
@ -131,7 +329,7 @@ describe('Networkmap', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
device.lastSeen = null;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
const data = {modelID: 'test'}
|
||||
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');
|
||||
@ -170,7 +368,7 @@ describe('Networkmap', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
device.lastSeen = null;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
const data = {modelID: 'test'}
|
||||
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');
|
||||
@ -276,7 +474,187 @@ describe('Networkmap', () => {
|
||||
let call = MQTT.publish.mock.calls[0];
|
||||
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
|
||||
|
||||
const expected = {"data":{"routes":true,"type":"raw","value":{"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), 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"}]}},"status":"ok"};
|
||||
const expected = {
|
||||
data: {
|
||||
routes: true,
|
||||
type: 'raw',
|
||||
value: {
|
||||
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), 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
status: 'ok',
|
||||
};
|
||||
const actual = JSON.parse(call[1]);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
@ -288,8 +666,9 @@ describe('Networkmap', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/networkmap',
|
||||
stringify({"data":{},"status":"error","error":"Type 'not_existing' not supported, allowed are: raw,graphviz,plantuml"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {}, status: 'error', error: "Type 'not_existing' not supported, allowed are: raw,graphviz,plantuml"}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -303,7 +682,147 @@ describe('Networkmap', () => {
|
||||
let call = MQTT.publish.mock.calls[0];
|
||||
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
|
||||
|
||||
const expected = {"data":{"routes":true,"type":"raw","value":{"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":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":"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"}]}},"status":"ok"}
|
||||
const expected = {
|
||||
data: {
|
||||
routes: true,
|
||||
type: 'raw',
|
||||
value: {
|
||||
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: 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: '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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
status: 'ok',
|
||||
};
|
||||
const actual = JSON.parse(call[1]);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
@ -44,8 +44,8 @@ describe('On event', () => {
|
||||
it('Should call with start event', async () => {
|
||||
expect(mockOnEvent).toHaveBeenCalledTimes(1);
|
||||
const call = mockOnEvent.mock.calls[0];
|
||||
expect(call[0]).toBe('start')
|
||||
expect(call[1]).toStrictEqual({})
|
||||
expect(call[0]).toBe('start');
|
||||
expect(call[1]).toStrictEqual({});
|
||||
expect(call[2]).toBe(device);
|
||||
expect(call[3]).toStrictEqual(settings.getDevice(device.ieeeAddr));
|
||||
expect(call[4]).toStrictEqual({});
|
||||
@ -57,8 +57,8 @@ describe('On event', () => {
|
||||
await flushPromises();
|
||||
expect(mockOnEvent).toHaveBeenCalledTimes(1);
|
||||
const call = mockOnEvent.mock.calls[0];
|
||||
expect(call[0]).toBe('stop')
|
||||
expect(call[1]).toStrictEqual({})
|
||||
expect(call[0]).toBe('stop');
|
||||
expect(call[1]).toStrictEqual({});
|
||||
expect(call[2]).toBe(device);
|
||||
});
|
||||
|
||||
|
@ -14,19 +14,18 @@ const zigbeeOTA = require('zigbee-herdsman-converters/lib/ota/zigbeeOTA');
|
||||
|
||||
const spyUseIndexOverride = jest.spyOn(zigbeeOTA, 'useIndexOverride');
|
||||
|
||||
|
||||
describe('OTA update', () => {
|
||||
let controller;
|
||||
|
||||
let resetExtension = async () => {
|
||||
await controller.enableDisableExtension(false, 'OTAUpdate');
|
||||
await controller.enableDisableExtension(true, 'OTAUpdate');
|
||||
}
|
||||
};
|
||||
|
||||
const mockClear = (mapped) => {
|
||||
mapped.ota.updateToLatest = jest.fn();
|
||||
mapped.ota.isUpdateAvailable = jest.fn();
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
data.writeDefaultConfiguration();
|
||||
@ -66,9 +65,9 @@ describe('OTA update', () => {
|
||||
let count = 0;
|
||||
endpoint.read.mockImplementation(() => {
|
||||
count++;
|
||||
return {swBuildId: count, dateCode: '2019010' + count}
|
||||
return {swBuildId: count, dateCode: '2019010' + count};
|
||||
});
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
device.save.mockClear();
|
||||
@ -87,164 +86,189 @@ describe('OTA update', () => {
|
||||
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.info).toHaveBeenCalledWith(
|
||||
`Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`,
|
||||
);
|
||||
expect(device.save).toHaveBeenCalledTimes(2);
|
||||
expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendPolicy': 'immediate'});
|
||||
expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {'sendPolicy': undefined});
|
||||
expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: 'immediate'});
|
||||
expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined});
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"update_available":false,"update":{"state":"updating","progress":0}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
stringify({update_available: false, 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}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
stringify({update_available: false, 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}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
stringify({update_available: false, update: {state: 'idle', installed_version: 90, latest_version: 90}}),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/ota_update/update',
|
||||
stringify({"data":{"from":{"date_code":"20190101","software_build_id":1},"id":"bulb","to":{"date_code":"20190102","software_build_id":2}},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/devices',
|
||||
expect.any(String),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
stringify({
|
||||
data: {from: {date_code: '20190101', software_build_id: 1}, id: 'bulb', to: {date_code: '20190102', software_build_id: 2}},
|
||||
status: 'ok',
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('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)
|
||||
endpoint.read.mockImplementation(() => {
|
||||
return {swBuildId: 1, dateCode: '2019010'};
|
||||
});
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
device.save.mockClear();
|
||||
mapped.ota.updateToLatest.mockImplementationOnce((a, onUpdate) => {
|
||||
throw new Error('Update failed');
|
||||
});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', stringify({id: "bulb"}));
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', stringify({id: 'bulb'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"update_available":true,"update":{"state":"available"}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
stringify({update_available: true, update: {state: 'available'}}),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/ota_update/update',
|
||||
stringify({"data":{"id": "bulb"},"status":"error","error":"Update of 'bulb' failed (Update failed)"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {id: 'bulb'}, status: 'error', error: "Update of 'bulb' failed (Update failed)"}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should be able to check if OTA update is available', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb");
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
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"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {id: 'bulb', updateAvailable: false}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb");
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2);
|
||||
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"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {id: 'bulb', updateAvailable: true}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should handle if OTA update check fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF signals disturbed because of dogs barking')});
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {
|
||||
throw new Error('RF signals disturbed because of dogs barking');
|
||||
});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb");
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/ota_update/check',
|
||||
stringify({"data":{"id": "bulb"},"status":"error","error": `Failed to check if update available for 'bulb' (RF signals disturbed because of dogs barking)`}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({
|
||||
data: {id: 'bulb'},
|
||||
status: 'error',
|
||||
error: `Failed to check if update available for 'bulb' (RF signals disturbed because of dogs barking)`,
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should fail when device does not exist', async () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "not_existing_deviceooo");
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'not_existing_deviceooo');
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/ota_update/check',
|
||||
stringify({"data":{"id": "not_existing_deviceooo"},"status":"error","error": `Device 'not_existing_deviceooo' does not exist`}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {id: 'not_existing_deviceooo'}, status: 'error', error: `Device 'not_existing_deviceooo' does not exist`}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should not check for OTA when device does not support it', async () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "dimmer_wall_switch");
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'dimmer_wall_switch');
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/ota_update/check',
|
||||
stringify({"data":{"id": "dimmer_wall_switch"},"status":"error","error": `Device 'dimmer_wall_switch' does not support OTA updates`}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {id: 'dimmer_wall_switch'}, status: 'error', error: `Device 'dimmer_wall_switch' does not support OTA updates`}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should refuse to check/update when already in progress', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {
|
||||
return new Promise((resolve, reject) => {setTimeout(() => resolve(), 99999)})
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => resolve(), 99999);
|
||||
});
|
||||
});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb");
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb");
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/ota_update/check',
|
||||
stringify({"data":{"id": "bulb"},"status":"error","error": `Update or check for update already in progress for 'bulb'`}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {id: 'bulb'}, status: 'error', error: `Update or check for update already in progress for 'bulb'`}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Shouldnt crash when read modelID before/after OTA update fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint = device.endpoints[0];
|
||||
endpoint.read.mockImplementation(() => {throw new Error('Failed!')});
|
||||
endpoint.read.mockImplementation(() => {
|
||||
throw new Error('Failed!');
|
||||
});
|
||||
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', "bulb");
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb');
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/ota_update/update',
|
||||
stringify({"data":{"id":"bulb","from":null,"to":null},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
stringify({data: {id: 'bulb', from: null, to: null}, status: 'ok'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -252,18 +276,26 @@ describe('OTA update', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
device.endpoints[0].commandResponse.mockClear();
|
||||
const data = {imageType: 12382};
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'genOta',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'commandQueryNextImageRequest',
|
||||
linkquality: 10,
|
||||
meta: {zclTransactionSequenceNumber: 10},
|
||||
};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {"imageType": 12382});
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {imageType: 12382});
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x98}, undefined, 10);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10);
|
||||
|
||||
// Should not request again when device asks again after a short time
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
@ -277,8 +309,9 @@ 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}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
stringify({update_available: true, update: {state: 'available', installed_version: 10, latest_version: 12}}),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -286,21 +319,32 @@ describe('OTA update', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
device.endpoints[0].commandResponse.mockClear();
|
||||
const data = {imageType: 12382};
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('Nothing to find here')})
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}};
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {
|
||||
throw new Error('Nothing to find here');
|
||||
});
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'genOta',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'commandQueryNextImageRequest',
|
||||
linkquality: 10,
|
||||
meta: {zclTransactionSequenceNumber: 10},
|
||||
};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {"imageType": 12382});
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {imageType: 12382});
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x98}, undefined, 10);
|
||||
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"}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
stringify({update_available: false, update: {state: 'idle'}}),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -308,21 +352,30 @@ describe('OTA update', () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
device.endpoints[0].commandResponse.mockClear();
|
||||
const data = {imageType: 12382};
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'genOta',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'commandQueryNextImageRequest',
|
||||
linkquality: 10,
|
||||
meta: {zclTransactionSequenceNumber: 10},
|
||||
};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {"imageType": 12382});
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {imageType: 12382});
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x98}, undefined, 10);
|
||||
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}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
stringify({update_available: false, update: {state: 'idle', installed_version: 13, latest_version: 13}}),
|
||||
{retain: true, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
@ -330,10 +383,18 @@ describe('OTA update', () => {
|
||||
settings.set(['ota', 'disable_automatic_update_check'], true);
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const data = {imageType: 12382};
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 13});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'genOta',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'commandQueryNextImageRequest',
|
||||
linkquality: 10,
|
||||
meta: {zclTransactionSequenceNumber: 10},
|
||||
};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
@ -343,22 +404,38 @@ describe('OTA update', () => {
|
||||
it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA', async () => {
|
||||
const device = zigbeeHerdsman.devices.HGZB04D;
|
||||
const data = {imageType: 12382};
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'genOta',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'commandQueryNextImageRequest',
|
||||
linkquality: 10,
|
||||
meta: {zclTransactionSequenceNumber: 10},
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 152}, undefined, 10);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10);
|
||||
});
|
||||
|
||||
it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA and device has no OTA endpoint to standard endpoint', async () => {
|
||||
const device = zigbeeHerdsman.devices.SV01;
|
||||
const data = {imageType: 12382};
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10, meta: {zclTransactionSequenceNumber: 10}};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'genOta',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'commandQueryNextImageRequest',
|
||||
linkquality: 10,
|
||||
meta: {zclTransactionSequenceNumber: 10},
|
||||
};
|
||||
logger.error.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 152}, undefined, 10);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10);
|
||||
});
|
||||
|
||||
it('Legacy api: Should OTA update a device', async () => {
|
||||
@ -367,9 +444,9 @@ describe('OTA update', () => {
|
||||
let count = 0;
|
||||
endpoint.read.mockImplementation(() => {
|
||||
count++;
|
||||
return {swBuildId: count, dateCode: '2019010' + count}
|
||||
return {swBuildId: count, dateCode: '2019010' + count};
|
||||
});
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
logger.error.mockClear();
|
||||
@ -389,18 +466,22 @@ describe('OTA update', () => {
|
||||
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.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});
|
||||
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)
|
||||
endpoint.read.mockImplementation(() => {
|
||||
return {swBuildId: 1, dateCode: '2019010'};
|
||||
});
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
logger.error.mockClear();
|
||||
@ -417,7 +498,7 @@ describe('OTA update', () => {
|
||||
|
||||
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)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
|
||||
logger.info.mockClear();
|
||||
@ -439,10 +520,12 @@ describe('OTA update', () => {
|
||||
|
||||
it('Legacy api: Should handle if OTA update check fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
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')});
|
||||
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();
|
||||
@ -462,12 +545,12 @@ describe('OTA update', () => {
|
||||
const endpoint = device.endpoints[0];
|
||||
let count = 0;
|
||||
endpoint.read.mockImplementation(() => {
|
||||
if (count === 1) throw new Error('Failed!')
|
||||
if (count === 1) throw new Error('Failed!');
|
||||
count++;
|
||||
return {swBuildId: 1, dateCode: '2019010'}
|
||||
return {swBuildId: 1, dateCode: '2019010'};
|
||||
});
|
||||
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device)
|
||||
const mapped = await zigbeeHerdsmanConverters.findByDevice(device);
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
|
||||
@ -480,7 +563,7 @@ describe('OTA update', () => {
|
||||
await resetExtension();
|
||||
expect(spyUseIndexOverride).toHaveBeenCalledWith(path.join(data.mockDir, 'local.index.json'));
|
||||
spyUseIndexOverride.mockClear();
|
||||
|
||||
|
||||
settings.set(['ota', 'zigbee_ota_override_index_location'], 'http://my.site/index.json');
|
||||
await resetExtension();
|
||||
expect(spyUseIndexOverride).toHaveBeenCalledWith('http://my.site/index.json');
|
||||
@ -489,7 +572,7 @@ describe('OTA update', () => {
|
||||
|
||||
it('Clear update state on startup', async () => {
|
||||
const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr);
|
||||
controller.state.set(device, {update: {progress: 100, remaining: 10, state: 'updating'}})
|
||||
controller.state.set(device, {update: {progress: 100, remaining: 10, state: 'updating'}});
|
||||
await resetExtension();
|
||||
expect(controller.state.get(device)).toStrictEqual({update: {state: 'available'}});
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -38,72 +38,108 @@ describe('Receive', () => {
|
||||
it('Should handle a zigbee message', async () => {
|
||||
const device = zigbeeHerdsman.devices.WXKG11LM;
|
||||
device.linkquality = 10;
|
||||
const data = {onOff: 1}
|
||||
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(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button', stringify({action: 'single', click: 'single', linkquality: 10}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button',
|
||||
stringify({action: 'single', click: 'single', linkquality: 10}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message which uses ep (left)', async () => {
|
||||
const device = zigbeeHerdsman.devices.WXKG02LM_rev1;
|
||||
const data = {onOff: 1}
|
||||
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(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'left', action: 'single_left'});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message which uses ep (right)', async () => {
|
||||
const device = zigbeeHerdsman.devices.WXKG02LM_rev1;
|
||||
const data = {onOff: 1}
|
||||
const data = {onOff: 1};
|
||||
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(2), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'right', action: 'single_right'});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with default precision', async () => {
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
const data = {measuredValue: -85}
|
||||
const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
const data = {measuredValue: -85};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'msTemperatureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.85});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
|
||||
});
|
||||
|
||||
it('Should allow to invert cover', async () => {
|
||||
const device = zigbeeHerdsman.devices.J1;
|
||||
|
||||
// Non-inverted (open = 100, close = 0)
|
||||
await zigbeeHerdsman.events.message({data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await zigbeeHerdsman.events.message({
|
||||
data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80},
|
||||
cluster: 'closuresWindowCovering',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 10, tilt: 20, state: 'OPEN'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/J1_cover',
|
||||
stringify({position: 10, tilt: 20, state: 'OPEN'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// Inverted
|
||||
MQTT.publish.mockClear();
|
||||
settings.set(['devices', device.ieeeAddr, 'invert_cover'], true);
|
||||
await zigbeeHerdsman.events.message({data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await zigbeeHerdsman.events.message({
|
||||
data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80},
|
||||
cluster: 'closuresWindowCovering',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 90, tilt: 80, state: 'OPEN'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/J1_cover',
|
||||
stringify({position: 90, tilt: 80, state: 'OPEN'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should allow to disable the legacy integration', async () => {
|
||||
const device = zigbeeHerdsman.devices.WXKG11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'legacy'], false);
|
||||
const data = {onOff: 1}
|
||||
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();
|
||||
@ -114,14 +150,35 @@ describe('Receive', () => {
|
||||
it('Should debounce messages', async () => {
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1);
|
||||
const data1 = {measuredValue: 8}
|
||||
const payload1 = {data: data1, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
const data1 = {measuredValue: 8};
|
||||
const payload1 = {
|
||||
data: data1,
|
||||
cluster: 'msTemperatureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload1);
|
||||
const data2 = {measuredValue: 1}
|
||||
const payload2 = {data: data2, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
const data2 = {measuredValue: 1};
|
||||
const payload2 = {
|
||||
data: data2,
|
||||
cluster: 'msRelativeHumidity',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload2);
|
||||
const data3 = {measuredValue: 2}
|
||||
const payload3 = {data: data3, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
const data3 = {measuredValue: 2};
|
||||
const payload3 = {
|
||||
data: data3,
|
||||
cluster: 'msPressureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload3);
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(50);
|
||||
@ -131,7 +188,7 @@ describe('Receive', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
|
||||
});
|
||||
|
||||
it('Should debounce and retain messages when set via device_options', async () => {
|
||||
@ -139,14 +196,35 @@ describe('Receive', () => {
|
||||
settings.set(['device_options', 'debounce'], 0.1);
|
||||
settings.set(['device_options', 'retain'], true);
|
||||
delete settings.get().devices['0x0017880104e45522']['retain'];
|
||||
const data1 = {measuredValue: 8}
|
||||
const payload1 = {data: data1, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
const data1 = {measuredValue: 8};
|
||||
const payload1 = {
|
||||
data: data1,
|
||||
cluster: 'msTemperatureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload1);
|
||||
const data2 = {measuredValue: 1}
|
||||
const payload2 = {data: data2, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
const data2 = {measuredValue: 1};
|
||||
const payload2 = {
|
||||
data: data2,
|
||||
cluster: 'msRelativeHumidity',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload2);
|
||||
const data3 = {measuredValue: 2}
|
||||
const payload3 = {data: data3, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
const data3 = {measuredValue: 2};
|
||||
const payload3 = {
|
||||
data: data3,
|
||||
cluster: 'msPressureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload3);
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(50);
|
||||
@ -156,20 +234,48 @@ describe('Receive', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": true});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: true});
|
||||
});
|
||||
|
||||
it('Should debounce messages only with the same payload values for provided debounce_ignore keys', async () => {
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1);
|
||||
settings.set(['devices', device.ieeeAddr, 'debounce_ignore'], ['temperature']);
|
||||
const tempMsg = {data: {measuredValue: 8}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
|
||||
const tempMsg = {
|
||||
data: {measuredValue: 8},
|
||||
cluster: 'msTemperatureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 13,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(tempMsg);
|
||||
const pressureMsg = {data: {measuredValue: 2}, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
|
||||
const pressureMsg = {
|
||||
data: {measuredValue: 2},
|
||||
cluster: 'msPressureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 13,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(pressureMsg);
|
||||
const tempMsg2 = {data: {measuredValue: 7}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
|
||||
const tempMsg2 = {
|
||||
data: {measuredValue: 7},
|
||||
cluster: 'msTemperatureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 13,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(tempMsg2);
|
||||
const humidityMsg = {data: {measuredValue: 3}, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
|
||||
const humidityMsg = {
|
||||
data: {measuredValue: 3},
|
||||
cluster: 'msRelativeHumidity',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 13,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(humidityMsg);
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(50);
|
||||
@ -182,16 +288,36 @@ describe('Receive', () => {
|
||||
});
|
||||
|
||||
it('Should NOT publish old messages from State cache during debouncing', async () => {
|
||||
|
||||
// Summary:
|
||||
// First send multiple measurements to device that is debouncing. Make sure only one message is sent out to MQTT. This also ensures first message is cached to "State".
|
||||
// Then send another measurement to that same device and trigger asynchronous event to push data from Cache. Newest value should be sent out.
|
||||
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1);
|
||||
await zigbeeHerdsman.events.message({data: {measuredValue: 8}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await zigbeeHerdsman.events.message( {data: {measuredValue: 1}, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10} );
|
||||
await zigbeeHerdsman.events.message( {data: {measuredValue: 2}, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10} );
|
||||
await zigbeeHerdsman.events.message({
|
||||
data: {measuredValue: 8},
|
||||
cluster: 'msTemperatureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
});
|
||||
await zigbeeHerdsman.events.message({
|
||||
data: {measuredValue: 1},
|
||||
cluster: 'msRelativeHumidity',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
});
|
||||
await zigbeeHerdsman.events.message({
|
||||
data: {measuredValue: 2},
|
||||
cluster: 'msPressureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
});
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(50);
|
||||
// Test that measurements are combined(=debounced)
|
||||
@ -205,11 +331,18 @@ describe('Receive', () => {
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
|
||||
// Send another Zigbee message...
|
||||
await zigbeeHerdsman.events.message({data: {measuredValue: 9}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await zigbeeHerdsman.events.message({
|
||||
data: {measuredValue: 9},
|
||||
cluster: 'msTemperatureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
});
|
||||
const realDevice = controller.zigbee.resolveEntity(device);
|
||||
|
||||
// Trigger asynchronous event while device is "debouncing" to trigger Message to be sent out from State cache.
|
||||
await controller.publishEntityState( realDevice, {} );
|
||||
await controller.publishEntityState(realDevice, {});
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
@ -220,13 +353,20 @@ describe('Receive', () => {
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
// Test that messages after debouncing contains NEW measurement and not old.
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
});
|
||||
});
|
||||
|
||||
it('Shouldnt republish old state', async () => {
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/3572
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1);
|
||||
await zigbeeHerdsman.events.message({data: {onOff: 0}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await zigbeeHerdsman.events.message({
|
||||
data: {onOff: 0},
|
||||
cluster: 'genOnOff',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
});
|
||||
await MQTT.events.message('zigbee2mqtt/bulb/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
jest.runOnlyPendingTimers();
|
||||
@ -238,83 +378,118 @@ describe('Receive', () => {
|
||||
it('Should handle a zigbee message with 1 precision', async () => {
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 1);
|
||||
const data = {measuredValue: -85}
|
||||
const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
const data = {measuredValue: -85};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'msTemperatureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with 0 precision', async () => {
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 0);
|
||||
const data = {measuredValue: -85}
|
||||
const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
const data = {measuredValue: -85};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'msTemperatureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with 1 precision when set via device_options', async () => {
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['device_options', 'temperature_precision'], 1);
|
||||
const data = {measuredValue: -85}
|
||||
const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
const data = {measuredValue: -85};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'msTemperatureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with 2 precision when overrides device_options', async () => {
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['device_options', 'temperature_precision'], 1);
|
||||
settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 0);
|
||||
const data = {measuredValue: -85}
|
||||
const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
const data = {measuredValue: -85};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'msTemperatureMeasurement',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with voltage 2990', async () => {
|
||||
const device = zigbeeHerdsman.devices.WXKG02LM_rev1;
|
||||
const data = {'65281': {'1': 2990}}
|
||||
const data = {65281: {1: 2990}};
|
||||
const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 93, voltage: 2990});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
|
||||
});
|
||||
|
||||
it('Should publish 1 message when converted twice', async () => {
|
||||
const device = zigbeeHerdsman.devices.RTCGQ11LM;
|
||||
const data = {'65281': {'1': 3045, '3': 19, '5': 35, '6': [0, 3], '11': 381, '100': 0}}
|
||||
const data = {65281: {1: 3045, 3: 19, 5: 35, 6: [0, 3], 11: 381, 100: 0}};
|
||||
const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'battery': 100, 'illuminance': 381, "illuminance_lux": 381, 'voltage': 3045, 'device_temperature': 19, 'power_outage_count': 34});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({
|
||||
battery: 100,
|
||||
illuminance: 381,
|
||||
illuminance_lux: 381,
|
||||
voltage: 3045,
|
||||
device_temperature: 19,
|
||||
power_outage_count: 34,
|
||||
});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
|
||||
});
|
||||
|
||||
it('Should publish 1 message when converted twice', async () => {
|
||||
const device = zigbeeHerdsman.devices.RTCGQ11LM;
|
||||
const data = {'9999': {'1': 3045}};
|
||||
const data = {9999: {1: 3045}};
|
||||
const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
@ -330,8 +505,8 @@ describe('Receive', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
|
||||
expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('number')
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('number');
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
|
||||
});
|
||||
|
||||
it('Should publish last_seen ISO_8601', async () => {
|
||||
@ -343,8 +518,8 @@ describe('Receive', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
|
||||
expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string')
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string');
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
|
||||
});
|
||||
|
||||
it('Should publish last_seen ISO_8601_local', async () => {
|
||||
@ -356,8 +531,8 @@ describe('Receive', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
|
||||
expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string')
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string');
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
|
||||
});
|
||||
|
||||
it('Should handle messages from Xiaomi router devices', async () => {
|
||||
@ -367,19 +542,13 @@ describe('Receive', () => {
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/power_plug',
|
||||
stringify({state: 'ON'}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/power_plug', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/switch_group',
|
||||
stringify({'state': 'ON'}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function)
|
||||
stringify({state: 'ON'}),
|
||||
{retain: false, qos: 0},
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('Should not handle messages from coordinator', async () => {
|
||||
@ -405,13 +574,21 @@ describe('Receive', () => {
|
||||
it('Should handle a command', async () => {
|
||||
const device = zigbeeHerdsman.devices.E1743;
|
||||
const data = {};
|
||||
const payload = {data, cluster: 'genLevelCtrl', device, endpoint: device.getEndpoint(1), type: 'commandStopWithOnOff', linkquality: 10, meta: {zclTransactionSequenceNumber: 1}};
|
||||
const payload = {
|
||||
data,
|
||||
cluster: 'genLevelCtrl',
|
||||
device,
|
||||
endpoint: device.getEndpoint(1),
|
||||
type: 'commandStopWithOnOff',
|
||||
linkquality: 10,
|
||||
meta: {zclTransactionSequenceNumber: 1},
|
||||
};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'click': 'brightness_stop', action: 'brightness_stop'});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
|
||||
});
|
||||
|
||||
it('Should add elapsed', async () => {
|
||||
@ -419,7 +596,7 @@ describe('Receive', () => {
|
||||
const device = zigbeeHerdsman.devices.E1743;
|
||||
const payload = {data: {}, cluster: 'genLevelCtrl', device, endpoint: device.getEndpoint(1), type: 'commandStopWithOnOff'};
|
||||
const oldNow = Date.now;
|
||||
Date.now = jest.fn()
|
||||
Date.now = jest.fn();
|
||||
Date.now.mockReturnValue(new Date(150));
|
||||
await zigbeeHerdsman.events.message({...payload, meta: {zclTransactionSequenceNumber: 2}});
|
||||
await flushPromises();
|
||||
@ -428,12 +605,12 @@ describe('Receive', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'click': 'brightness_stop', action: 'brightness_stop'});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
|
||||
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toMatchObject({'click': 'brightness_stop', action: 'brightness_stop'});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toMatchObject({click: 'brightness_stop', action: 'brightness_stop'});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1]).elapsed).toBe(50);
|
||||
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false});
|
||||
Date.now = oldNow;
|
||||
});
|
||||
|
||||
@ -444,16 +621,26 @@ describe('Receive', () => {
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
expect(logger.debug).toHaveBeenCalledWith('No converter available for \'ZNCZ02LM\' with cluster \'genBinaryOutput\' and type \'attributeReport\' and data \'{"inactiveText":"hello"}\'');
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
"No converter available for 'ZNCZ02LM' with cluster 'genBinaryOutput' and type 'attributeReport' and data '{\"inactiveText\":\"hello\"}'",
|
||||
);
|
||||
});
|
||||
|
||||
it('Should report correct energy and power values for different versions of SP600', async () => {
|
||||
// https://github.com/Koenkk/zigbee-herdsman-converters/issues/915, OLD and NEW use different date code
|
||||
// divisor of OLD is not correct and therefore underreports by factor 10.
|
||||
const data = {instantaneousDemand:496,currentSummDelivered:[0,6648]}
|
||||
const data = {instantaneousDemand: 496, currentSummDelivered: [0, 6648]};
|
||||
|
||||
const SP600_NEW = zigbeeHerdsman.devices.SP600_NEW;
|
||||
await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_NEW, endpoint: SP600_NEW.getEndpoint(1), type: 'attributeReport', linkquality: 10, meta: {zclTransactionSequenceNumber: 1}});
|
||||
await zigbeeHerdsman.events.message({
|
||||
data,
|
||||
cluster: 'seMetering',
|
||||
device: SP600_NEW,
|
||||
endpoint: SP600_NEW.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
meta: {zclTransactionSequenceNumber: 1},
|
||||
});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_NEW');
|
||||
@ -461,7 +648,15 @@ describe('Receive', () => {
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
const SP600_OLD = zigbeeHerdsman.devices.SP600_OLD;
|
||||
await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_OLD, endpoint: SP600_OLD.getEndpoint(1), type: 'attributeReport', linkquality: 10, meta: {zclTransactionSequenceNumber: 2}});
|
||||
await zigbeeHerdsman.events.message({
|
||||
data,
|
||||
cluster: 'seMetering',
|
||||
device: SP600_OLD,
|
||||
endpoint: SP600_OLD.getEndpoint(1),
|
||||
type: 'attributeReport',
|
||||
linkquality: 10,
|
||||
meta: {zclTransactionSequenceNumber: 2},
|
||||
});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD');
|
||||
@ -470,9 +665,9 @@ describe('Receive', () => {
|
||||
|
||||
it('Should emit DevicesChanged event when a converter announces changed exposes', async () => {
|
||||
const device = zigbeeHerdsman.devices['BMCT-SLZ'];
|
||||
const data = {deviceMode: 0}
|
||||
const data = {deviceMode: 0};
|
||||
const payload = {data, cluster: 'boschSpecific', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bridge/devices");
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices');
|
||||
});
|
||||
});
|
||||
|
@ -19,8 +19,8 @@ const minimalConfig = {
|
||||
};
|
||||
|
||||
describe('Settings', () => {
|
||||
const write = (file, json, reread=true) => {
|
||||
fs.writeFileSync(file, yaml.dump(json))
|
||||
const write = (file, json, reread = true) => {
|
||||
fs.writeFileSync(file, yaml.dump(json));
|
||||
if (reread) {
|
||||
settings.reRead();
|
||||
}
|
||||
@ -28,14 +28,14 @@ describe('Settings', () => {
|
||||
const read = (file) => yaml.load(fs.readFileSync(file, 'utf8'));
|
||||
const remove = (file) => {
|
||||
if (fs.existsSync(file)) fs.unlinkSync(file);
|
||||
}
|
||||
};
|
||||
const clearEnvironmentVariables = () => {
|
||||
Object.keys(process.env).forEach((key) => {
|
||||
if(key.indexOf('ZIGBEE2MQTT_CONFIG_') >= 0) {
|
||||
if (key.indexOf('ZIGBEE2MQTT_CONFIG_') >= 0) {
|
||||
delete process.env[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
remove(configurationFile);
|
||||
@ -69,7 +69,8 @@ describe('Settings', () => {
|
||||
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_SOFT_RESET_TIMEOUT'] = 1;
|
||||
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'] = '{"enddevice": "#ff0000", "coordinator": "#00ff00", "router": "#0000ff"}';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_MAP_OPTIONS_GRAPHVIZ_COLORS_FILL'] =
|
||||
'{"enddevice": "#ff0000", "coordinator": "#00ff00", "router": "#0000ff"}';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC'] = 'testtopic';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_MQTT_SERVER'] = 'testserver';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_NETWORK_KEY'] = 'GENERATE';
|
||||
@ -97,7 +98,7 @@ describe('Settings', () => {
|
||||
expected.groups = {};
|
||||
expected.serial.disable_led = true;
|
||||
expected.advanced.soft_reset_timeout = 1;
|
||||
expected.advanced.log_output = ["console"];
|
||||
expected.advanced.log_output = ['console'];
|
||||
expected.advanced.output = 'attribute_and_json';
|
||||
expected.map_options.graphviz.colors.fill = {enddevice: '#ff0000', coordinator: '#00ff00', router: '#0000ff'};
|
||||
expected.mqtt.base_topic = 'testtopic';
|
||||
@ -153,7 +154,7 @@ describe('Settings', () => {
|
||||
|
||||
const device = settings.getDevice('0x12345678');
|
||||
const expected = {
|
||||
ID: "0x12345678",
|
||||
ID: '0x12345678',
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
};
|
||||
@ -175,14 +176,14 @@ describe('Settings', () => {
|
||||
password: '!secret password',
|
||||
},
|
||||
advanced: {
|
||||
network_key: '!secret network_key'
|
||||
}
|
||||
network_key: '!secret network_key',
|
||||
},
|
||||
};
|
||||
|
||||
const contentSecret = {
|
||||
username: 'mysecretusername',
|
||||
password: 'mysecretpassword',
|
||||
network_key: [1,2,3],
|
||||
network_key: [1, 2, 3],
|
||||
};
|
||||
|
||||
write(secretFile, contentSecret, false);
|
||||
@ -192,22 +193,22 @@ describe('Settings', () => {
|
||||
base_topic: 'zigbee2mqtt',
|
||||
include_device_information: false,
|
||||
force_disable_retain: false,
|
||||
password: "mysecretpassword",
|
||||
server: "my.mqtt.server",
|
||||
user: "mysecretusername",
|
||||
password: 'mysecretpassword',
|
||||
server: 'my.mqtt.server',
|
||||
user: 'mysecretusername',
|
||||
};
|
||||
|
||||
expect(settings.get().mqtt).toStrictEqual(expected);
|
||||
expect(settings.get().advanced.network_key).toStrictEqual([1,2,3]);
|
||||
expect(settings.get().advanced.network_key).toStrictEqual([1, 2, 3]);
|
||||
|
||||
settings.testing.write();
|
||||
expect(read(configurationFile)).toStrictEqual(contentConfiguration);
|
||||
expect(read(secretFile)).toStrictEqual(contentSecret);
|
||||
|
||||
settings.set(['mqtt', 'user'], 'test123');
|
||||
settings.set(['advanced', 'network_key'], [1,2,3, 4]);
|
||||
settings.set(['advanced', 'network_key'], [1, 2, 3, 4]);
|
||||
expect(read(configurationFile)).toStrictEqual(contentConfiguration);
|
||||
expect(read(secretFile)).toStrictEqual({...contentSecret, username: 'test123', network_key: [1,2,3,4]});
|
||||
expect(read(secretFile)).toStrictEqual({...contentSecret, username: 'test123', network_key: [1, 2, 3, 4]});
|
||||
});
|
||||
|
||||
it('Should read ALL secrets form a separate file', () => {
|
||||
@ -219,14 +220,14 @@ describe('Settings', () => {
|
||||
},
|
||||
advanced: {
|
||||
network_key: '!secret network_key',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const contentSecret = {
|
||||
server: 'my.mqtt.server',
|
||||
username: 'mysecretusername',
|
||||
password: 'mysecretpassword',
|
||||
network_key: [1,2,3],
|
||||
network_key: [1, 2, 3],
|
||||
};
|
||||
|
||||
write(secretFile, contentSecret, false);
|
||||
@ -236,13 +237,13 @@ describe('Settings', () => {
|
||||
base_topic: 'zigbee2mqtt',
|
||||
include_device_information: false,
|
||||
force_disable_retain: false,
|
||||
password: "mysecretpassword",
|
||||
server: "my.mqtt.server",
|
||||
user: "mysecretusername",
|
||||
password: 'mysecretpassword',
|
||||
server: 'my.mqtt.server',
|
||||
user: 'mysecretusername',
|
||||
};
|
||||
|
||||
expect(settings.get().mqtt).toStrictEqual(expected);
|
||||
expect(settings.get().advanced.network_key).toStrictEqual([1,2,3]);
|
||||
expect(settings.get().advanced.network_key).toStrictEqual([1, 2, 3]);
|
||||
|
||||
settings.testing.write();
|
||||
expect(read(configurationFile)).toStrictEqual(contentConfiguration);
|
||||
@ -269,7 +270,7 @@ describe('Settings', () => {
|
||||
write(devicesFile, contentDevices);
|
||||
const device = settings.getDevice('0x12345678');
|
||||
const expected = {
|
||||
ID: "0x12345678",
|
||||
ID: '0x12345678',
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
};
|
||||
@ -279,7 +280,7 @@ describe('Settings', () => {
|
||||
|
||||
it('Should read devices form 2 separate files', () => {
|
||||
const contentConfiguration = {
|
||||
devices: ['devices.yaml', 'devices2.yaml']
|
||||
devices: ['devices.yaml', 'devices2.yaml'],
|
||||
};
|
||||
|
||||
const contentDevices = {
|
||||
@ -343,7 +344,7 @@ describe('Settings', () => {
|
||||
const contentDevices = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -358,9 +359,9 @@ describe('Settings', () => {
|
||||
const expected = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
retain: false,
|
||||
},
|
||||
'0x1234': {
|
||||
'0x1234': {
|
||||
friendly_name: '0x1234',
|
||||
},
|
||||
};
|
||||
@ -373,13 +374,13 @@ describe('Settings', () => {
|
||||
extractFromMultipleDeviceConfigs({
|
||||
'0x87654321': {
|
||||
friendly_name: '0x87654321',
|
||||
retain: false,
|
||||
retain: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('Should add devices for first file when using 2 separates file and the second file is empty', () => {
|
||||
extractFromMultipleDeviceConfigs(null)
|
||||
extractFromMultipleDeviceConfigs(null);
|
||||
});
|
||||
|
||||
it('Should add devices to a separate file if devices.yaml doesnt exist', () => {
|
||||
@ -400,8 +401,7 @@ describe('Settings', () => {
|
||||
};
|
||||
|
||||
expect(read(devicesFile)).toStrictEqual(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Should add and remove devices to a separate file if devices.yaml doesnt exist', () => {
|
||||
const contentConfiguration = {
|
||||
@ -417,13 +417,12 @@ describe('Settings', () => {
|
||||
expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'});
|
||||
|
||||
expect(read(devicesFile)).toStrictEqual({});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Should read groups', () => {
|
||||
const content = {
|
||||
groups: {
|
||||
'1': {
|
||||
1: {
|
||||
friendly_name: '123',
|
||||
},
|
||||
},
|
||||
@ -447,7 +446,7 @@ describe('Settings', () => {
|
||||
};
|
||||
|
||||
const contentGroups = {
|
||||
'1': {
|
||||
1: {
|
||||
friendly_name: '123',
|
||||
},
|
||||
};
|
||||
@ -472,7 +471,7 @@ describe('Settings', () => {
|
||||
};
|
||||
|
||||
const contentGroups = {
|
||||
'1': {
|
||||
1: {
|
||||
friendly_name: '123',
|
||||
devices: [],
|
||||
},
|
||||
@ -523,7 +522,7 @@ describe('Settings', () => {
|
||||
|
||||
const added = settings.addGroup('test123');
|
||||
const expected = {
|
||||
'1': {
|
||||
1: {
|
||||
friendly_name: 'test123',
|
||||
},
|
||||
};
|
||||
@ -536,7 +535,7 @@ describe('Settings', () => {
|
||||
|
||||
const added = settings.addGroup('test123', 123);
|
||||
const expected = {
|
||||
'123': {
|
||||
123: {
|
||||
friendly_name: 'test123',
|
||||
},
|
||||
};
|
||||
@ -560,7 +559,7 @@ describe('Settings', () => {
|
||||
settings.addGroup('test123');
|
||||
}).toThrow(new Error("friendly_name 'test123' is already in use"));
|
||||
const expected = {
|
||||
'1': {
|
||||
1: {
|
||||
friendly_name: 'test123',
|
||||
},
|
||||
};
|
||||
@ -576,7 +575,7 @@ describe('Settings', () => {
|
||||
settings.addGroup('test_id_123', 123);
|
||||
}).toThrow(new Error("Group ID '123' is already in use"));
|
||||
const expected = {
|
||||
'123': {
|
||||
123: {
|
||||
friendly_name: 'test123',
|
||||
},
|
||||
};
|
||||
@ -590,14 +589,14 @@ describe('Settings', () => {
|
||||
'0x123': {
|
||||
friendly_name: 'bulb',
|
||||
retain: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
settings.addGroup('test123');
|
||||
settings.addDeviceToGroup('test123', ['0x123']);
|
||||
const expected = {
|
||||
'1': {
|
||||
1: {
|
||||
friendly_name: 'test123',
|
||||
devices: ['0x123'],
|
||||
},
|
||||
@ -612,19 +611,19 @@ describe('Settings', () => {
|
||||
'0x123': {
|
||||
friendly_name: 'bulb',
|
||||
retain: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
'1': {
|
||||
1: {
|
||||
friendly_name: 'test123',
|
||||
devices: ['0x123'],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
settings.removeDeviceFromGroup('test123', ['0x123']);
|
||||
const expected = {
|
||||
'1': {
|
||||
1: {
|
||||
friendly_name: 'test123',
|
||||
devices: [],
|
||||
},
|
||||
@ -639,18 +638,18 @@ describe('Settings', () => {
|
||||
'0x123': {
|
||||
friendly_name: 'bulb',
|
||||
retain: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
'1': {
|
||||
1: {
|
||||
friendly_name: 'test123',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
settings.removeDeviceFromGroup('test123', ['0x123']);
|
||||
const expected = {
|
||||
'1': {
|
||||
1: {
|
||||
friendly_name: 'test123',
|
||||
},
|
||||
};
|
||||
@ -664,12 +663,12 @@ describe('Settings', () => {
|
||||
'0x123': {
|
||||
friendly_name: 'bulb',
|
||||
retain: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
settings.removeDeviceFromGroup('test123', 'bulb')
|
||||
settings.removeDeviceFromGroup('test123', 'bulb');
|
||||
}).toThrow(new Error("Group 'test123' does not exist"));
|
||||
});
|
||||
|
||||
@ -679,12 +678,12 @@ describe('Settings', () => {
|
||||
'0x123': {
|
||||
friendly_name: 'bulb',
|
||||
retain: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
settings.addDevice('0x123')
|
||||
settings.addDevice('0x123');
|
||||
}).toThrow(new Error("Device '0x123' already exists"));
|
||||
});
|
||||
|
||||
@ -735,7 +734,6 @@ describe('Settings', () => {
|
||||
expect(settings.validate()).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
it('Should not allow retention configuration without MQTT v5', () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
@ -782,10 +780,13 @@ describe('Settings', () => {
|
||||
});
|
||||
|
||||
it('Should throw error when yaml file is invalid', () => {
|
||||
fs.writeFileSync(configurationFile, `
|
||||
fs.writeFileSync(
|
||||
configurationFile,
|
||||
`
|
||||
good: 9
|
||||
\t wrong
|
||||
`)
|
||||
`,
|
||||
);
|
||||
|
||||
settings.testing.clear();
|
||||
const error = `Your YAML file: '${configurationFile}' is invalid (use https://jsonformatter.org/yaml-validator to find and fix the issue)`;
|
||||
@ -813,7 +814,7 @@ describe('Settings', () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'myname', retain: false}},
|
||||
groups: {'1': {friendly_name: 'myname', retain: false}},
|
||||
groups: {1: {friendly_name: 'myname', retain: false}},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
@ -886,7 +887,7 @@ describe('Settings', () => {
|
||||
write(configurationFile, {
|
||||
devices: {
|
||||
'0x0017880104e45519': {friendly_name: 'myname', retain: false},
|
||||
'0x0017880104e45511': {friendly_name: 'myname1', retain: false}
|
||||
'0x0017880104e45511': {friendly_name: 'myname1', retain: false},
|
||||
},
|
||||
});
|
||||
|
||||
@ -901,7 +902,7 @@ describe('Settings', () => {
|
||||
write(configurationFile, {
|
||||
devices: {
|
||||
'0x0017880104e45519': {friendly_name: 'myname', retain: false},
|
||||
'0x0017880104e45511': {friendly_name: 'myname1', retain: false}
|
||||
'0x0017880104e45511': {friendly_name: 'myname1', retain: false},
|
||||
},
|
||||
});
|
||||
|
||||
@ -925,48 +926,48 @@ describe('Settings', () => {
|
||||
|
||||
it('Should keep homeassistant null property on device setting change', () => {
|
||||
write(configurationFile, {
|
||||
devices: {
|
||||
'0x12345678': {
|
||||
friendly_name: 'custom discovery',
|
||||
homeassistant: {
|
||||
entityXYZ: {
|
||||
entity_category: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
settings.changeEntityOptions('0x12345678',{disabled: true});
|
||||
devices: {
|
||||
'0x12345678': {
|
||||
friendly_name: 'custom discovery',
|
||||
homeassistant: {
|
||||
entityXYZ: {
|
||||
entity_category: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
settings.changeEntityOptions('0x12345678', {disabled: true});
|
||||
|
||||
const actual = read(configurationFile);
|
||||
const expected = {
|
||||
devices: {
|
||||
'0x12345678': {
|
||||
friendly_name: 'custom discovery',
|
||||
disabled: true,
|
||||
homeassistant: {
|
||||
entityXYZ: {
|
||||
entity_category: null,
|
||||
}
|
||||
}
|
||||
devices: {
|
||||
'0x12345678': {
|
||||
friendly_name: 'custom discovery',
|
||||
disabled: true,
|
||||
homeassistant: {
|
||||
entityXYZ: {
|
||||
entity_category: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should keep homeassistant null properties on apply', async () => {
|
||||
write(configurationFile, {
|
||||
device_options: {
|
||||
homeassistant: {temperature: null},
|
||||
},
|
||||
devices: {
|
||||
'0x1234567812345678': {
|
||||
friendly_name: 'custom discovery',
|
||||
homeassistant: {humidity: null},
|
||||
}
|
||||
}
|
||||
});
|
||||
device_options: {
|
||||
homeassistant: {temperature: null},
|
||||
},
|
||||
devices: {
|
||||
'0x1234567812345678': {
|
||||
friendly_name: 'custom discovery',
|
||||
homeassistant: {humidity: null},
|
||||
},
|
||||
},
|
||||
});
|
||||
settings.reRead();
|
||||
settings.apply({permit_join: false});
|
||||
expect(settings.get().device_options.homeassistant).toStrictEqual({temperature: null});
|
||||
@ -974,86 +975,76 @@ describe('Settings', () => {
|
||||
});
|
||||
|
||||
it('Frontend config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
frontend: true,
|
||||
});
|
||||
write(configurationFile, {...minimalConfig, frontend: true});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().frontend).toStrictEqual({port: 8080, auth_token: false})
|
||||
expect(settings.get().frontend).toStrictEqual({port: 8080, auth_token: false});
|
||||
});
|
||||
|
||||
it('Baudrate config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
advanced: {baudrate: 20},
|
||||
});
|
||||
write(configurationFile, {...minimalConfig, advanced: {baudrate: 20}});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().serial.baudrate).toStrictEqual(20)
|
||||
expect(settings.get().serial.baudrate).toStrictEqual(20);
|
||||
});
|
||||
|
||||
it('ikea_ota_use_test_url config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
advanced: {ikea_ota_use_test_url: true},
|
||||
});
|
||||
write(configurationFile, {...minimalConfig, advanced: {ikea_ota_use_test_url: true}});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().ota.ikea_ota_use_test_url).toStrictEqual(true)
|
||||
expect(settings.get().ota.ikea_ota_use_test_url).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('transmit_power config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
experimental: {transmit_power: 1337},
|
||||
});
|
||||
write(configurationFile, {...minimalConfig, experimental: {transmit_power: 1337}});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().advanced.transmit_power).toStrictEqual(1337)
|
||||
expect(settings.get().advanced.transmit_power).toStrictEqual(1337);
|
||||
});
|
||||
|
||||
it('output config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
experimental: {output: 'json'},
|
||||
});
|
||||
write(configurationFile, {...minimalConfig, experimental: {output: 'json'}});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().advanced.output).toStrictEqual('json')
|
||||
expect(settings.get().advanced.output).toStrictEqual('json');
|
||||
});
|
||||
|
||||
it('Baudrartsctste config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
advanced: {rtscts: true},
|
||||
});
|
||||
write(configurationFile, {...minimalConfig, advanced: {rtscts: true}});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().serial.rtscts).toStrictEqual(true)
|
||||
expect(settings.get().serial.rtscts).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('Deprecated: Home Assistant config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
homeassistant: {discovery_topic: 'new'},
|
||||
advanced: {homeassistant_discovery_topic: 'old', homeassistant_status_topic: 'olds'},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().homeassistant).toStrictEqual({discovery_topic: 'new', legacy_entity_attributes: true, legacy_triggers: true, status_topic: 'olds'})
|
||||
expect(settings.get().homeassistant).toStrictEqual({
|
||||
discovery_topic: 'new',
|
||||
legacy_entity_attributes: true,
|
||||
legacy_triggers: true,
|
||||
status_topic: 'olds',
|
||||
});
|
||||
});
|
||||
|
||||
it('Deprecated: ban/whitelist config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
ban: ['ban'], whitelist: ['whitelist'], passlist: ['passlist'], blocklist: ['blocklist']
|
||||
});
|
||||
write(configurationFile, {...minimalConfig, ban: ['ban'], whitelist: ['whitelist'], passlist: ['passlist'], blocklist: ['blocklist']});
|
||||
|
||||
settings.reRead();
|
||||
expect(settings.get().blocklist).toStrictEqual(['blocklist', 'ban'])
|
||||
expect(settings.get().passlist).toStrictEqual(['passlist', 'whitelist'])
|
||||
expect(settings.get().blocklist).toStrictEqual(['blocklist', 'ban']);
|
||||
expect(settings.get().passlist).toStrictEqual(['passlist', 'whitelist']);
|
||||
});
|
||||
|
||||
it('Deprecated: warn log level', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
advanced: {log_level: 'warn'}
|
||||
});
|
||||
write(configurationFile, {...minimalConfig, advanced: {log_level: 'warn'}});
|
||||
|
||||
settings.reRead();
|
||||
|
||||
expect(settings.get().advanced.log_level).toStrictEqual('warning')
|
||||
})
|
||||
expect(settings.get().advanced.log_level).toStrictEqual('warning');
|
||||
});
|
||||
});
|
||||
|
@ -12,121 +12,121 @@ function writeDefaultConfiguration() {
|
||||
homeassistant: false,
|
||||
permit_join: true,
|
||||
mqtt: {
|
||||
base_topic: "zigbee2mqtt",
|
||||
server: "mqtt://localhost",
|
||||
base_topic: 'zigbee2mqtt',
|
||||
server: 'mqtt://localhost',
|
||||
},
|
||||
serial: {
|
||||
"port": "/dev/dummy",
|
||||
port: '/dev/dummy',
|
||||
},
|
||||
devices: {
|
||||
"0x000b57fffec6a5b2": {
|
||||
'0x000b57fffec6a5b2': {
|
||||
retain: true,
|
||||
friendly_name: "bulb",
|
||||
description: "this is my bulb",
|
||||
friendly_name: 'bulb',
|
||||
description: 'this is my bulb',
|
||||
},
|
||||
"0x0017880104e45517": {
|
||||
'0x0017880104e45517': {
|
||||
retain: true,
|
||||
friendly_name: "remote"
|
||||
friendly_name: 'remote',
|
||||
},
|
||||
"0x0017880104e45520": {
|
||||
'0x0017880104e45520': {
|
||||
retain: false,
|
||||
friendly_name: "button"
|
||||
friendly_name: 'button',
|
||||
},
|
||||
"0x0017880104e45521": {
|
||||
'0x0017880104e45521': {
|
||||
retain: false,
|
||||
friendly_name: "button_double_key"
|
||||
friendly_name: 'button_double_key',
|
||||
},
|
||||
"0x0017880104e45522": {
|
||||
'0x0017880104e45522': {
|
||||
qos: 1,
|
||||
retain: false,
|
||||
friendly_name: "weather_sensor"
|
||||
friendly_name: 'weather_sensor',
|
||||
},
|
||||
"0x0017880104e45523": {
|
||||
'0x0017880104e45523': {
|
||||
retain: false,
|
||||
friendly_name: "occupancy_sensor"
|
||||
friendly_name: 'occupancy_sensor',
|
||||
},
|
||||
"0x0017880104e45524": {
|
||||
'0x0017880104e45524': {
|
||||
retain: false,
|
||||
friendly_name: "power_plug"
|
||||
friendly_name: 'power_plug',
|
||||
},
|
||||
"0x0017880104e45530": {
|
||||
'0x0017880104e45530': {
|
||||
retain: false,
|
||||
friendly_name: "button_double_key_interviewing"
|
||||
friendly_name: 'button_double_key_interviewing',
|
||||
},
|
||||
"0x0017880104e45540": {
|
||||
friendly_name: "ikea_onoff"
|
||||
'0x0017880104e45540': {
|
||||
friendly_name: 'ikea_onoff',
|
||||
},
|
||||
'0x000b57fffec6a5b7': {
|
||||
retain: false,
|
||||
friendly_name: "bulb_2"
|
||||
friendly_name: 'bulb_2',
|
||||
},
|
||||
"0x000b57fffec6a5b3": {
|
||||
'0x000b57fffec6a5b3': {
|
||||
retain: false,
|
||||
friendly_name: "bulb_color"
|
||||
friendly_name: 'bulb_color',
|
||||
},
|
||||
'0x000b57fffec6a5b4': {
|
||||
retain: false,
|
||||
friendly_name: "bulb_color_2"
|
||||
friendly_name: 'bulb_color_2',
|
||||
},
|
||||
"0x0017880104e45541": {
|
||||
'0x0017880104e45541': {
|
||||
retain: false,
|
||||
friendly_name: "wall_switch"
|
||||
friendly_name: 'wall_switch',
|
||||
},
|
||||
"0x0017880104e45542": {
|
||||
'0x0017880104e45542': {
|
||||
retain: false,
|
||||
friendly_name: "wall_switch_double"
|
||||
friendly_name: 'wall_switch_double',
|
||||
},
|
||||
"0x0017880104e45543": {
|
||||
'0x0017880104e45543': {
|
||||
retain: false,
|
||||
friendly_name: "led_controller_1"
|
||||
friendly_name: 'led_controller_1',
|
||||
},
|
||||
"0x0017880104e45544": {
|
||||
'0x0017880104e45544': {
|
||||
retain: false,
|
||||
friendly_name: "led_controller_2"
|
||||
friendly_name: 'led_controller_2',
|
||||
},
|
||||
'0x0017880104e45545': {
|
||||
retain: false,
|
||||
friendly_name: "dimmer_wall_switch"
|
||||
friendly_name: 'dimmer_wall_switch',
|
||||
},
|
||||
'0x0017880104e45547': {
|
||||
retain: false,
|
||||
friendly_name: "curtain"
|
||||
friendly_name: 'curtain',
|
||||
},
|
||||
'0x0017880104e45548': {
|
||||
retain: false,
|
||||
friendly_name: 'fan'
|
||||
friendly_name: 'fan',
|
||||
},
|
||||
'0x0017880104e45549': {
|
||||
retain: false,
|
||||
friendly_name: 'siren'
|
||||
friendly_name: 'siren',
|
||||
},
|
||||
'0x0017880104e45529': {
|
||||
retain: false,
|
||||
friendly_name: 'unsupported2'
|
||||
friendly_name: 'unsupported2',
|
||||
},
|
||||
'0x0017880104e45550': {
|
||||
retain: false,
|
||||
friendly_name: 'thermostat'
|
||||
friendly_name: 'thermostat',
|
||||
},
|
||||
'0x0017880104e45551': {
|
||||
retain: false,
|
||||
friendly_name: 'smart vent'
|
||||
friendly_name: 'smart vent',
|
||||
},
|
||||
'0x0017880104e45552': {
|
||||
retain: false,
|
||||
friendly_name: 'j1'
|
||||
friendly_name: 'j1',
|
||||
},
|
||||
'0x0017880104e45553': {
|
||||
retain: false,
|
||||
friendly_name: 'bulb_enddevice'
|
||||
friendly_name: 'bulb_enddevice',
|
||||
},
|
||||
'0x0017880104e45559': {
|
||||
retain: false,
|
||||
friendly_name: 'cc2530_router'
|
||||
friendly_name: 'cc2530_router',
|
||||
},
|
||||
'0x0017880104e45560': {
|
||||
retain: false,
|
||||
friendly_name: 'livolo'
|
||||
friendly_name: 'livolo',
|
||||
},
|
||||
'0x90fd9ffffe4b64ae': {
|
||||
retain: false,
|
||||
@ -151,7 +151,7 @@ function writeDefaultConfiguration() {
|
||||
friendly_name: 'GL-S-007ZS',
|
||||
},
|
||||
'0x0017880104e43559': {
|
||||
friendly_name: 'U202DST600ZB'
|
||||
friendly_name: 'U202DST600ZB',
|
||||
},
|
||||
'0xf4ce368a38be56a1': {
|
||||
retain: false,
|
||||
@ -196,44 +196,44 @@ function writeDefaultConfiguration() {
|
||||
},
|
||||
'0x0017880104e45562': {
|
||||
friendly_name: 'heating_actuator',
|
||||
}
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
'1': {
|
||||
1: {
|
||||
friendly_name: 'group_1',
|
||||
retain: false,
|
||||
},
|
||||
'2': {
|
||||
2: {
|
||||
friendly_name: 'group_2',
|
||||
retain: false,
|
||||
},
|
||||
'15071': {
|
||||
15071: {
|
||||
friendly_name: 'group_tradfri_remote',
|
||||
retain: false,
|
||||
devices: ['bulb_color_2', 'bulb_2']
|
||||
devices: ['bulb_color_2', 'bulb_2'],
|
||||
},
|
||||
'11': {
|
||||
11: {
|
||||
friendly_name: 'group_with_tradfri',
|
||||
retain: false,
|
||||
devices: ['bulb_2']
|
||||
devices: ['bulb_2'],
|
||||
},
|
||||
'12': {
|
||||
12: {
|
||||
friendly_name: 'thermostat_group',
|
||||
retain: false,
|
||||
devices: ['TS0601_thermostat'],
|
||||
},
|
||||
'14': {
|
||||
14: {
|
||||
friendly_name: 'switch_group',
|
||||
retain: false,
|
||||
devices: ['power_plug', 'bulb_2'],
|
||||
},
|
||||
'21': {
|
||||
21: {
|
||||
friendly_name: 'gledopto_group',
|
||||
devices: ['GLEDOPTO_2ID/cct'],
|
||||
},
|
||||
'9': {
|
||||
9: {
|
||||
friendly_name: 'ha_discovery_group',
|
||||
devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right']
|
||||
devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'],
|
||||
},
|
||||
},
|
||||
external_converters: [],
|
||||
@ -257,19 +257,19 @@ function stateExists() {
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
"0x000b57fffec6a5b2": {
|
||||
"state": "ON",
|
||||
"brightness": 50,
|
||||
"color_temp": 370,
|
||||
"linkquality": 99,
|
||||
'0x000b57fffec6a5b2': {
|
||||
state: 'ON',
|
||||
brightness: 50,
|
||||
color_temp: 370,
|
||||
linkquality: 99,
|
||||
},
|
||||
"0x0017880104e45517": {
|
||||
"brightness": 255
|
||||
'0x0017880104e45517': {
|
||||
brightness: 255,
|
||||
},
|
||||
"1": {
|
||||
'state': 'ON',
|
||||
}
|
||||
}
|
||||
1: {
|
||||
state: 'ON',
|
||||
},
|
||||
};
|
||||
|
||||
function getDefaultState() {
|
||||
return defaultState;
|
||||
|
@ -11,7 +11,7 @@ const callTransports = (level, message, namespace) => {
|
||||
transport.log({level, message, namespace}, () => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mock = {
|
||||
init: jest.fn(),
|
||||
@ -26,16 +26,24 @@ const mock = {
|
||||
removeTransport: (transport) => {
|
||||
transports = transports.filter((t) => t !== transport);
|
||||
},
|
||||
setLevel: (newLevel) => {level = newLevel},
|
||||
setLevel: (newLevel) => {
|
||||
level = newLevel;
|
||||
},
|
||||
getLevel: () => level,
|
||||
setNamespacedLevels: (nsLevels) => {namespacedLevels = nsLevels},
|
||||
setNamespacedLevels: (nsLevels) => {
|
||||
namespacedLevels = nsLevels;
|
||||
},
|
||||
getNamespacedLevels: () => namespacedLevels,
|
||||
setDebugNamespaceIgnore: (newIgnore) => {debugNamespaceIgnore = newIgnore},
|
||||
setDebugNamespaceIgnore: (newIgnore) => {
|
||||
debugNamespaceIgnore = newIgnore;
|
||||
},
|
||||
getDebugNamespaceIgnore: () => debugNamespaceIgnore,
|
||||
setTransportsEnabled: (value) => {transportsEnabled = value},
|
||||
setTransportsEnabled: (value) => {
|
||||
transportsEnabled = value;
|
||||
},
|
||||
end: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('../../lib/util/logger', () => (mock));
|
||||
jest.mock('../../lib/util/logger', () => mock);
|
||||
|
||||
module.exports = {...mock};
|
||||
|
@ -7,13 +7,13 @@ const mock = {
|
||||
unsubscribe: jest.fn(),
|
||||
reconnecting: false,
|
||||
on: jest.fn(),
|
||||
stream: {setMaxListeners: jest.fn()}
|
||||
stream: {setMaxListeners: jest.fn()},
|
||||
};
|
||||
|
||||
const mockConnect = jest.fn().mockReturnValue(mock);
|
||||
|
||||
jest.mock('mqtt', () => {
|
||||
return {connect: mockConnect};
|
||||
return {connect: mockConnect};
|
||||
});
|
||||
|
||||
const restoreOnMock = () => {
|
||||
@ -22,12 +22,16 @@ const restoreOnMock = () => {
|
||||
handler();
|
||||
}
|
||||
|
||||
events[type] = handler
|
||||
events[type] = handler;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
restoreOnMock();
|
||||
|
||||
module.exports = {
|
||||
events, ...mock, connect: mockConnect, mock, restoreOnMock
|
||||
};
|
||||
events,
|
||||
...mock,
|
||||
connect: mockConnect,
|
||||
mock,
|
||||
restoreOnMock,
|
||||
};
|
||||
|
@ -19,20 +19,20 @@ class Group {
|
||||
}
|
||||
|
||||
const clusters = {
|
||||
'genBasic': 0,
|
||||
'genOta': 25,
|
||||
'genScenes': 5,
|
||||
'genOnOff': 6,
|
||||
'genLevelCtrl': 8,
|
||||
'lightingColorCtrl': 768,
|
||||
'closuresWindowCovering': 258,
|
||||
'hvacThermostat': 513,
|
||||
'msIlluminanceMeasurement': 1024,
|
||||
'msTemperatureMeasurement': 1026,
|
||||
'msRelativeHumidity': 1029,
|
||||
'msSoilMoisture': 1032,
|
||||
'msCO2': 1037
|
||||
}
|
||||
genBasic: 0,
|
||||
genOta: 25,
|
||||
genScenes: 5,
|
||||
genOnOff: 6,
|
||||
genLevelCtrl: 8,
|
||||
lightingColorCtrl: 768,
|
||||
closuresWindowCovering: 258,
|
||||
hvacThermostat: 513,
|
||||
msIlluminanceMeasurement: 1024,
|
||||
msTemperatureMeasurement: 1026,
|
||||
msRelativeHumidity: 1029,
|
||||
msSoilMoisture: 1032,
|
||||
msCO2: 1037,
|
||||
};
|
||||
|
||||
const custom_clusters = {
|
||||
custom_1: {
|
||||
@ -42,14 +42,25 @@ const custom_clusters = {
|
||||
attribute_0: {ID: 0, type: 49},
|
||||
},
|
||||
commands: {
|
||||
command_0: { ID: 0, response: 0, parameters: [{name: 'reset', type: 40}], },
|
||||
command_0: {ID: 0, response: 0, parameters: [{name: 'reset', type: 40}]},
|
||||
},
|
||||
commandsResponse: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
class Endpoint {
|
||||
constructor(ID, inputClusters, outputClusters, deviceIeeeAddress, binds=[], clusterValues={}, configuredReportings=[], profileID=null, deviceID=null, meta={}) {
|
||||
constructor(
|
||||
ID,
|
||||
inputClusters,
|
||||
outputClusters,
|
||||
deviceIeeeAddress,
|
||||
binds = [],
|
||||
clusterValues = {},
|
||||
configuredReportings = [],
|
||||
profileID = null,
|
||||
deviceID = null,
|
||||
meta = {},
|
||||
) {
|
||||
this.deviceIeeeAddress = deviceIeeeAddress;
|
||||
this.clusterValues = clusterValues;
|
||||
this.ID = ID;
|
||||
@ -68,32 +79,38 @@ class Endpoint {
|
||||
this.profileID = profileID;
|
||||
this.deviceID = deviceID;
|
||||
this.configuredReportings = configuredReportings;
|
||||
this.getInputClusters = () => inputClusters.map((c) => {
|
||||
return {ID: c, name: getKeyByValue(clusters, c)};
|
||||
}).filter((c) => c.name);
|
||||
this.getInputClusters = () =>
|
||||
inputClusters
|
||||
.map((c) => {
|
||||
return {ID: c, name: getKeyByValue(clusters, c)};
|
||||
})
|
||||
.filter((c) => c.name);
|
||||
|
||||
this.getOutputClusters = () => outputClusters.map((c) => {
|
||||
return {ID: c, name: getKeyByValue(clusters, c)};
|
||||
}).filter((c) => c.name);
|
||||
this.getOutputClusters = () =>
|
||||
outputClusters
|
||||
.map((c) => {
|
||||
return {ID: c, name: getKeyByValue(clusters, c)};
|
||||
})
|
||||
.filter((c) => c.name);
|
||||
|
||||
this.supportsInputCluster = (cluster) => {
|
||||
assert(clusters[cluster] !== undefined, `Undefined '${cluster}'`);
|
||||
return this.inputClusters.includes(clusters[cluster]);
|
||||
}
|
||||
};
|
||||
|
||||
this.supportsOutputCluster = (cluster) => {
|
||||
assert(clusters[cluster], `Undefined '${cluster}'`);
|
||||
return this.outputClusters.includes(clusters[cluster]);
|
||||
}
|
||||
};
|
||||
|
||||
this.addToGroup = jest.fn();
|
||||
this.addToGroup.mockImplementation((group) => {
|
||||
if (!group.members.includes(this)) group.members.push(this);
|
||||
})
|
||||
});
|
||||
|
||||
this.getDevice = () => {
|
||||
return Object.values(devices).find(d => d.ieeeAddr === deviceIeeeAddress);
|
||||
}
|
||||
return Object.values(devices).find((d) => d.ieeeAddr === deviceIeeeAddress);
|
||||
};
|
||||
|
||||
this.removeFromGroup = jest.fn();
|
||||
this.removeFromGroup.mockImplementation((group) => {
|
||||
@ -104,8 +121,8 @@ class Endpoint {
|
||||
});
|
||||
|
||||
this.removeFromAllGroups = () => {
|
||||
Object.values(groups).forEach((g) => this.removeFromGroup(g))
|
||||
}
|
||||
Object.values(groups).forEach((g) => this.removeFromGroup(g));
|
||||
};
|
||||
|
||||
this.getClusterAttributeValue = jest.fn();
|
||||
this.getClusterAttributeValue.mockImplementation((cluster, value) => {
|
||||
@ -116,7 +133,21 @@ class Endpoint {
|
||||
}
|
||||
|
||||
class Device {
|
||||
constructor(type, ieeeAddr, networkAddress, manufacturerID, endpoints, interviewCompleted, powerSource = null, modelID = null, interviewing=false, manufacturerName, dateCode= null, softwareBuildID=null, customClusters = {}) {
|
||||
constructor(
|
||||
type,
|
||||
ieeeAddr,
|
||||
networkAddress,
|
||||
manufacturerID,
|
||||
endpoints,
|
||||
interviewCompleted,
|
||||
powerSource = null,
|
||||
modelID = null,
|
||||
interviewing = false,
|
||||
manufacturerName,
|
||||
dateCode = null,
|
||||
softwareBuildID = null,
|
||||
customClusters = {},
|
||||
) {
|
||||
this.type = type;
|
||||
this.ieeeAddr = ieeeAddr;
|
||||
this.dateCode = dateCode;
|
||||
@ -147,88 +178,624 @@ class Device {
|
||||
|
||||
const returnDevices = [];
|
||||
|
||||
const bulb_color = new Device('Router', '0x000b57fffec6a5b3', 40399, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b3', [], {lightingColorCtrl: {colorCapabilities: 254}})], true, "Mains (single phase)", "LLC020");
|
||||
const bulb_color_2 = new Device('Router', '0x000b57fffec6a5b4', 401292, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b4', [], {lightingColorCtrl: {colorCapabilities: 254}}, [], null, null, {'scenes': {'1_0': {name: 'Chill scene', state: {state: 'ON'}}, '4_9': {state: {state: 'OFF'}}}})], true, "Mains (single phase)", "LLC020", false, 'Philips', '2019.09', '5.127.1.26581');
|
||||
const bulb_2 = new Device('Router', '0x000b57fffec6a5b7', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b7', [], {lightingColorCtrl: {colorCapabilities: 17}})], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm");
|
||||
const TS0601_thermostat = new Device('EndDevice', '0x0017882104a44559', 6544,4151, [new Endpoint(1, [], [], '0x0017882104a44559')], true, "Mains (single phase)", 'kud7u2l');
|
||||
const TS0601_switch = new Device('EndDevice', '0x0017882104a44560', 6544,4151, [new Endpoint(1, [], [], '0x0017882104a44560')], true, "Mains (single phase)", 'kjintbl');
|
||||
const TS0601_cover_switch = new Device('EndDevice', '0x0017882104a44562', 6544,4151, [new Endpoint(1, [], [], '0x0017882104a44562')], true, "Mains (single phase)", 'TS0601', false, '_TZE200_5nldle7w');
|
||||
const ZNCZ02LM = new Device('Router', '0x0017880104e45524', 6540,4151, [new Endpoint(1, [0, 6], [], '0x0017880104e45524')], true, "Mains (single phase)", "lumi.plug");
|
||||
const GLEDOPTO_2ID = new Device('Router', '0x0017880104e45724', 6540,4151, [new Endpoint(11, [0,3,4,5,6,8,768], [], '0x0017880104e45724', [], {}, [], 49246, 528), new Endpoint(12, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 260, 258), new Endpoint(13, [4096], [4096], '0x0017880104e45724', [], {}, [], 49246, 57694), new Endpoint(15, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 49246, 256)], true, "Mains (single phase)", 'GL-C-007', false, 'GLEDOPTO');
|
||||
const QBKG03LM = new Device('Router', '0x0017880104e45542', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45542'), new Endpoint(2, [0, 6], [], '0x0017880104e45542'), new Endpoint(3, [0, 6], [], '0x0017880104e45542')], true, "Mains (single phase)", 'lumi.ctrl_neutral2');
|
||||
const zigfred_plus = new Device('Router', '0xf4ce368a38be56a1', 6589, 0x129C, [new Endpoint(5, [0, 3, 4, 5, 6, 8, 0x0300, 0xFC42], [0xFC42], '0xf4ce368a38be56a1'), new Endpoint(7, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(8, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(9, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(10, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(11, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1'), new Endpoint(12, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1')], true, "Mains (single phase)", 'zigfred plus', false, 'Siglis');
|
||||
const bulb_color = new Device(
|
||||
'Router',
|
||||
'0x000b57fffec6a5b3',
|
||||
40399,
|
||||
4107,
|
||||
[
|
||||
new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], '0x000b57fffec6a5b3', [], {
|
||||
lightingColorCtrl: {colorCapabilities: 254},
|
||||
}),
|
||||
],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'LLC020',
|
||||
);
|
||||
const bulb_color_2 = new Device(
|
||||
'Router',
|
||||
'0x000b57fffec6a5b4',
|
||||
401292,
|
||||
4107,
|
||||
[
|
||||
new Endpoint(
|
||||
1,
|
||||
[0, 3, 4, 5, 6, 8, 768, 2821, 4096],
|
||||
[5, 25, 32, 4096],
|
||||
'0x000b57fffec6a5b4',
|
||||
[],
|
||||
{lightingColorCtrl: {colorCapabilities: 254}},
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
{scenes: {'1_0': {name: 'Chill scene', state: {state: 'ON'}}, '4_9': {state: {state: 'OFF'}}}},
|
||||
),
|
||||
],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'LLC020',
|
||||
false,
|
||||
'Philips',
|
||||
'2019.09',
|
||||
'5.127.1.26581',
|
||||
);
|
||||
const bulb_2 = new Device(
|
||||
'Router',
|
||||
'0x000b57fffec6a5b7',
|
||||
40369,
|
||||
4476,
|
||||
[new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], '0x000b57fffec6a5b7', [], {lightingColorCtrl: {colorCapabilities: 17}})],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'TRADFRI bulb E27 WS opal 980lm',
|
||||
);
|
||||
const TS0601_thermostat = new Device(
|
||||
'EndDevice',
|
||||
'0x0017882104a44559',
|
||||
6544,
|
||||
4151,
|
||||
[new Endpoint(1, [], [], '0x0017882104a44559')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'kud7u2l',
|
||||
);
|
||||
const TS0601_switch = new Device(
|
||||
'EndDevice',
|
||||
'0x0017882104a44560',
|
||||
6544,
|
||||
4151,
|
||||
[new Endpoint(1, [], [], '0x0017882104a44560')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'kjintbl',
|
||||
);
|
||||
const TS0601_cover_switch = new Device(
|
||||
'EndDevice',
|
||||
'0x0017882104a44562',
|
||||
6544,
|
||||
4151,
|
||||
[new Endpoint(1, [], [], '0x0017882104a44562')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'TS0601',
|
||||
false,
|
||||
'_TZE200_5nldle7w',
|
||||
);
|
||||
const ZNCZ02LM = new Device(
|
||||
'Router',
|
||||
'0x0017880104e45524',
|
||||
6540,
|
||||
4151,
|
||||
[new Endpoint(1, [0, 6], [], '0x0017880104e45524')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'lumi.plug',
|
||||
);
|
||||
const GLEDOPTO_2ID = new Device(
|
||||
'Router',
|
||||
'0x0017880104e45724',
|
||||
6540,
|
||||
4151,
|
||||
[
|
||||
new Endpoint(11, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 49246, 528),
|
||||
new Endpoint(12, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 260, 258),
|
||||
new Endpoint(13, [4096], [4096], '0x0017880104e45724', [], {}, [], 49246, 57694),
|
||||
new Endpoint(15, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 49246, 256),
|
||||
],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'GL-C-007',
|
||||
false,
|
||||
'GLEDOPTO',
|
||||
);
|
||||
const QBKG03LM = new Device(
|
||||
'Router',
|
||||
'0x0017880104e45542',
|
||||
6540,
|
||||
4151,
|
||||
[
|
||||
new Endpoint(1, [0], [], '0x0017880104e45542'),
|
||||
new Endpoint(2, [0, 6], [], '0x0017880104e45542'),
|
||||
new Endpoint(3, [0, 6], [], '0x0017880104e45542'),
|
||||
],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'lumi.ctrl_neutral2',
|
||||
);
|
||||
const zigfred_plus = new Device(
|
||||
'Router',
|
||||
'0xf4ce368a38be56a1',
|
||||
6589,
|
||||
0x129c,
|
||||
[
|
||||
new Endpoint(5, [0, 3, 4, 5, 6, 8, 0x0300, 0xfc42], [0xfc42], '0xf4ce368a38be56a1'),
|
||||
new Endpoint(7, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'),
|
||||
new Endpoint(8, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'),
|
||||
new Endpoint(9, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'),
|
||||
new Endpoint(10, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'),
|
||||
new Endpoint(11, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1'),
|
||||
new Endpoint(12, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1'),
|
||||
],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'zigfred plus',
|
||||
false,
|
||||
'Siglis',
|
||||
);
|
||||
|
||||
const groups = {
|
||||
'group_1': new Group(1, []),
|
||||
'group_tradfri_remote': new Group(15071, [bulb_color_2.endpoints[0], bulb_2.endpoints[0]]),
|
||||
group_1: new Group(1, []),
|
||||
group_tradfri_remote: new Group(15071, [bulb_color_2.endpoints[0], bulb_2.endpoints[0]]),
|
||||
'group/with/slashes': new Group(99, []),
|
||||
'group_with_tradfri': new Group(11, [bulb_2.endpoints[0]]),
|
||||
'thermostat_group': new Group(12, [TS0601_thermostat.endpoints[0]]),
|
||||
'group_with_switch': new Group(14, [ZNCZ02LM.endpoints[0], bulb_2.endpoints[0]]),
|
||||
'gledopto_group': new Group(21, [GLEDOPTO_2ID.endpoints[3]]),
|
||||
'default_bind_group': new Group(901, []),
|
||||
'ha_discovery_group': new Group(9, [bulb_color_2.endpoints[0], bulb_2.endpoints[0], QBKG03LM.endpoints[1]]),
|
||||
}
|
||||
group_with_tradfri: new Group(11, [bulb_2.endpoints[0]]),
|
||||
thermostat_group: new Group(12, [TS0601_thermostat.endpoints[0]]),
|
||||
group_with_switch: new Group(14, [ZNCZ02LM.endpoints[0], bulb_2.endpoints[0]]),
|
||||
gledopto_group: new Group(21, [GLEDOPTO_2ID.endpoints[3]]),
|
||||
default_bind_group: new Group(901, []),
|
||||
ha_discovery_group: new Group(9, [bulb_color_2.endpoints[0], bulb_2.endpoints[0], QBKG03LM.endpoints[1]]),
|
||||
};
|
||||
|
||||
const devices = {
|
||||
'coordinator': new Device('Coordinator', '0x00124b00120144ae', 0, 0, [new Endpoint(1, [], [], '0x00124b00120144ae')], false),
|
||||
'bulb': new Device('Router', '0x000b57fffec6a5b2', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b2', [], {lightingColorCtrl: {colorCapabilities: 17}}, [{cluster: {name: 'genOnOff'}, attribute: {name: 'onOff'}, minimumReportInterval: 1, maximumReportInterval: 10, reportableChange: 20}])], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm"),
|
||||
'bulb_color': bulb_color,
|
||||
'bulb_2': bulb_2,
|
||||
'bulb_color_2': bulb_color_2,
|
||||
'remote': new Device('EndDevice', '0x0017880104e45517', 6535, 4107, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x0017880104e45517', [{target: bulb_color.endpoints[0], cluster: {ID: 8, name: 'genLevelCtrl'}}, {target: bulb_color.endpoints[0], cluster: {ID: 6, name: 'genOnOff'}}, {target: bulb_color.endpoints[0], cluster: {ID: 768, name: 'lightingColorCtrl'}}, {target: groups.group_1, cluster: {ID: 6, name: 'genOnOff'}}, {target: groups.group_1, cluster: {ID: 6, name: 'genLevelCtrl'}}]), new Endpoint(2, [0,1,3,15,64512], [25, 6])], true, "Battery", "RWL021"),
|
||||
'unsupported': new Device('EndDevice', '0x0017880104e45518', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID", false, "notSupportedMfg"),
|
||||
'unsupported2': new Device('EndDevice', '0x0017880104e45529', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID"),
|
||||
'interviewing': new Device('EndDevice', '0x0017880104e45530', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", undefined, true),
|
||||
'notInSettings': new Device('EndDevice', '0x0017880104e45519', 6537, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "lumi.sensor_switch.aq2"),
|
||||
'WXKG11LM': new Device('EndDevice', '0x0017880104e45520', 6537,4151, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x0017880104e45520', [], {}, [{cluster: {name: 'genOnOff'}, attribute: {name: undefined, ID: 1337}, minimumReportInterval: 1, maximumReportInterval: 10, reportableChange: 20}])], true, "Battery", "lumi.sensor_switch.aq2"),
|
||||
'WXKG02LM_rev1': new Device('EndDevice', '0x0017880104e45521', 6538,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], true, "Battery", "lumi.sensor_86sw2.es1"),
|
||||
'WSDCGQ11LM': new Device('EndDevice', '0x0017880104e45522', 6539,4151, [new Endpoint(1, [0], [])], true, "Battery", "lumi.weather"),
|
||||
'RTCGQ11LM': new Device('EndDevice', '0x0017880104e45523', 6540,4151, [new Endpoint(1, [0], [])], true, "Battery", "lumi.sensor_motion.aq2"),
|
||||
'ZNCZ02LM': ZNCZ02LM,
|
||||
'E1743': new Device('Router', '0x0017880104e45540', 6540,4476, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'TRADFRI on/off switch'),
|
||||
'QBKG04LM': new Device('Router', '0x0017880104e45541', 6549,4151, [new Endpoint(1, [0], [25]), new Endpoint(2, [0, 6], [])], true, "Mains (single phase)", 'lumi.ctrl_neutral1'),
|
||||
'QBKG03LM':QBKG03LM,
|
||||
'GLEDOPTO1112': new Device('Router', '0x0017880104e45543', 6540, 4151, [new Endpoint(11, [0], [], '0x0017880104e45543'), new Endpoint(13, [0], [], '0x0017880104e45543')], true, "Mains (single phase)", 'GL-C-008'),
|
||||
'GLEDOPTO111213': new Device('Router', '0x0017880104e45544', 6540,4151, [new Endpoint(11, [0], []), new Endpoint(13, [0], []), new Endpoint(12, [0], [])], true, "Mains (single phase)", 'GL-C-008'),
|
||||
'GLEDOPTO_2ID': GLEDOPTO_2ID,
|
||||
'HGZB04D': new Device('Router', '0x0017880104e45545', 6540,4151, [new Endpoint(1, [0], [25], '0x0017880104e45545')], true, "Mains (single phase)", 'FB56+ZSC05HG1.0'),
|
||||
'ZNCLDJ11LM': new Device('Router', '0x0017880104e45547', 6540,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], true, "Mains (single phase)", 'lumi.curtain'),
|
||||
'HAMPTON99432': new Device('Router', '0x0017880104e45548', 6540,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], true, "Mains (single phase)", 'HDC52EastwindFan'),
|
||||
'HS2WD': new Device('Router', '0x0017880104e45549', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'WarningDevice'),
|
||||
'1TST_EU': new Device('Router', '0x0017880104e45550', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'Thermostat'),
|
||||
'SV01': new Device('Router', '0x0017880104e45551', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'SV01-410-MP-1.0'),
|
||||
'J1': new Device('Router', '0x0017880104e45552', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'J1 (5502)'),
|
||||
'E11_G13': new Device('EndDevice', '0x0017880104e45553', 6540,4151, [new Endpoint(1, [0, 6], [])], true, "Mains (single phase)", 'E11-G13'),
|
||||
'nomodel': new Device('Router', '0x0017880104e45535', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Mains (single phase)", undefined, true),
|
||||
'unsupported_router': new Device('Router', '0x0017880104e45525', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Mains (single phase)", "notSupportedModelID", false, "Boef"),
|
||||
'CC2530_ROUTER': new Device('Router', '0x0017880104e45559', 6540,4151, [new Endpoint(1, [0, 6], [])], true, "Mains (single phase)", 'lumi.router'),
|
||||
'LIVOLO': new Device('Router', '0x0017880104e45560', 6541,4152, [new Endpoint(6, [0, 6], [])], true, "Mains (single phase)", 'TI0001 '),
|
||||
'tradfri_remote': new Device('EndDevice', '0x90fd9ffffe4b64ae', 33906, 4476, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x90fd9ffffe4b64ae')], true, "Battery", "TRADFRI remote control"),
|
||||
'roller_shutter': new Device('EndDevice', '0x90fd9ffffe4b64af', 33906, 4476, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x90fd9ffffe4b64af')], true, "Battery", "SCM-R_00.00.03.15TC"),
|
||||
'ZNLDP12LM': new Device('Router', '0x90fd9ffffe4b64ax', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64ax', [], {lightingColorCtrl: {colorCapabilities: 254}})], true, "Mains (single phase)", "lumi.light.aqcn02"),
|
||||
'SP600_OLD': new Device('Router', '0x90fd9ffffe4b64aa', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {seMetering: {"multiplier":1,"divisor":10000}})], true, "Mains (single phase)", "SP600", false, 'Salus', '20160120'),
|
||||
'SP600_NEW': new Device('Router', '0x90fd9ffffe4b64ab', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {seMetering: {"multiplier":1,"divisor":10000}})], true, "Mains (single phase)", "SP600", false, 'Salus', '20170220'),
|
||||
'MKS-CM-W5': new Device('Router', '0x90fd9ffffe4b64ac', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {})], true, "Mains (single phase)", "qnazj70", false),
|
||||
'GL-S-007ZS': new Device('Router', '0x0017880104e45526', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45526')], true, "Mains (single phase)", 'GL-S-007ZS'),
|
||||
'U202DST600ZB': new Device('Router', '0x0017880104e43559', 6540,4151, [new Endpoint(10, [0, 6], [], '0x0017880104e43559'), new Endpoint(11, [0, 6], [], '0x0017880104e43559')], true, "Mains (single phase)", 'U202DST600ZB'),
|
||||
'zigfred_plus': zigfred_plus,
|
||||
'3157100': new Device('Router', '0x0017880104e44559', 6542,4151, [new Endpoint(1, [], [], '0x0017880104e44559')], true, "Mains (single phase)", '3157100', false, 'Centralite'),
|
||||
'J1': new Device('Router', '0x0017880104a44559', 6543,4151, [new Endpoint(1, [], [], '0x0017880104a44559')], true, "Mains (single phase)", 'J1 (5502)'),
|
||||
'TS0601_thermostat': TS0601_thermostat,
|
||||
'TS0601_switch': TS0601_switch,
|
||||
'TS0601_cover_switch': TS0601_cover_switch,
|
||||
'external_converter_device': new Device('EndDevice', '0x0017880104e45511', 1114, 'external', [new Endpoint(1, [], [], '0x0017880104e45511')], false, null, 'external_converter_device' ),
|
||||
'QS_Zigbee_D02_TRIAC_2C_LN':new Device('Router', '0x0017882194e45543', 6549,4151, [new Endpoint(1, [0], [], '0x0017882194e45543'), new Endpoint(2, [0, 6], [], '0x0017882194e45543')], true, "Mains (single phase)", 'TS110F', false, '_TYZB01_v8gtiaed'),
|
||||
'unknown': new Device('Router', '0x0017980134e45545', 6540,4151, [], true, "Mains (single phase)"),
|
||||
'temperature_sensor': new Device('EndDevice', '0x0017880104e45561', 6544,4151, [new Endpoint(1, [0,3,4,1026], [])], true, "Battery", "temperature.sensor"),
|
||||
'heating_actuator': new Device('Router', '0x0017880104e45562', 6545,4151, [new Endpoint(1, [0,3,4,513], [1026])], true, "Mains (single phase)", "heating.actuator"),
|
||||
'bj_scene_switch': new Device('EndDevice', '0xd85def11a1002caa', 50117, 4398, [new Endpoint(10, [0,4096], [3,4,5,6,8,25,768,4096], '0xd85def11a1002caa', [{target: bulb_color_2.endpoints[0], cluster: {ID: 8, name: 'genLevelCtrl'}}, {target: bulb_color_2.endpoints[0], cluster: {ID: 6, name: 'genOnOff'}}, {target: bulb_color_2.endpoints[0], cluster: {ID: 768, name: 'lightingColorCtrl'}},]), new Endpoint(11, [0,4096], [3,4,5,6,8,25,768,4096], '0xd85def11a1002caa')], true, 'Battery', 'RB01', false, 'Busch-Jaeger', '20161222', '1.2.0'),
|
||||
'GW003-AS-IN-TE-FC': new Device('Router', '0x0017548104a44669', 6545,4699, [new Endpoint(1, [3], [0,3,513,514], '0x0017548104a44669')], true, "Mains (single phase)", 'Adapter Zigbee FUJITSU'),
|
||||
'BMCT-SLZ': new Device('Router', '0x18fc26000000cafe', 6546,4617, [new Endpoint(1, [0,3,4,5,258,1794,2820,2821,64672], [10,25], '0x18fc26000000cafe')], true, "Mains (single phase)", 'RBSH-MMS-ZB-EU'),
|
||||
'BMCT_SLZ': new Device('Router', '0x0026decafe000473', 6546,4617, [new Endpoint(1, [0,3,4,5,258,1794,2820,2821,64672], [10,25], '0x0026decafe000473')], true, "Mains (single phase)", 'RBSH-MMS-ZB-EU', false, null, null, null, custom_clusters),
|
||||
'bulb_custom_cluster': new Device('Router', '0x000b57fffec6a5c2', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5c2')], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm", false, null, null, null, custom_clusters),
|
||||
}
|
||||
coordinator: new Device('Coordinator', '0x00124b00120144ae', 0, 0, [new Endpoint(1, [], [], '0x00124b00120144ae')], false),
|
||||
bulb: new Device(
|
||||
'Router',
|
||||
'0x000b57fffec6a5b2',
|
||||
40369,
|
||||
4476,
|
||||
[
|
||||
new Endpoint(
|
||||
1,
|
||||
[0, 3, 4, 5, 6, 8, 768, 2821, 4096],
|
||||
[5, 25, 32, 4096],
|
||||
'0x000b57fffec6a5b2',
|
||||
[],
|
||||
{lightingColorCtrl: {colorCapabilities: 17}},
|
||||
[
|
||||
{
|
||||
cluster: {name: 'genOnOff'},
|
||||
attribute: {name: 'onOff'},
|
||||
minimumReportInterval: 1,
|
||||
maximumReportInterval: 10,
|
||||
reportableChange: 20,
|
||||
},
|
||||
],
|
||||
),
|
||||
],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'TRADFRI bulb E27 WS opal 980lm',
|
||||
),
|
||||
bulb_color: bulb_color,
|
||||
bulb_2: bulb_2,
|
||||
bulb_color_2: bulb_color_2,
|
||||
remote: new Device(
|
||||
'EndDevice',
|
||||
'0x0017880104e45517',
|
||||
6535,
|
||||
4107,
|
||||
[
|
||||
new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45517', [
|
||||
{target: bulb_color.endpoints[0], cluster: {ID: 8, name: 'genLevelCtrl'}},
|
||||
{target: bulb_color.endpoints[0], cluster: {ID: 6, name: 'genOnOff'}},
|
||||
{target: bulb_color.endpoints[0], cluster: {ID: 768, name: 'lightingColorCtrl'}},
|
||||
{target: groups.group_1, cluster: {ID: 6, name: 'genOnOff'}},
|
||||
{target: groups.group_1, cluster: {ID: 6, name: 'genLevelCtrl'}},
|
||||
]),
|
||||
new Endpoint(2, [0, 1, 3, 15, 64512], [25, 6]),
|
||||
],
|
||||
true,
|
||||
'Battery',
|
||||
'RWL021',
|
||||
),
|
||||
unsupported: new Device(
|
||||
'EndDevice',
|
||||
'0x0017880104e45518',
|
||||
6536,
|
||||
0,
|
||||
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])],
|
||||
true,
|
||||
'Battery',
|
||||
'notSupportedModelID',
|
||||
false,
|
||||
'notSupportedMfg',
|
||||
),
|
||||
unsupported2: new Device(
|
||||
'EndDevice',
|
||||
'0x0017880104e45529',
|
||||
6536,
|
||||
0,
|
||||
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])],
|
||||
true,
|
||||
'Battery',
|
||||
'notSupportedModelID',
|
||||
),
|
||||
interviewing: new Device(
|
||||
'EndDevice',
|
||||
'0x0017880104e45530',
|
||||
6536,
|
||||
0,
|
||||
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])],
|
||||
true,
|
||||
'Battery',
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
notInSettings: new Device(
|
||||
'EndDevice',
|
||||
'0x0017880104e45519',
|
||||
6537,
|
||||
0,
|
||||
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])],
|
||||
true,
|
||||
'Battery',
|
||||
'lumi.sensor_switch.aq2',
|
||||
),
|
||||
WXKG11LM: new Device(
|
||||
'EndDevice',
|
||||
'0x0017880104e45520',
|
||||
6537,
|
||||
4151,
|
||||
[
|
||||
new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45520', [], {}, [
|
||||
{
|
||||
cluster: {name: 'genOnOff'},
|
||||
attribute: {name: undefined, ID: 1337},
|
||||
minimumReportInterval: 1,
|
||||
maximumReportInterval: 10,
|
||||
reportableChange: 20,
|
||||
},
|
||||
]),
|
||||
],
|
||||
true,
|
||||
'Battery',
|
||||
'lumi.sensor_switch.aq2',
|
||||
),
|
||||
WXKG02LM_rev1: new Device(
|
||||
'EndDevice',
|
||||
'0x0017880104e45521',
|
||||
6538,
|
||||
4151,
|
||||
[new Endpoint(1, [0], []), new Endpoint(2, [0], [])],
|
||||
true,
|
||||
'Battery',
|
||||
'lumi.sensor_86sw2.es1',
|
||||
),
|
||||
WSDCGQ11LM: new Device('EndDevice', '0x0017880104e45522', 6539, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.weather'),
|
||||
RTCGQ11LM: new Device('EndDevice', '0x0017880104e45523', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.sensor_motion.aq2'),
|
||||
ZNCZ02LM: ZNCZ02LM,
|
||||
E1743: new Device('Router', '0x0017880104e45540', 6540, 4476, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'TRADFRI on/off switch'),
|
||||
QBKG04LM: new Device(
|
||||
'Router',
|
||||
'0x0017880104e45541',
|
||||
6549,
|
||||
4151,
|
||||
[new Endpoint(1, [0], [25]), new Endpoint(2, [0, 6], [])],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'lumi.ctrl_neutral1',
|
||||
),
|
||||
QBKG03LM: QBKG03LM,
|
||||
GLEDOPTO1112: new Device(
|
||||
'Router',
|
||||
'0x0017880104e45543',
|
||||
6540,
|
||||
4151,
|
||||
[new Endpoint(11, [0], [], '0x0017880104e45543'), new Endpoint(13, [0], [], '0x0017880104e45543')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'GL-C-008',
|
||||
),
|
||||
GLEDOPTO111213: new Device(
|
||||
'Router',
|
||||
'0x0017880104e45544',
|
||||
6540,
|
||||
4151,
|
||||
[new Endpoint(11, [0], []), new Endpoint(13, [0], []), new Endpoint(12, [0], [])],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'GL-C-008',
|
||||
),
|
||||
GLEDOPTO_2ID: GLEDOPTO_2ID,
|
||||
HGZB04D: new Device(
|
||||
'Router',
|
||||
'0x0017880104e45545',
|
||||
6540,
|
||||
4151,
|
||||
[new Endpoint(1, [0], [25], '0x0017880104e45545')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'FB56+ZSC05HG1.0',
|
||||
),
|
||||
ZNCLDJ11LM: new Device(
|
||||
'Router',
|
||||
'0x0017880104e45547',
|
||||
6540,
|
||||
4151,
|
||||
[new Endpoint(1, [0], []), new Endpoint(2, [0], [])],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'lumi.curtain',
|
||||
),
|
||||
HAMPTON99432: new Device(
|
||||
'Router',
|
||||
'0x0017880104e45548',
|
||||
6540,
|
||||
4151,
|
||||
[new Endpoint(1, [0], []), new Endpoint(2, [0], [])],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'HDC52EastwindFan',
|
||||
),
|
||||
HS2WD: new Device('Router', '0x0017880104e45549', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'WarningDevice'),
|
||||
'1TST_EU': new Device('Router', '0x0017880104e45550', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'Thermostat'),
|
||||
SV01: new Device('Router', '0x0017880104e45551', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'SV01-410-MP-1.0'),
|
||||
J1: new Device('Router', '0x0017880104e45552', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'J1 (5502)'),
|
||||
E11_G13: new Device('EndDevice', '0x0017880104e45553', 6540, 4151, [new Endpoint(1, [0, 6], [])], true, 'Mains (single phase)', 'E11-G13'),
|
||||
nomodel: new Device(
|
||||
'Router',
|
||||
'0x0017880104e45535',
|
||||
6536,
|
||||
0,
|
||||
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
unsupported_router: new Device(
|
||||
'Router',
|
||||
'0x0017880104e45525',
|
||||
6536,
|
||||
0,
|
||||
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'notSupportedModelID',
|
||||
false,
|
||||
'Boef',
|
||||
),
|
||||
CC2530_ROUTER: new Device('Router', '0x0017880104e45559', 6540, 4151, [new Endpoint(1, [0, 6], [])], true, 'Mains (single phase)', 'lumi.router'),
|
||||
LIVOLO: new Device('Router', '0x0017880104e45560', 6541, 4152, [new Endpoint(6, [0, 6], [])], true, 'Mains (single phase)', 'TI0001 '),
|
||||
tradfri_remote: new Device(
|
||||
'EndDevice',
|
||||
'0x90fd9ffffe4b64ae',
|
||||
33906,
|
||||
4476,
|
||||
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64ae')],
|
||||
true,
|
||||
'Battery',
|
||||
'TRADFRI remote control',
|
||||
),
|
||||
roller_shutter: new Device(
|
||||
'EndDevice',
|
||||
'0x90fd9ffffe4b64af',
|
||||
33906,
|
||||
4476,
|
||||
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64af')],
|
||||
true,
|
||||
'Battery',
|
||||
'SCM-R_00.00.03.15TC',
|
||||
),
|
||||
ZNLDP12LM: new Device(
|
||||
'Router',
|
||||
'0x90fd9ffffe4b64ax',
|
||||
33901,
|
||||
4476,
|
||||
[
|
||||
new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64ax', [], {
|
||||
lightingColorCtrl: {colorCapabilities: 254},
|
||||
}),
|
||||
],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'lumi.light.aqcn02',
|
||||
),
|
||||
SP600_OLD: new Device(
|
||||
'Router',
|
||||
'0x90fd9ffffe4b64aa',
|
||||
33901,
|
||||
4476,
|
||||
[
|
||||
new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64aa', [], {
|
||||
seMetering: {multiplier: 1, divisor: 10000},
|
||||
}),
|
||||
],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'SP600',
|
||||
false,
|
||||
'Salus',
|
||||
'20160120',
|
||||
),
|
||||
SP600_NEW: new Device(
|
||||
'Router',
|
||||
'0x90fd9ffffe4b64ab',
|
||||
33901,
|
||||
4476,
|
||||
[
|
||||
new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64aa', [], {
|
||||
seMetering: {multiplier: 1, divisor: 10000},
|
||||
}),
|
||||
],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'SP600',
|
||||
false,
|
||||
'Salus',
|
||||
'20170220',
|
||||
),
|
||||
'MKS-CM-W5': new Device(
|
||||
'Router',
|
||||
'0x90fd9ffffe4b64ac',
|
||||
33901,
|
||||
4476,
|
||||
[new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], '0x90fd9ffffe4b64aa', [], {})],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'qnazj70',
|
||||
false,
|
||||
),
|
||||
'GL-S-007ZS': new Device(
|
||||
'Router',
|
||||
'0x0017880104e45526',
|
||||
6540,
|
||||
4151,
|
||||
[new Endpoint(1, [0], [], '0x0017880104e45526')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'GL-S-007ZS',
|
||||
),
|
||||
U202DST600ZB: new Device(
|
||||
'Router',
|
||||
'0x0017880104e43559',
|
||||
6540,
|
||||
4151,
|
||||
[new Endpoint(10, [0, 6], [], '0x0017880104e43559'), new Endpoint(11, [0, 6], [], '0x0017880104e43559')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'U202DST600ZB',
|
||||
),
|
||||
zigfred_plus: zigfred_plus,
|
||||
3157100: new Device(
|
||||
'Router',
|
||||
'0x0017880104e44559',
|
||||
6542,
|
||||
4151,
|
||||
[new Endpoint(1, [], [], '0x0017880104e44559')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'3157100',
|
||||
false,
|
||||
'Centralite',
|
||||
),
|
||||
J1: new Device(
|
||||
'Router',
|
||||
'0x0017880104a44559',
|
||||
6543,
|
||||
4151,
|
||||
[new Endpoint(1, [], [], '0x0017880104a44559')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'J1 (5502)',
|
||||
),
|
||||
TS0601_thermostat: TS0601_thermostat,
|
||||
TS0601_switch: TS0601_switch,
|
||||
TS0601_cover_switch: TS0601_cover_switch,
|
||||
external_converter_device: new Device(
|
||||
'EndDevice',
|
||||
'0x0017880104e45511',
|
||||
1114,
|
||||
'external',
|
||||
[new Endpoint(1, [], [], '0x0017880104e45511')],
|
||||
false,
|
||||
null,
|
||||
'external_converter_device',
|
||||
),
|
||||
QS_Zigbee_D02_TRIAC_2C_LN: new Device(
|
||||
'Router',
|
||||
'0x0017882194e45543',
|
||||
6549,
|
||||
4151,
|
||||
[new Endpoint(1, [0], [], '0x0017882194e45543'), new Endpoint(2, [0, 6], [], '0x0017882194e45543')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'TS110F',
|
||||
false,
|
||||
'_TYZB01_v8gtiaed',
|
||||
),
|
||||
unknown: new Device('Router', '0x0017980134e45545', 6540, 4151, [], true, 'Mains (single phase)'),
|
||||
temperature_sensor: new Device(
|
||||
'EndDevice',
|
||||
'0x0017880104e45561',
|
||||
6544,
|
||||
4151,
|
||||
[new Endpoint(1, [0, 3, 4, 1026], [])],
|
||||
true,
|
||||
'Battery',
|
||||
'temperature.sensor',
|
||||
),
|
||||
heating_actuator: new Device(
|
||||
'Router',
|
||||
'0x0017880104e45562',
|
||||
6545,
|
||||
4151,
|
||||
[new Endpoint(1, [0, 3, 4, 513], [1026])],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'heating.actuator',
|
||||
),
|
||||
bj_scene_switch: new Device(
|
||||
'EndDevice',
|
||||
'0xd85def11a1002caa',
|
||||
50117,
|
||||
4398,
|
||||
[
|
||||
new Endpoint(10, [0, 4096], [3, 4, 5, 6, 8, 25, 768, 4096], '0xd85def11a1002caa', [
|
||||
{target: bulb_color_2.endpoints[0], cluster: {ID: 8, name: 'genLevelCtrl'}},
|
||||
{target: bulb_color_2.endpoints[0], cluster: {ID: 6, name: 'genOnOff'}},
|
||||
{target: bulb_color_2.endpoints[0], cluster: {ID: 768, name: 'lightingColorCtrl'}},
|
||||
]),
|
||||
new Endpoint(11, [0, 4096], [3, 4, 5, 6, 8, 25, 768, 4096], '0xd85def11a1002caa'),
|
||||
],
|
||||
true,
|
||||
'Battery',
|
||||
'RB01',
|
||||
false,
|
||||
'Busch-Jaeger',
|
||||
'20161222',
|
||||
'1.2.0',
|
||||
),
|
||||
'GW003-AS-IN-TE-FC': new Device(
|
||||
'Router',
|
||||
'0x0017548104a44669',
|
||||
6545,
|
||||
4699,
|
||||
[new Endpoint(1, [3], [0, 3, 513, 514], '0x0017548104a44669')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'Adapter Zigbee FUJITSU',
|
||||
),
|
||||
'BMCT-SLZ': new Device(
|
||||
'Router',
|
||||
'0x18fc26000000cafe',
|
||||
6546,
|
||||
4617,
|
||||
[new Endpoint(1, [0, 3, 4, 5, 258, 1794, 2820, 2821, 64672], [10, 25], '0x18fc26000000cafe')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'RBSH-MMS-ZB-EU',
|
||||
),
|
||||
BMCT_SLZ: new Device(
|
||||
'Router',
|
||||
'0x0026decafe000473',
|
||||
6546,
|
||||
4617,
|
||||
[new Endpoint(1, [0, 3, 4, 5, 258, 1794, 2820, 2821, 64672], [10, 25], '0x0026decafe000473')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'RBSH-MMS-ZB-EU',
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
custom_clusters,
|
||||
),
|
||||
bulb_custom_cluster: new Device(
|
||||
'Router',
|
||||
'0x000b57fffec6a5c2',
|
||||
40369,
|
||||
4476,
|
||||
[new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], '0x000b57fffec6a5c2')],
|
||||
true,
|
||||
'Mains (single phase)',
|
||||
'TRADFRI bulb E27 WS opal 980lm',
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
custom_clusters,
|
||||
),
|
||||
};
|
||||
|
||||
const mock = {
|
||||
setTransmitPower: jest.fn(),
|
||||
@ -252,13 +819,19 @@ const mock = {
|
||||
return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr));
|
||||
}),
|
||||
getDevicesByType: jest.fn().mockImplementation((type) => {
|
||||
return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)).filter((d) => d.type === type);
|
||||
return Object.values(devices)
|
||||
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
|
||||
.filter((d) => d.type === type);
|
||||
}),
|
||||
getDeviceByIeeeAddr: jest.fn().mockImplementation((ieeeAddr) => {
|
||||
return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)).find((d) => d.ieeeAddr === ieeeAddr);
|
||||
return Object.values(devices)
|
||||
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
|
||||
.find((d) => d.ieeeAddr === ieeeAddr);
|
||||
}),
|
||||
getDeviceByNetworkAddress: jest.fn().mockImplementation((networkAddress) => {
|
||||
return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)).find((d) => d.networkAddress === networkAddress);
|
||||
return Object.values(devices)
|
||||
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
|
||||
.find((d) => d.networkAddress === networkAddress);
|
||||
}),
|
||||
getGroups: jest.fn().mockImplementation((query) => {
|
||||
return Object.values(groups);
|
||||
@ -271,9 +844,9 @@ const mock = {
|
||||
reset: jest.fn(),
|
||||
createGroup: jest.fn().mockImplementation((groupID) => {
|
||||
const group = new Group(groupID, []);
|
||||
groups[`group_${groupID}`] = group
|
||||
groups[`group_${groupID}`] = group;
|
||||
return group;
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
const mockConstructor = jest.fn().mockImplementation(() => mock);
|
||||
@ -284,5 +857,11 @@ jest.mock('zigbee-herdsman', () => ({
|
||||
}));
|
||||
|
||||
module.exports = {
|
||||
events, ...mock, constructor: mockConstructor, devices, groups, returnDevices, custom_clusters
|
||||
events,
|
||||
...mock,
|
||||
constructor: mockConstructor,
|
||||
devices,
|
||||
groups,
|
||||
returnDevices,
|
||||
custom_clusters,
|
||||
};
|
||||
|
@ -7,25 +7,25 @@ describe('Utils', () => {
|
||||
it('Object has properties', () => {
|
||||
expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b'])).toBeTruthy();
|
||||
expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b', 'd'])).toBeFalsy();
|
||||
})
|
||||
});
|
||||
|
||||
it('git last commit', async () => {
|
||||
let mockReturnValue = [];
|
||||
jest.mock('git-last-commit', () => ({
|
||||
getLastCommit: (cb) => cb(mockReturnValue[0], mockReturnValue[1])
|
||||
getLastCommit: (cb) => cb(mockReturnValue[0], mockReturnValue[1]),
|
||||
}));
|
||||
|
||||
mockReturnValue = [false, {shortHash: '123'}]
|
||||
expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({"commitHash": "123", "version": version});
|
||||
mockReturnValue = [false, {shortHash: '123'}];
|
||||
expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({commitHash: '123', version: version});
|
||||
|
||||
mockReturnValue = [true, null]
|
||||
expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({"commitHash": expect.any(String), "version": version});
|
||||
})
|
||||
mockReturnValue = [true, null];
|
||||
expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({commitHash: expect.any(String), version: version});
|
||||
});
|
||||
|
||||
it('Check dependency version', async () => {
|
||||
expect(await utils.getDependencyVersion('zigbee-herdsman')).toStrictEqual({"version": versionHerdsman});
|
||||
expect(await utils.getDependencyVersion('zigbee-herdsman-converters')).toStrictEqual({"version": versionHerdsmanConverters});
|
||||
})
|
||||
expect(await utils.getDependencyVersion('zigbee-herdsman')).toStrictEqual({version: versionHerdsman});
|
||||
expect(await utils.getDependencyVersion('zigbee-herdsman-converters')).toStrictEqual({version: versionHerdsmanConverters});
|
||||
});
|
||||
|
||||
it('To local iso string', async () => {
|
||||
var date = new Date('August 19, 1975 23:15:30 UTC+00:00');
|
||||
@ -35,7 +35,7 @@ describe('Utils', () => {
|
||||
Date.prototype.getTimezoneOffset = () => -60;
|
||||
expect(utils.formatDate(date, 'ISO_8601_local').endsWith('+01:00')).toBeTruthy();
|
||||
Date.prototype.getTimezoneOffset = getTimezoneOffset;
|
||||
})
|
||||
});
|
||||
it('Removes null properties from object', () => {
|
||||
const obj1 = {
|
||||
ab: 0,
|
||||
|
Loading…
Reference in New Issue
Block a user