Merge branch 'dev'

This commit is contained in:
Koen Kanters
2021-04-01 09:06:39 +02:00
35 changed files with 1637 additions and 1362 deletions
+1
View File
@@ -24,6 +24,7 @@ async function start() {
// Validate settings
const settings = require('./lib/util/settings');
settings.reRead();
const errors = settings.validate();
if (errors.length > 0) {
console.log(`\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`);
+2
View File
@@ -108,6 +108,8 @@ class Controller {
this.zigbee.on('adapterDisconnected', this.onZigbeeAdapterDisconnected);
} catch (error) {
logger.error('Failed to start zigbee');
// eslint-disable-next-line
logger.error('Check https://www.zigbee2mqtt.io/information/FAQ.html#help-zigbee2mqtt-fails-to-start for possible solutions');
logger.error('Exiting...');
logger.error(error.stack);
this.exitCallback(1);
+1
View File
@@ -146,6 +146,7 @@ class Bind extends Extension {
async onMQTTMessage(topic, message) {
const {type, sourceKey, targetKey, clusters} = this.parseMQTTMessage(topic, message);
if (!type) return null;
message = utils.parseJSON(message, message);
let error = null;
const source = this.zigbee.resolveEntity(sourceKey);
+3 -2
View File
@@ -559,7 +559,8 @@ class Bridge extends Extension {
coordinator: this.coordinatorVersion,
network: utils.toSnakeCase(await this.zigbee.getNetworkParameters()),
log_level: logger.getLevel(),
permit_join: await this.zigbee.getPermitJoin(),
permit_join: this.zigbee.getPermitJoin(),
permit_join_timeout: this.zigbee.getPermitJoinTimeout(),
restart_required: this.restartRequired,
config,
config_schema: settings.schema,
@@ -601,7 +602,7 @@ class Bridge extends Extension {
for (const configuredReporting of endpoint.configuredReportings) {
data.configured_reportings.push({
cluster: configuredReporting.cluster.name,
attribute: configuredReporting.attribute.name,
attribute: configuredReporting.attribute.name || configuredReporting.attribute.ID,
minimum_report_interval: configuredReporting.minimumReportInterval,
maximum_report_interval: configuredReporting.maximumReportInterval,
reportable_change: configuredReporting.reportableChange,
+4 -3
View File
@@ -82,12 +82,12 @@ class Groups extends Extension {
return;
}
const properties = ['state', 'brightness', 'color_temp', 'color'];
const properties = ['state', 'brightness', 'color_temp', 'color', 'color_mode'];
const payload = {};
properties.forEach((prop) => {
if (data.to.hasOwnProperty(prop)) {
payload[prop] = data.to[prop];
if (data.changed.hasOwnProperty(prop)) {
payload[prop] = data.changed[prop];
}
});
@@ -237,6 +237,7 @@ class Groups extends Extension {
groupKey, deviceKey,
} = this.parseMQTTMessage(topic, message);
if (!type) return;
message = utils.parseJSON(message, message);
const responseData = {device: deviceKey};
if (groupKey) {
+9 -3
View File
@@ -162,6 +162,12 @@ class HomeAssistant extends Extension {
const mode = expose.features.find((f) => f.name === 'system_mode');
if (mode) {
if (mode.values.includes('sleep')) {
// 'sleep' is not supported by homeassistent, but is valid according to ZCL
// TRV that support sleep (e.g. Viessmann) will have it removed from here,
// this allows other expose consumers to still use it, e.g. the frontend.
mode.values.splice(mode.values.indexOf('sleep'), 1);
}
discoveryEntry.discovery_payload.mode_state_topic = true;
discoveryEntry.discovery_payload.mode_state_template = `{{ value_json.${mode.property} }}`;
discoveryEntry.discovery_payload.modes = mode.values;
@@ -245,8 +251,8 @@ class HomeAssistant extends Extension {
// deprecated: child_lock is messy, but changing is breaking
discoveryEntry.discovery_payload.payload_lock = state.value_on;
discoveryEntry.discovery_payload.payload_unlock = state.value_off;
discoveryEntry.discovery_payload.state_locked = 'LOCKED';
discoveryEntry.discovery_payload.state_unlocked = 'UNLOCKED';
discoveryEntry.discovery_payload.state_locked = 'LOCK';
discoveryEntry.discovery_payload.state_unlocked = 'UNLOCK';
discoveryEntry.discovery_payload.state_topic = true;
discoveryEntry.object_id = 'child_lock';
} else {
@@ -277,7 +283,7 @@ class HomeAssistant extends Extension {
if (hasPosition) {
discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload,
value_template: '{{ value_json.position }}',
position_template: '{{ value_json.position }}',
set_position_template: '{ "position": {{ position }} }',
set_position_topic: true,
position_topic: true,
+1 -1
View File
@@ -382,7 +382,7 @@ class BridgeLegacy extends Extension {
coordinator,
network: await this.zigbee.getNetworkParameters(),
log_level: logger.getLevel(),
permit_join: await this.zigbee.getPermitJoin(),
permit_join: this.zigbee.getPermitJoin(),
};
await this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0});
+1 -1
View File
@@ -42,7 +42,7 @@ class OTAUpdate extends Extension {
if (supportsOTA) {
// When a device does a next image request, it will usually do it a few times after each other
// 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 10 minutes.
// 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;
+7 -4
View File
@@ -77,18 +77,21 @@ class State {
set(ID, state, reason=null) {
const toState = objectAssignDeep.noMutate(state);
const fromState = this.state[ID];
const changed = {};
for (const property of Object.keys(toState)) {
if (dontCacheProperties.find((p) => property.match(p))) {
delete toState[property];
}
if (!fromState || toState[property] !== fromState[property]) {
changed[property] = toState[property];
}
}
const fromState = this.state[ID];
this.state[ID] = toState;
this.eventBus.emit('stateChange', {ID, from: fromState, to: state, reason});
this.eventBus.emit('stateChange', {ID, from: fromState, to: state, reason, changed});
}
removeKey(ID, path) {
+5 -5
View File
@@ -141,7 +141,7 @@ const defaults = {
/**
* Minimal time delta in minutes between polling third party server for potential firmware updates
*/
update_check_interval: 10,
update_check_interval: 24 * 60,
/**
* Completely disallow Zigbee devices to initiate a search for a potential firmware update.
* If set to true, only a user-initiated update search will be possible.
@@ -683,15 +683,15 @@ module.exports = {
changeEntityOptions,
changeFriendlyName,
schema,
// For tests only
_write: write,
_reRead: () => {
reRead: () => {
_settings = null;
get();
_settingsWithDefaults = null;
getWithDefaults();
},
// For tests only
_write: write,
_clear: () => {
_settings = null;
_settingsWithDefaults = null;
+3 -3
View File
@@ -144,7 +144,7 @@
},
"adapter": {
"type": ["string", "null"],
"enum": ["deconz", "zstack", "zigate"],
"enum": ["deconz", "zstack", "zigate", "ezsp"],
"title": "Adapter",
"requiresRestart": true,
"description": "Adapter type, not needed unless you are experiencing problems"
@@ -589,8 +589,8 @@
"update_check_interval": {
"type": "number",
"title": "Update check interval",
"description": "Your device may request a check for a new firmware update. This value determines how frequently third party servers may actually be contacted to look for firmware updates. The value is set in minutes, and the default is 10.",
"default": 10
"description": "Your device may request a check for a new firmware update. This value determines how frequently third party servers may actually be contacted to look for firmware updates. The value is set in minutes, and the default is 1 day.",
"default": 1440
},
"disable_automatic_update_check": {
"type": "boolean",
+6
View File
@@ -152,6 +152,12 @@ function loadModuleFromText(moduleCode) {
require: require,
module: {},
console,
setTimeout,
clearTimeout,
setInterval,
clearInterval,
setImmediate,
clearImmediate,
};
vm.runInNewContext(moduleCode, sandbox, moduleFakePath);
return sandbox.module.exports;
+5 -1
View File
@@ -141,10 +141,14 @@ class Zigbee extends events.EventEmitter {
}
}
async getPermitJoin() {
getPermitJoin() {
return this.herdsman.getPermitJoin();
}
getPermitJoinTimeout() {
return this.herdsman.getPermitJoinTimeout();
}
getClients() {
return this.herdsman.getDevices().filter((device) => device.type !== 'Coordinator');
}
+1489 -1268
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "1.18.1",
"version": "1.18.1-dev",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"repository": {
@@ -52,9 +52,9 @@
"winston": "^3.3.3",
"winston-syslog": "^2.4.4",
"ws": "^7.3.1",
"zigbee-herdsman": "0.13.71",
"zigbee-herdsman-converters": "14.0.74-0",
"zigbee2mqtt-frontend": "0.3.76"
"zigbee-herdsman": "0.13.88",
"zigbee-herdsman-converters": "14.0.102",
"zigbee2mqtt-frontend": "0.3.114"
},
"devDependencies": {
"eslint": "*",
+1 -1
View File
@@ -25,7 +25,7 @@ describe('Availability', () => {
beforeEach(async () => {
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
data.writeEmptyState();
jest.useFakeTimers();
settings.set(['advanced', 'availability_timeout'], 10);
+3 -3
View File
@@ -25,7 +25,7 @@ describe('Bind', () => {
beforeEach(async () => {
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
data.writeEmptyState();
zigbeeHerdsman.groups.group_1.members = [];
zigbeeHerdsman.devices.bulb_color.getEndpoint(1).configureReporting.mockClear();
@@ -53,7 +53,7 @@ describe('Bind', () => {
mockClear(device);
target.configureReporting.mockImplementationOnce(() => {throw new Error("timeout")});
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({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(endpoint.bind).toHaveBeenCalledTimes(4);
@@ -67,7 +67,7 @@ describe('Bind', () => {
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({"data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl", "lightingColorCtrl"],"failed":[]},"status":"ok"}),
stringify({"transaction": "1234","data":{"from":"remote","to":"bulb_color","clusters":["genScenes","genOnOff","genLevelCtrl", "lightingColorCtrl"],"failed":[]},"status":"ok"}),
{retain: false, qos: 0}, expect.any(Function)
);
+3 -3
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -44,7 +44,7 @@ describe('Configure', () => {
beforeEach(async () => {
jest.useRealTimers();
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
data.writeEmptyState();
controller = new Controller(jest.fn(), jest.fn());
await controller.start();
+1 -1
View File
@@ -26,7 +26,7 @@ describe('Controller', () => {
controller = new Controller(jest.fn(), mockExit);
mocksClear.forEach((m) => m.mockClear());
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
data.writeDefaultState();
});
+1 -1
View File
@@ -47,7 +47,7 @@ describe('Loads external converters', () => {
mocksClear.forEach((m) => m.mockClear());
data.writeDefaultConfiguration();
data.writeEmptyState();
settings._reRead();
settings.reRead();
});
it('Does not load external converters', async () => {
+1 -1
View File
@@ -28,7 +28,7 @@ describe('User extensions', () => {
controller = new Controller(jest.fn(), mockExit);
mocksClear.forEach((m) => m.mockClear());
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
data.writeDefaultState();
});
afterEach(() => {
+1 -1
View File
@@ -74,7 +74,7 @@ describe('Frontend', () => {
mockWS.implementation.clients = [];
data.writeDefaultConfiguration();
data.writeDefaultState();
settings._reRead();
settings.reRead();
settings.set(['frontend'], {port: 8081, host: "127.0.0.1"});
settings.set(['homeassistant'], true);
zigbeeHerdsman.devices.bulb.linkquality = 10;
+31 -3
View File
@@ -23,7 +23,7 @@ describe('Groups', () => {
controller = new Controller(jest.fn(), jest.fn());
Object.values(zigbeeHerdsman.groups).forEach((g) => g.members = []);
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
MQTT.publish.mockClear();
zigbeeHerdsmanConverters.toZigbeeConverters.__clearStore__();
})
@@ -505,6 +505,34 @@ describe('Groups', () => {
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 () => {
const device_1 = zigbeeHerdsman.devices.bulb_color;
const device_2 = zigbeeHerdsman.devices.bulb;
const endpoint_1 = device_1.getEndpoint(1);
const endpoint_2 = device_2.getEndpoint(1);
const group = zigbeeHerdsman.groups.group_1;
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}
});
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', color_temp: 200}));
await MQTT.events.message('zigbee2mqtt/bulb/set', stringify({state: 'ON', color_temp: 250}));
await flushPromises();
MQTT.publish.mockClear();
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":{"x":0.415211980162654,"y":0.395434886759171},"color_temp":300,"state":"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"color_mode": "color_temp", "color":{"x":0.415211980162654,"y":0.395434886759171},"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":{"x":0.415211980162654,"y":0.395434886759171},"color_temp":300,"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
});
it('Should publish state change off even when missing current state', async () => {
const device_1 = zigbeeHerdsman.devices.bulb_color;
const device_2 = zigbeeHerdsman.devices.bulb;
@@ -541,14 +569,14 @@ describe('Groups', () => {
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({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"},"status":"ok"}),
stringify({"data":{"device":"bulb_color","group":"group_1"},"transaction": "123", "status":"ok"}),
{retain: false, qos: 0}, expect.any(Function)
);
});
+2 -2
View File
@@ -18,7 +18,7 @@ describe('HomeAssistant extension', () => {
this.version = `Zigbee2MQTT ${this.version.version}`;
jest.useRealTimers();
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
data.writeEmptyState();
MQTT.publish.mockClear();
settings.set(['homeassistant'], true);
@@ -669,7 +669,7 @@ describe('HomeAssistant extension', () => {
position_topic: 'zigbee2mqtt/smart vent',
set_position_topic: 'zigbee2mqtt/smart vent/set',
set_position_template: '{ "position": {{ position }} }',
value_template: '{{ value_json.position }}',
position_template: '{{ value_json.position }}',
json_attributes_topic: 'zigbee2mqtt/smart vent',
name: 'smart vent',
unique_id: '0x0017880104e45551_cover_zigbee2mqtt',
+1 -1
View File
@@ -21,7 +21,7 @@ describe('Bridge legacy', () => {
beforeEach(() => {
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
data.writeDefaultState();
logger.info.mockClear();
logger.warn.mockClear();
+1 -1
View File
@@ -71,7 +71,7 @@ describe('Report', () => {
beforeEach(async () => {
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
data.writeEmptyState();
settings.set(['advanced', 'report'], true);
for (const device of Object.values(zigbeeHerdsman.devices)) {
+1 -1
View File
@@ -14,7 +14,7 @@ describe('Logger', () => {
jest.resetModules();
settings = require('../lib/util/settings');
settings.set(['advanced', 'log_directory'], dir.name + '/%TIMESTAMP%');
settings._reRead();
settings.reRead();
stdOutWriteOriginal = console._stdout.write;
console._stdout.write = () => {};
});
+1 -1
View File
@@ -27,7 +27,7 @@ describe('Networkmap', () => {
beforeAll(async () => {
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
data.writeEmptyState();
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']);
+1 -1
View File
@@ -23,7 +23,7 @@ describe('On event', () => {
beforeEach(async () => {
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
data.writeEmptyState();
controller = new Controller(jest.fn(), jest.fn());
await controller.start();
+1 -1
View File
@@ -18,7 +18,7 @@ describe('OTA update', () => {
beforeEach(async () => {
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
settings.set(['advanced', 'ikea_ota_use_test_url'], true);
data.writeEmptyState();
controller = new Controller(jest.fn(), jest.fn());
+26 -26
View File
@@ -38,7 +38,7 @@ describe('Publish', () => {
await flushPromises();
data.writeDefaultConfiguration();
controller.state.state = {};
settings._reRead();
settings.reRead();
mocksClear.forEach((m) => m.mockClear());
Object.values(zigbeeHerdsman.devices).forEach((d) => {
d.endpoints.forEach((e) => {
@@ -139,7 +139,7 @@ describe('Publish', () => {
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 222, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 222, color: {x: 0.360786471097048, y: 0.363543544699483}});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 222, color: {x: 0.360786471097048, y: 0.363543544699483}});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -152,7 +152,7 @@ describe('Publish', () => {
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 500, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 500, color: {x: 0.526676280311873, y: 0.41329727450763}});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 500, color: {x: 0.526676280311873, y: 0.41329727450763}});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -219,7 +219,7 @@ describe('Publish', () => {
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 62, color: {x: 100, y: 50}});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'xy', color_temp: 62, color: {x: 100, y: 50}});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -236,7 +236,7 @@ describe('Publish', () => {
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 100, y: 50}, color_temp: 62, state: 'ON'});
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color_mode: 'xy', color: {x: 100, y: 50}, color_temp: 62, state: 'ON'});
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -253,7 +253,7 @@ describe('Publish', () => {
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 20});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 100, y: 50}, state: 'ON', color_temp: 62, brightness: 20});
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color_mode: 'xy', color: {x: 100, y: 50}, state: 'ON', color_temp: 62, brightness: 20});
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -270,7 +270,7 @@ describe('Publish', () => {
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 20});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 100, y: 50}, state: 'ON', color_temp: 62, brightness: 20});
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 100, y: 50}, state: 'ON', color_temp: 62, brightness: 20, color_mode: 'xy'});
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -284,10 +284,10 @@ describe('Publish', () => {
expect(endpoint.command).toHaveBeenNthCalledWith(2, "genOnOff", "off", {}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 100, y: 50}, color_temp: 62});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'xy', color: {x: 100, y: 50}, color_temp: 62});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 100, y: 50}, color_temp: 62, state: 'OFF'});
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color_mode: 'xy', color: {x: 100, y: 50}, color_temp: 62, state: 'OFF'});
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -300,7 +300,7 @@ describe('Publish', () => {
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17721, colory: 43148, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 157, color: {x: 0.2704, y: 0.6584}});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 157, color: {x: 0.2704, y: 0.6584}, color_mode: 'xy'});
});
it('Should publish messages to zigbee devices with color rgb', async () => {
@@ -312,7 +312,7 @@ describe('Publish', () => {
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17721, colory: 43148, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 157, color: {x: 0.2704, y: 0.6584}});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 157, color: {x: 0.2704, y: 0.6584}, color_mode: 'xy'});
});
it('Should publish messages to zigbee devices with color rgb', async () => {
@@ -343,10 +343,10 @@ describe('Publish', () => {
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({brightness_percent: 50}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 127, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 128, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 127});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 128});
});
it('Should publish messages to groups when converter is not in the default list but device in it supports it', async () => {
@@ -387,7 +387,7 @@ describe('Publish', () => {
expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 24248, colory: 18350, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_temp: 249});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_temp: 249, color_mode: 'xy'});
});
it('Should publish messages to groups color temperature', async () => {
@@ -398,7 +398,7 @@ describe('Publish', () => {
expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.280632719756407, y: 0.288286029784579}, color_temp: 100});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.280632719756407, y: 0.288286029784579}, color_temp: 100, color_mode: 'color_temp'});
});
it('Should create and publish to group which is in configuration.yaml but not in zigbee-herdsman', async () => {
@@ -554,7 +554,7 @@ describe('Publish', () => {
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17721, colory: 43148, transtime: 0}, {});
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 157, color: {x: 0.2704, y: 0.6584}});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 157, color: {x: 0.2704, y: 0.6584}, color_mode: "xy"});
});
it('Should parse set with ieeeAddr topic', async () => {
@@ -910,10 +910,10 @@ describe('Publish', () => {
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "enhancedMoveToHueAndSaturation", {"direction": 0, "enhancehue": 44891.475, "saturation": 199.21474, "transtime": 0,}, {}]);
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "enhancedMoveToHueAndSaturation", {"direction": 0, "enhancehue": 44891, "saturation": 199, "transtime": 0,}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"color":{"hue":250,"saturation":50}});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"color":{"hue":250,"saturation":50}, "color_mode": "hs"});
});
it('ZNCLDJ11LM open', async () => {
@@ -1073,7 +1073,7 @@ describe('Publish', () => {
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', color: {x: 0.280632719756407, y: 0.288286029784579}, color_temp: 100});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', color: {x: 0.280632719756407, y: 0.288286029784579}, color_temp: 100, color_mode: 'color_temp'});
});
it('Home Assistant: should set state when color temperature is also set and device is off', async () => {
@@ -1091,7 +1091,7 @@ describe('Publish', () => {
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', color: {x: 0.280632719756407, y: 0.288286029784579}, color_temp: 100});
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', color: {x: 0.280632719756407, y: 0.288286029784579}, color_temp: 100, color_mode: 'color_temp'});
});
it('Home Assistant: should not set state when color is also set', async () => {
@@ -1106,7 +1106,7 @@ describe('Publish', () => {
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColor", {colorx: 26869, colory: 16384, transtime: 0}, {}]);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, color_temp: 150, state: 'ON'});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, color_temp: 150, state: 'ON', color_mode: 'xy'});
});
it('Should publish correct state on toggle command to zigbee bulb', async () => {
@@ -1362,27 +1362,27 @@ describe('Publish', () => {
expect(MQTT.publish).toHaveBeenCalledTimes(5);
expect(MQTT.publish).toHaveBeenNthCalledWith(1,
'zigbee2mqtt/group_tradfri_remote',
stringify({"brightness":50,"color":{"x":0.408707336668894,"y":0.39239142575868},"color_temp":290,"state":"ON"}),
stringify({"brightness":50,"color":{"x":0.408707336668894,"y":0.39239142575868},"color_temp":290,"state":"ON","color_mode": "color_temp"}),
{retain: false, qos: 0}, expect.any(Function)
);
expect(MQTT.publish).toHaveBeenNthCalledWith(2,
'zigbee2mqtt/bulb_color_2',
stringify({"brightness":50,"color":{"x":0.408707336668894,"y":0.39239142575868},"color_temp":290,"state":"ON"}),
stringify({"color_mode": "color_temp", "brightness":50,"color":{"x":0.408707336668894,"y":0.39239142575868},"color_temp":290,"state":"ON"}),
{retain: false, qos: 0}, expect.any(Function)
);
expect(MQTT.publish).toHaveBeenNthCalledWith(3,
'zigbee2mqtt/group_tradfri_remote',
stringify({"brightness":100,"color":{"x":0.408707336668894,"y":0.39239142575868},"color_temp":290,"state":"ON"}),
stringify({"brightness":100,"color":{"x":0.408707336668894,"y":0.39239142575868},"color_temp":290,"state":"ON","color_mode": "color_temp"}),
{retain: false, qos: 0}, expect.any(Function)
);
expect(MQTT.publish).toHaveBeenNthCalledWith(4,
'zigbee2mqtt/bulb_2',
stringify({"brightness":100,"color":{"x":0.408707336668894,"y":0.39239142575868},"color_temp":290,"state":"ON"}),
stringify({"brightness":100,"color":{"x":0.408707336668894,"y":0.39239142575868},"state":"ON"}),
{retain: false, qos: 0}, expect.any(Function)
);
expect(MQTT.publish).toHaveBeenNthCalledWith(5,
'zigbee2mqtt/group_with_tradfri',
stringify({"brightness":100,"color":{"x":0.408707336668894,"y":0.39239142575868},"color_temp":290,"state":"ON"}),
stringify({"brightness":100,"color":{"x":0.408707336668894,"y":0.39239142575868},"state":"ON"}),
{retain: false, qos: 0}, expect.any(Function)
);
});
+1 -1
View File
@@ -15,7 +15,7 @@ describe('Receive', () => {
beforeEach(async () => {
jest.useRealTimers();
data.writeDefaultConfiguration();
settings._reRead();
settings.reRead();
data.writeEmptyState();
controller = new Controller(jest.fn(), jest.fn());
await controller.start();
+16 -16
View File
@@ -22,7 +22,7 @@ describe('Settings', () => {
const write = (file, json, reread=true) => {
fs.writeFileSync(file, yaml.safeDump(json))
if (reread) {
settings._reRead();
settings.reRead();
}
};
const read = (file) => yaml.safeLoad(fs.readFileSync(file, 'utf8'));
@@ -616,7 +616,7 @@ describe('Settings', () => {
advanced: {network_key: 'NOT_GENERATE'},
});
settings._reRead();
settings.reRead();
const error = `advanced.network_key: should be array or 'GENERATE' (is 'NOT_GENERATE')`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -628,7 +628,7 @@ describe('Settings', () => {
advanced: {pan_id: 'NOT_GENERATE'},
});
settings._reRead();
settings.reRead();
const error = `advanced.pan_id: should be number or 'GENERATE' (is 'NOT_GENERATE')`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -641,7 +641,7 @@ describe('Settings', () => {
devices: {'0x0017880104e45519': {friendly_name: 'tain', retention: 900}},
});
settings._reRead();
settings.reRead();
expect(settings.validate()).toEqual([]);
});
@@ -652,7 +652,7 @@ describe('Settings', () => {
devices: {'0x0017880104e45519': {friendly_name: 'tain', retention: 900}},
});
settings._reRead();
settings.reRead();
const error = 'MQTT retention requires protocol version 5';
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -665,7 +665,7 @@ describe('Settings', () => {
advanced: {availability_blocklist: ['0x0017880104e45519', 'non_existing']},
});
settings._reRead();
settings.reRead();
const error = `Non-existing entity 'non_existing' specified in 'availability_blocklist'`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -677,7 +677,7 @@ describe('Settings', () => {
advanced: null,
});
settings._reRead();
settings.reRead();
const error = `advanced should be object`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -723,7 +723,7 @@ describe('Settings', () => {
groups: {'1': {friendly_name: 'myname', retain: false}},
});
settings._reRead();
settings.reRead();
const error = `Duplicate friendly_name 'myname' found`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -735,7 +735,7 @@ describe('Settings', () => {
devices: {'0x0017880104e45519': {friendly_name: '', retain: false}},
});
settings._reRead();
settings.reRead();
const error = `friendly_name must be at least 1 char long`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -747,7 +747,7 @@ describe('Settings', () => {
devices: {'0x0017880104e45519': {friendly_name: 'blaa/', retain: false}},
});
settings._reRead();
settings.reRead();
const error = `friendly_name is not allowed to end or start with /`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -759,7 +759,7 @@ describe('Settings', () => {
devices: {'0x0017880104e45519': {friendly_name: 'blaa/blaa' + String.fromCharCode(0), retain: false}},
});
settings._reRead();
settings.reRead();
const error = `friendly_name is not allowed to contain null char`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -771,7 +771,7 @@ describe('Settings', () => {
devices: {'0x0017880104e45519': {friendly_name: 'myname/123', retain: false}},
});
settings._reRead();
settings.reRead();
const error = `Friendly name cannot end with a "/DIGIT" ('myname/123')`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -783,7 +783,7 @@ describe('Settings', () => {
devices: {'0x0017880104e45519': {friendly_name: 'myname#', retain: false}},
});
settings._reRead();
settings.reRead();
const error = `MQTT wildcard (+ and #) not allowed in friendly_name ('myname#')`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -795,7 +795,7 @@ describe('Settings', () => {
devices: {'0x0017880104e45519': {friendly_name: 'left', retain: false}},
});
settings._reRead();
settings.reRead();
const error = `Following friendly_name are not allowed: '${utils.getEndpointNames()}'`;
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
@@ -809,7 +809,7 @@ describe('Settings', () => {
},
});
settings._reRead();
settings.reRead();
expect(() => {
settings.changeFriendlyName('myname1', 'myname');
@@ -824,7 +824,7 @@ describe('Settings', () => {
},
});
settings._reRead();
settings.reRead();
expect(() => {
settings.removeDevice('myname33');
+2 -1
View File
@@ -149,7 +149,7 @@ const devices = {
'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])], 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"),
@@ -221,6 +221,7 @@ const mock = {
return Object.values(groups).find((d) => d.groupID === groupID);
}),
getPermitJoin: jest.fn().mockReturnValue(false),
getPermitJoinTimeout: jest.fn().mockReturnValue(undefined),
reset: jest.fn(),
createGroup: jest.fn().mockImplementation((groupID) => {
const group = new Group(groupID, []);