Merge branch 'dev'

This commit is contained in:
Koen Kanters
2021-12-01 17:19:37 +01:00
32 changed files with 4496 additions and 4424 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ async function start() {
for (const error of errors) {
console.log(`- ${error}`);
}
console.log(`\nIf you don't know how to solve this, read https://www.zigbee2mqtt.io/information/configuration.html`); // eslint-disable-line
console.log(`\nIf you don't know how to solve this, read https://www.zigbee2mqtt.io/guide/configuration`); // eslint-disable-line
console.log(`\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n`);
exit(1);
}
+5 -7
View File
@@ -37,7 +37,7 @@ const AllExtensions = [
];
type ExtensionArgs = [Zigbee, MQTT, State, PublishEntityState, EventBus,
(enable: boolean, name: string) => Promise<void>, () => void, (extension: Extension) => void];
(enable: boolean, name: string) => Promise<void>, () => void, (extension: Extension) => Promise<void>];
class Controller {
private eventBus: EventBus;
@@ -101,7 +101,7 @@ class Controller {
this.eventBus.onAdapterDisconnected(this, this.onZigbeeAdapterDisconnected);
} catch (error) {
logger.error('Failed to start zigbee');
logger.error('Check https://www.zigbee2mqtt.io/information/FAQ.html#help-zigbee2mqtt-fails-to-start 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'); /* eslint-disable-line max-len */
logger.error('Exiting...');
logger.error(error.stack);
this.exitCallback(1);
@@ -160,10 +160,8 @@ class Controller {
}
}
if (settings.get().advanced.last_seen && settings.get().advanced.last_seen !== 'disable') {
this.eventBus.onLastSeenChanged(this, (data) =>
this.publishEntityState(data.device, {}, 'lastSeenChanged'));
}
this.eventBus.onLastSeenChanged(this,
(data) => utils.publishLastSeen(data, settings.get(), false, this.publishEntityState));
}
@bind async enableDisableExtension(enable: boolean, name: string): Promise<void> {
@@ -276,7 +274,7 @@ class Controller {
}
}
this.eventBus.emitPublishEntityState({entity, message, stateChangeReason});
this.eventBus.emitPublishEntityState({entity, message, stateChangeReason, payload});
}
async iteratePayloadAttributeOutput(topicRoot: string, payload: KeyValue, options: MQTTOptions): Promise<void> {
+6 -6
View File
@@ -77,7 +77,7 @@ export default class Availability extends Extension {
logger.debug(`Succesfully pinged '${device.name}' (attempt ${i + 1}/${attempts})`);
break;
} catch (error) {
logger.error(`Failed to ping '${device.name}' (attempt ${i + 1}/${attempts}, ${error.message})`);
logger.warn(`Failed to ping '${device.name}' (attempt ${i + 1}/${attempts}, ${error.message})`);
// Try again in 3 seconds.
const lastAttempt = i - 1 === attempts;
!lastAttempt && await utils.sleep(3);
@@ -97,10 +97,10 @@ export default class Availability extends Extension {
override async start(): Promise<void> {
logger.warn('Using experimental new availability feature');
this.eventBus.onDeviceRenamed(this, (data: eventdata.DeviceRenamed) =>
this.publishAvailability(data.device, false, true));
this.eventBus.onDeviceLeave(this, (data: eventdata.DeviceLeave) => clearTimeout(this.timers[data.ieeeAddr]));
this.eventBus.onDeviceAnnounce(this, (data: eventdata.DeviceAnnounce) => this.retrieveState(data.device));
this.eventBus.onDeviceRenamed(this, (data) => this.publishAvailability(data.device, false, true));
this.eventBus.onDeviceRemoved(this, (data) => clearTimeout(this.timers[data.ieeeAddr]));
this.eventBus.onDeviceLeave(this, (data) => clearTimeout(this.timers[data.ieeeAddr]));
this.eventBus.onDeviceAnnounce(this, (data) => this.retrieveState(data.device));
this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged);
for (const device of this.zigbee.devices(false)) {
@@ -157,8 +157,8 @@ export default class Availability extends Extension {
}
override async stop(): Promise<void> {
super.stop();
Object.values(this.timers).forEach((t) => clearTimeout(t));
super.stop();
}
private retrieveState(device: Device): void {
+2 -2
View File
@@ -6,7 +6,7 @@ abstract class Extension {
protected eventBus: EventBus;
protected enableDisableExtension: (enable: boolean, name: string) => Promise<void>;
protected restartCallback: () => void;
protected addExtension: (extension: Extension) => void;
protected addExtension: (extension: Extension) => Promise<void>;
/**
* Besides intializing variables, the constructor should do nothing!
@@ -22,7 +22,7 @@ abstract class Extension {
*/
constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState,
eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
restartCallback: () => void, addExtension: (extension: Extension) => void) {
restartCallback: () => void, addExtension: (extension: Extension) => Promise<void>) {
this.zigbee = zigbee;
this.mqtt = mqtt;
this.state = state;
+1 -1
View File
@@ -6,7 +6,7 @@ import Extension from './extension';
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: () => void, addExtension: (extension: Extension) => void) {
restartCallback: () => void, addExtension: (extension: Extension) => Promise<void>) {
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
for (const definition of utils.getExternalConvertersDefinitions(settings.get())) {
+11 -11
View File
@@ -11,7 +11,7 @@ import Extension from './extension';
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/extension/(save|remove)`);
export default class ExternalExtension extends Extension {
private requestLookup: {[s: string]: (message: KeyValue) => MQTTResponse};
private requestLookup: {[s: string]: (message: KeyValue) => Promise<MQTTResponse>};
override async start(): Promise<void> {
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
@@ -36,13 +36,13 @@ export default class ExternalExtension extends Extension {
}
}
@bind private removeExtension(message: KeyValue): MQTTResponse {
@bind private async removeExtension(message: KeyValue): Promise<MQTTResponse> {
const {name} = message;
const extensions = this.getListOfUserDefinedExtensions();
const extensionToBeRemoved = extensions.find((e) => e.name === name);
if (extensionToBeRemoved) {
this.enableDisableExtension(false, extensionToBeRemoved.name);
await this.enableDisableExtension(false, extensionToBeRemoved.name);
const basePath = this.getExtensionsBasePath();
const extensionFilePath = path.join(basePath, path.basename(name));
fs.unlinkSync(extensionFilePath);
@@ -54,10 +54,10 @@ export default class ExternalExtension extends Extension {
}
}
@bind private saveExtension(message: KeyValue): MQTTResponse {
@bind private async saveExtension(message: KeyValue): Promise<MQTTResponse> {
const {name, code} = message;
const ModuleConstructor = utils.loadModuleFromText(code) as Extension;
this.loadExtension(ModuleConstructor);
const ModuleConstructor = utils.loadModuleFromText(code) as typeof Extension;
await this.loadExtension(ModuleConstructor);
const basePath = this.getExtensionsBasePath();
/* istanbul ignore else */
if (!fs.existsSync(basePath)) {
@@ -75,7 +75,7 @@ export default class ExternalExtension extends Extension {
if (match && this.requestLookup[match[1].toLowerCase()]) {
const message = utils.parseJSON(data.message, data.message) as KeyValue;
try {
const response = this.requestLookup[match[1].toLowerCase()](message);
const response = await this.requestLookup[match[1].toLowerCase()](message);
await this.mqtt.publish(`bridge/response/extension/${match[1]}`, stringify(response));
} catch (error) {
logger.error(`Request '${data.topic}' failed with error: '${error.message}'`);
@@ -85,11 +85,11 @@ export default class ExternalExtension extends Extension {
}
}
@bind private loadExtension(ConstructorClass: Extension): void {
this.enableDisableExtension(false, ConstructorClass.constructor.name);
@bind private async loadExtension(ConstructorClass: typeof Extension): Promise<void> {
await this.enableDisableExtension(false, ConstructorClass.name);
// @ts-ignore
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 loadUserDefinedExtensions(): void {
+13 -11
View File
@@ -1,5 +1,5 @@
import http from 'http';
import serveStatic from 'serve-static';
import gzipStatic, {RequestHandler} from 'connect-gzip-static';
import finalhandler from 'finalhandler';
import logger from '../util/logger';
import frontend from 'zigbee2mqtt-frontend';
@@ -23,12 +23,12 @@ export default class Frontend extends Extension {
private retainedMessages = new Map();
private server: http.Server;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private fileServer: serveStatic.RequestHandler<any>;
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: () => void, addExtension: (extension: Extension) => void) {
restartCallback: () => void, addExtension: (extension: Extension) => Promise<void>) {
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
this.eventBus.onMQTTMessagePublished(this, this.onMQTTPublishMessage);
}
@@ -37,14 +37,16 @@ export default class Frontend extends Extension {
this.server = http.createServer(this.onRequest);
this.server.on('upgrade', this.onUpgrade);
/* istanbul ignore next */ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const options = {setHeaders: (res: any, path: any): void => {
if (path.endsWith('index.html')) {
res.setHeader('Cache-Control', 'no-store');
}
}};
this.fileServer = serveStatic(frontend.getPath(), options);
/* istanbul ignore next */
const options = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setHeaders: (res: any, path: string): void => {
if (path.endsWith('index.html')) {
res.setHeader('Cache-Control', 'no-store');
}
},
};
this.fileServer = gzipStatic(frontend.getPath(), options);
this.wss = new WebSocket.Server({noServer: true});
this.wss.on('connection', this.onWebSocketConnection);
+233 -107
View File
@@ -40,7 +40,6 @@ export default class HomeAssistant extends Extension {
private discovered: {[s: string]: {topics: Set<string>, mockProperties: Set<string>}} = {};
private mapping: {[s: string]: DiscoveryEntry[]} = {};
private discoveredTriggers : {[s: string]: Set<string>}= {};
private legacyApi = settings.get().advanced.legacy_api;
private discoveryTopic = settings.get().advanced.homeassistant_discovery_topic;
private statusTopic = settings.get().advanced.homeassistant_status_topic;
private entityAttributes = settings.get().advanced.homeassistant_legacy_entity_attributes;
@@ -48,7 +47,7 @@ export default class HomeAssistant extends Extension {
constructor(zigbee: Zigbee, mqtt: MQTT, state: State, publishEntityState: PublishEntityState,
eventBus: EventBus, enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
restartCallback: () => void, addExtension: (extension: Extension) => void) {
restartCallback: () => void, addExtension: (extension: Extension) => Promise<void>) {
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
if (settings.get().experimental.output === 'attribute') {
throw new Error('Home Assistant integration is not possible with attribute output!');
@@ -197,6 +196,7 @@ export default class HomeAssistant extends Extension {
// Temperature
current_temperature_topic: true,
current_temperature_template: `{{ value_json.${temperature.property} }}`,
command_topic_prefix: endpoint,
},
};
@@ -219,7 +219,7 @@ export default class HomeAssistant extends Extension {
discoveryEntry.mockProperties.push(state.property);
discoveryEntry.discovery_payload.action_topic = true;
discoveryEntry.discovery_payload.action_template = `{% set values = ` +
`{None:None,'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'}` +
`{None:None,'idle':'off','heat':'heating','cool':'cooling','fan_only':'fan'}` +
` %}{{ values[value_json.${state.property}] }}`;
}
@@ -266,6 +266,45 @@ export default class HomeAssistant extends Extension {
`{{ value_json.${awayMode.property} }}`;
}
const tempCalibration = firstExpose.features.find((f) => f.name === 'local_temperature_calibration');
if (tempCalibration) {
const discoveryEntry: DiscoveryEntry = {
type: 'number',
object_id: endpoint ? `${tempCalibration.name}_${endpoint}` : `${tempCalibration.name}`,
mockProperties: [tempCalibration.property],
discovery_payload: {
value_template: `{{ value_json.${tempCalibration.property} }}`,
command_topic: true,
command_topic_prefix: endpoint,
command_topic_postfix: tempCalibration.property,
entity_category: 'config',
icon: 'mdi:math-compass',
...(tempCalibration.unit && {unit_of_measurement: tempCalibration.unit}),
},
};
if (tempCalibration.value_min != null) discoveryEntry.discovery_payload.min = tempCalibration.value_min;
if (tempCalibration.value_max != null) discoveryEntry.discovery_payload.max = tempCalibration.value_max;
discoveryEntries.push(discoveryEntry);
}
const piHeatingDemand = firstExpose.features.find((f) => f.name === 'pi_heating_demand');
if (piHeatingDemand) {
const discoveryEntry = {
type: 'sensor',
object_id: endpoint ? `${piHeatingDemand.name}_${endpoint}` : `${piHeatingDemand.name}`,
mockProperties: [piHeatingDemand.property],
discovery_payload: {
value_template: `{{ value_json.${piHeatingDemand.property} }}`,
...(piHeatingDemand.unit && {unit_of_measurement: piHeatingDemand.unit}),
entity_category: 'diagnostic',
icon: 'mdi:radiator',
},
};
discoveryEntries.push(discoveryEntry);
}
discoveryEntries.push(discoveryEntry);
} else if (firstExpose.type === 'lock') {
assert(!endpoint, `Endpoint not supported for lock type`);
@@ -307,26 +346,28 @@ export default class HomeAssistant extends Extension {
discoveryEntries.push(discoveryEntry);
} else if (firstExpose.type === 'cover') {
const position = exposes.find((expose) => expose.features.find((e) => e.name === 'position'));
const hasTilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'));
const tilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'));
const discoveryEntry: DiscoveryEntry = {
type: 'cover',
mockProperties: [],
object_id: endpoint ? `cover_${endpoint}` : 'cover',
discovery_payload: {},
discovery_payload: {
command_topic_prefix: endpoint,
},
};
// For covers only supporting tilt don't discover the command/state_topic, otherwise
// HA does not correctly reflect the state
// - https://github.com/home-assistant/core/issues/51793
// - https://github.com/Koenkk/zigbee-herdsman-converters/pull/2663
if (!hasTilt || (hasTilt && position)) {
if (!tilt || (tilt && position)) {
discoveryEntry.discovery_payload.command_topic = true;
discoveryEntry.discovery_payload.state_topic = !position;
discoveryEntry.discovery_payload.command_topic_prefix = endpoint;
}
if (!position && !hasTilt) {
if (!position && !tilt) {
discoveryEntry.discovery_payload.optimistic = true;
}
@@ -340,12 +381,12 @@ export default class HomeAssistant extends Extension {
};
}
if (hasTilt) {
assert(!endpoint, `Endpoint with tilt not supported for cover type`);
if (tilt) {
const t = tilt.features.find((f) => f.name === 'tilt');
discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload,
tilt_command_topic: true,
tilt_status_topic: true,
tilt_status_template: '{{ value_json.tilt }}',
tilt_status_template: `{{ value_json.${getProperty(t)} }}`,
};
}
@@ -378,8 +419,8 @@ 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'].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)) {
@@ -405,6 +446,7 @@ export default class HomeAssistant extends Extension {
`{{ {${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 = true;
discoveryEntry.discovery_payload.preset_mode_value_template =
@@ -416,15 +458,31 @@ export default class HomeAssistant extends Extension {
discoveryEntries.push(discoveryEntry);
} else if (firstExpose.type === 'binary') {
const lookup: {[s: string]: KeyValue}= {
occupancy: {device_class: 'motion'},
battery_low: {device_class: 'battery'},
water_leak: {device_class: 'moisture'},
vibration: {device_class: 'vibration'},
contact: {device_class: 'door'},
smoke: {device_class: 'smoke'},
gas: {device_class: 'gas'},
battery_low: {entity_category: 'diagnostic', device_class: 'battery'},
button_lock: {entity_category: 'config', icon: 'mdi:lock'},
carbon_monoxide: {device_class: 'safety'},
child_lock: {entity_category: 'config', icon: 'mdi:account-lock'},
color_sync: {entity_category: 'config', icon: 'mdi:sync-circle'},
consumer_connected: {entity_category: 'diagnostic', device_class: 'connectivity'},
contact: {device_class: 'door'},
eco_mode: {entity_category: 'config', icon: 'mdi:leaf'},
expose_pin: {entity_category: 'config', icon: 'mdi:pin'},
gas: {device_class: 'gas'},
invert_cover: {entity_category: 'config', icon: 'mdi:arrow-left-right'},
led_disabled_night: {entity_category: 'config', icon: 'mdi:led-off'},
led_indication: {entity_category: 'config', icon: 'mdi:led-on'},
legacy: {entity_category: 'config', icon: 'mdi:cog'},
moving: {device_class: 'moving'},
no_position_support: {entity_category: 'config', icon: 'mdi:minus-circle-outline'},
occupancy: {device_class: 'motion'},
power_outage_memory: {entity_category: 'config', icon: 'mdi:memory'},
presence: {device_class: 'presence'},
smoke: {device_class: 'smoke'},
sos: {device_class: 'safety'},
tamper: {device_class: 'tamper'},
test: {entity_category: 'diagnostic', icon: 'mdi:test-tube'},
vibration: {device_class: 'vibration'},
water_leak: {device_class: 'moisture'},
};
/**
@@ -469,44 +527,102 @@ export default class HomeAssistant extends Extension {
}
} else if (firstExpose.type === 'numeric') {
const lookup: {[s: string]: KeyValue} = {
battery: {device_class: 'battery', state_class: 'measurement'},
temperature: {device_class: 'temperature', state_class: 'measurement'},
angle: {icon: 'angle-acute'},
angle_axis: {icon: 'angle-acute'},
aqi: {device_class: 'aqi', state_class: 'measurement'},
auto_relock_time: {entity_category: 'config', icon: 'mdi:timer'},
away_preset_days: {entity_category: 'config', icon: 'mdi:timer'},
away_preset_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
battery: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'},
battery_voltage: {device_class: 'voltage', entity_category: 'diagnostic', state_class: 'measurement'},
boost_time: {entity_category: 'config', icon: 'mdi:timer'},
calibration: {entity_category: 'config'},
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',
},
cube_side: {icon: 'mdi:cube'},
current: {
device_class: 'current',
enabled_by_default: false,
entity_category: 'diagnostic',
state_class: 'measurement',
},
current_phase_b: {
device_class: 'current',
enabled_by_default: false,
entity_category: 'diagnostic',
state_class: 'measurement',
},
current_phase_c: {
device_class: 'current',
enabled_by_default: false,
entity_category: 'diagnostic',
state_class: 'measurement',
},
deadzone_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
device_temperature: {
device_class: 'temperature', entity_category: 'diagnostic', state_class: 'measurement',
},
eco2: {device_class: 'carbon_dioxide', state_class: 'measurement'},
eco_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
energy: {device_class: 'energy', state_class: 'total_increasing'},
formaldehyd: {state_class: 'measurement'},
gas_density: {icon: 'mdi:google-circles-communities', state_class: 'measurement'},
hcho: {icon: 'mdi:air-filter', state_class: 'measurement'},
humidity: {device_class: 'humidity', state_class: 'measurement'},
illuminance_lux: {device_class: 'illuminance', state_class: 'measurement'},
illuminance: {
device_class: 'illuminance', enabled_by_default: false, state_class: 'measurement',
illuminance: {device_class: 'illuminance', enabled_by_default: false, state_class: 'measurement'},
linkquality: {
enabled_by_default: false,
entity_category: 'diagnostic',
icon: 'mdi:signal',
state_class: 'measurement',
},
soil_moisture: {icon: 'mdi:water-percent', state_class: 'measurement'},
local_temperature: {device_class: 'temperature', state_class: 'measurement'},
max_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
max_temperature_limit: {entity_category: 'config', icon: 'mdi:thermometer'},
min_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
measurement_poll_interval: {entity_category: 'config', icon: 'mdi:clock-out'},
occupancy_timeout: {entity_category: 'config', icon: 'mdi:timer'},
pm10: {device_class: 'pm10', state_class: 'measurement'},
pm25: {device_class: 'pm25', state_class: 'measurement'},
position: {icon: 'mdi:valve', state_class: 'measurement'},
power: {device_class: 'power', entity_category: 'diagnostic', state_class: 'measurement'},
precision: {entity_category: 'config', icon: 'mdi:decimal-comma-increase'},
pressure: {device_class: 'pressure', state_class: 'measurement'},
power: {device_class: 'power', state_class: 'measurement'},
linkquality: {enabled_by_default: false, icon: 'mdi:signal', state_class: 'measurement'},
current: {device_class: 'current', state_class: 'measurement'},
voltage: {device_class: 'voltage', enabled_by_default: false, state_class: 'measurement'},
current_phase_b: {device_class: 'current', state_class: 'measurement'},
voltage_phase_b: {
device_class: 'voltage', enabled_by_default: false, state_class: 'measurement',
presence_timeout: {entity_category: 'config', icon: 'mdi:timer'},
requested_brightness_level: {
enabled_by_default: false, entity_category: 'diagnostic', icon: 'mdi:brightness-5',
},
current_phase_c: {device_class: 'current', state_class: 'measurement'},
voltage_phase_c: {
device_class: 'voltage', enabled_by_default: false, state_class: 'measurement',
},
energy: {
device_class: 'energy',
state_class: 'total_increasing',
requested_brightness_percent: {
enabled_by_default: false, entity_category: 'diagnostic', icon: 'mdi:brightness-5',
},
smoke_density: {icon: 'mdi:google-circles-communities', state_class: 'measurement'},
gas_density: {icon: 'mdi:google-circles-communities', state_class: 'measurement'},
pm25: {device_class: 'pm25', state_class: 'measurement'},
pm10: {device_class: 'pm10', state_class: 'measurement'},
voc: {icon: 'mdi:air-filter', state_class: 'measurement'},
aqi: {device_class: 'aqi', state_class: 'measurement'},
hcho: {icon: 'mdi:air-filter', state_class: 'measurement'},
requested_brightness_level: {enabled_by_default: false, icon: 'mdi:brightness-5'},
requested_brightness_percent: {enabled_by_default: false, icon: 'mdi:brightness-5'},
eco2: {device_class: 'carbon_dioxide', state_class: 'measurement'},
co2: {device_class: 'carbon_dioxide', state_class: 'measurement'},
local_temperature: {device_class: 'temperature', state_class: 'measurement'},
soil_moisture: {icon: 'mdi:water-percent', state_class: 'measurement'},
temperature: {device_class: 'temperature', state_class: 'measurement'},
transition: {entity_category: 'config', icon: 'mdi:transition'},
voc: {device_class: 'volatile_organic_compounds', state_class: 'measurement'},
vibration_timeout: {entity_category: 'config', icon: 'mdi:timer'},
voltage: {
device_class: 'voltage',
enabled_by_default: false,
entity_category: 'diagnostic',
state_class: 'measurement',
},
voltage_phase_b: {
device_class: 'voltage',
enabled_by_default: false,
entity_category: 'diagnostic',
state_class: 'measurement',
},
voltage_phase_c: {
device_class: 'voltage',
enabled_by_default: false,
entity_category: 'diagnostic',
state_class: 'measurement',
},
x_axis: {icon: 'mdi:axis-x-arrow'},
y_axis: {icon: 'mdi:axis-y-arrow'},
z_axis: {icon: 'mdi:axis-z-arrow'},
@@ -533,7 +649,7 @@ export default class HomeAssistant extends Extension {
* breaking changes for sensors already existing in HA (legacy).
*/
if (allowsSet) {
const discoveryEntry = {
const discoveryEntry: DiscoveryEntry = {
type: 'number',
object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`,
mockProperties: [firstExpose.property],
@@ -542,32 +658,41 @@ export default class HomeAssistant extends Extension {
command_topic: true,
command_topic_prefix: endpoint,
command_topic_postfix: firstExpose.property,
min: firstExpose.value_min != null ? firstExpose.value_min : -65535,
max: firstExpose.value_max != null ? firstExpose.value_max : 65535,
...(firstExpose.unit && {unit_of_measurement: firstExpose.unit}),
...lookup[firstExpose.name],
},
};
if (firstExpose.value_min != null) discoveryEntry.discovery_payload.min = firstExpose.value_min;
if (firstExpose.value_max != null) discoveryEntry.discovery_payload.max = firstExpose.value_max;
discoveryEntries.push(discoveryEntry);
}
} else if (firstExpose.type === 'enum') {
const lookup: {[s: string]: KeyValue} = {
action: {icon: 'mdi:gesture-double-tap'},
backlight_auto_dim: {enabled_by_default: false, icon: 'mdi:brightness-auto'},
backlight_mode: {enabled_by_default: false, icon: 'mdi:lightbulb'},
color_power_on_behavior: {enabled_by_default: false, icon: 'mdi:palette'},
device_mode: {enabled_by_default: false, icon: 'mdi:tune'},
keep_time: {enabled_by_default: false, icon: 'mdi:av-timer'},
melody: {icon: 'mdi:music-note'},
mode_phase_control: {enabled_by_default: false, icon: 'mdi:tune'},
mode: {enabled_by_default: false, icon: 'mdi:tune'},
motion_sensitivity: {enabled_by_default: false, icon: 'mdi:tune'},
operation_mode: {enabled_by_default: false, icon: 'mdi:tune'},
power_on_behavior: {enabled_by_default: false, icon: 'mdi:power-settings'},
power_outage_memory: {enabled_by_default: false, icon: 'mdi:power-settings'},
sensitivity: {enabled_by_default: false, icon: 'mdi:tune'},
sensors_type: {enabled_by_default: false, icon: 'mdi:tune'},
switch_type: {enabled_by_default: false, icon: 'mdi:tune'},
volume: {icon: 'mdi: volume-high'},
backlight_auto_dim: {entity_category: 'config', icon: 'mdi:brightness-auto'},
backlight_mode: {entity_category: 'config', icon: 'mdi:lightbulb'},
color_power_on_behavior: {entity_category: 'config', icon: 'mdi:palette'},
device_mode: {entity_category: 'config', icon: 'mdi:tune'},
effect: {enabled_by_default: false, icon: 'mdi:palette'},
force: {enabled_by_default: false, icon: 'mdi:valve'},
keep_time: {entity_category: 'config', icon: 'mdi:av-timer'},
keypad_lockout: {entity_category: 'config', icon: 'mdi:lock'},
melody: {entity_category: 'config', icon: 'mdi:music-note'},
mode_phase_control: {entity_category: 'config', icon: 'mdi:tune'},
mode: {entity_category: 'config', icon: 'mdi:tune'},
motion_sensitivity: {entity_category: 'config', icon: 'mdi:tune'},
operation_mode: {entity_category: 'config', icon: 'mdi:tune'},
power_on_behavior: {entity_category: 'config', icon: 'mdi:power-settings'},
power_outage_memory: {entity_category: 'config', icon: 'mdi:power-settings'},
sensitivity: {entity_category: 'config', icon: 'mdi:tune'},
sensors_type: {entity_category: 'config', icon: 'mdi:tune'},
sound_volume: {entity_category: 'config', icon: 'mdi:volume-high'},
switch_type: {entity_category: 'config', icon: 'mdi:tune'},
thermostat_unit: {entity_category: 'config', icon: 'mdi:thermometer'},
volume: {entity_category: 'config', icon: 'mdi: volume-high'},
week: {entity_category: 'config', icon: 'mdi:calendar-clock'},
};
if (firstExpose.access & ACCESS_STATE) {
@@ -800,9 +925,11 @@ export default class HomeAssistant extends Extension {
object_id: 'last_seen',
mockProperties: ['last_seen'],
discovery_payload: {
icon: 'mdi:clock',
value_template: '{{ value_json.last_seen }}',
icon: 'mdi:clock',
enabled_by_default: false,
entity_category: 'diagnostic',
device_class: 'timestamp',
},
});
}
@@ -816,24 +943,25 @@ export default class HomeAssistant extends Extension {
icon: 'mdi:update',
value_template: `{{ value_json['update']['state'] }}`,
enabled_by_default: false,
entity_category: 'diagnostic',
},
};
configs.push(updateStateSensor);
if (this.legacyApi) {
const updateAvailableSensor = {
type: 'binary_sensor',
object_id: 'update_available',
mockProperties: ['update_available'],
discovery_payload: {
payload_on: true,
payload_off: false,
value_template: '{{ value_json.update_available}}',
enabled_by_default: false,
},
};
configs.push(updateAvailableSensor);
}
const updateAvailableSensor = {
type: 'binary_sensor',
object_id: 'update_available',
mockProperties: ['update_available'],
discovery_payload: {
payload_on: true,
payload_off: false,
value_template: `{{ value_json['update']['state'] == "available" }}`,
enabled_by_default: true,
device_class: 'update',
entity_category: 'diagnostic',
},
};
configs.push(updateAvailableSensor);
}
if (isDevice && entity.settings.hasOwnProperty('legacy') && !entity.settings.legacy) {
@@ -882,7 +1010,8 @@ export default class HomeAssistant extends Extension {
this.discovered[discoverKey] = {topics: new Set(), mockProperties: new Set()};
this.getConfigs(entity).forEach((config) => {
const payload = {...config.discovery_payload};
let stateTopic = `${settings.get().mqtt.base_topic}/${entity.name}`;
const baseTopic = `${settings.get().mqtt.base_topic}/${entity.name}`;
let stateTopic = baseTopic;
if (payload.state_topic_postfix) {
stateTopic += `/${payload.state_topic_postfix}`;
delete payload.state_topic_postfix;
@@ -932,19 +1061,14 @@ export default class HomeAssistant extends Extension {
/* istanbul ignore next */
if (availabilityEnabled) {
payload.availability_mode = 'all';
payload.availability.push({topic: `${settings.get().mqtt.base_topic}/${entity.name}/availability`});
payload.availability.push({topic: `${baseTopic}/availability`});
}
let commandTopic = `${settings.get().mqtt.base_topic}/${entity.name}/`;
if (payload.command_topic_prefix) {
commandTopic += `${payload.command_topic_prefix}/`;
delete payload.command_topic_prefix;
}
commandTopic += 'set';
if (payload.command_topic_postfix) {
commandTopic += `/${payload.command_topic_postfix}`;
delete payload.command_topic_postfix;
}
const commandTopicPrefix = payload.command_topic_prefix ? `${payload.command_topic_prefix}/` : '';
delete payload.command_topic_prefix;
const commandTopicPostfix = payload.command_topic_postfix ? `/${payload.command_topic_postfix}` : '';
delete payload.command_topic_postfix;
const commandTopic = `${baseTopic}/${commandTopicPrefix}set${commandTopicPostfix}`;
if (payload.command_topic) {
payload.command_topic = commandTopic;
@@ -955,9 +1079,7 @@ export default class HomeAssistant extends Extension {
}
if (payload.tilt_command_topic) {
// Home Assistant does not support templates to set tilt (as of 2019-08-17),
// so we (have to) use a subtopic.
payload.tilt_command_topic = commandTopic + '/tilt';
payload.tilt_command_topic = `${baseTopic}/${commandTopicPrefix}set/tilt`;
}
if (payload.mode_state_topic) {
@@ -965,11 +1087,11 @@ export default class HomeAssistant extends Extension {
}
if (payload.mode_command_topic) {
payload.mode_command_topic = `${stateTopic}/set/system_mode`;
payload.mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/system_mode`;
}
if (payload.hold_command_topic) {
payload.hold_command_topic = `${stateTopic}/set/preset`;
payload.hold_command_topic = `${baseTopic}/${commandTopicPrefix}set/preset`;
}
if (payload.hold_state_topic) {
@@ -981,7 +1103,7 @@ export default class HomeAssistant extends Extension {
}
if (payload.away_mode_command_topic) {
payload.away_mode_command_topic = `${stateTopic}/set/away_mode`;
payload.away_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/away_mode`;
}
if (payload.current_temperature_topic) {
@@ -1001,15 +1123,18 @@ export default class HomeAssistant extends Extension {
}
if (payload.temperature_command_topic) {
payload.temperature_command_topic = `${stateTopic}/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 = `${stateTopic}/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 = `${stateTopic}/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) {
@@ -1017,7 +1142,7 @@ export default class HomeAssistant extends Extension {
}
if (payload.fan_mode_command_topic) {
payload.fan_mode_command_topic = `${stateTopic}/set/fan_mode`;
payload.fan_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/fan_mode`;
}
if (payload.percentage_state_topic) {
@@ -1025,7 +1150,7 @@ export default class HomeAssistant extends Extension {
}
if (payload.percentage_command_topic) {
payload.percentage_command_topic = `${stateTopic}/set/fan_mode`;
payload.percentage_command_topic = `${baseTopic}/${commandTopicPrefix}set/fan_mode`;
}
if (payload.preset_mode_state_topic) {
@@ -1033,7 +1158,7 @@ export default class HomeAssistant extends Extension {
}
if (payload.preset_mode_command_topic) {
payload.preset_mode_command_topic = `${stateTopic}/set/fan_mode`;
payload.preset_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/fan_mode`;
}
if (payload.action_topic) {
@@ -1157,6 +1282,7 @@ export default class HomeAssistant extends Extension {
if (entity.isDevice()) {
payload.model = `${entity.definition.description} (${entity.definition.model})`;
payload.manufacturer = entity.definition.vendor;
payload.sw_version = entity.zh.softwareBuildID;
}
if (settings.get().frontend?.url) {
+1 -1
View File
@@ -330,7 +330,7 @@ export default class BridgeLegacy extends Extension {
} catch (error) {
logger.error(`Failed to ${lookup[action][2]} ${entity.name} (${error})`);
// eslint-disable-next-line
logger.error(`See https://www.zigbee2mqtt.io/information/mqtt_topics_and_message_structure.html#zigbee2mqttbridgeconfigremove for more info`);
logger.error(`See https://www.zigbee2mqtt.io/guide/usage/mqtt_topics_and_messages.html#zigbee2mqtt-bridge-request for more info`);
this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}_failed`, message}));
}
+1 -1
View File
@@ -7,7 +7,7 @@ import Extension from './extension';
export default class OnEvent extends Extension {
override async start(): Promise<void> {
for (const device of this.zigbee.devices(false)) {
await this.callOnEvent(device, 'start', {});
this.callOnEvent(device, 'start', {});
}
this.eventBus.onDeviceMessage(this, (data) => this.callOnEvent(data.device, 'message', this.convertData(data)));
+9 -3
View File
@@ -45,7 +45,7 @@ export default class OTAUpdate extends Extension {
if (data.type !== 'commandQueryNextImageRequest' || !data.device.definition) return;
logger.debug(`Device '${data.device.name}' requested OTA`);
const supportsOTA = data.device.definition.hasOwnProperty('ota');
let supportsOTA = data.device.definition.hasOwnProperty('ota');
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
@@ -56,8 +56,14 @@ export default class OTAUpdate extends Extension {
if (!check || this.inProgress.has(data.device.ieeeAddr)) return;
this.lastChecked[data.device.ieeeAddr] = Date.now();
const available = await data.device.definition.ota.isUpdateAvailable(
data.device.zh, logger, data.data);
let available = false;
try {
available = await data.device.definition.ota.isUpdateAvailable(data.device.zh, logger, data.data);
} catch (e) {
supportsOTA = false;
logger.debug(`Failed to check if update available for '${data.device.name}' (${e.message})`);
}
const payload = this.getEntityPublishPayload(available ? 'available' : 'idle');
this.publishEntityState(data.device, payload);
+13 -4
View File
@@ -4,6 +4,7 @@ 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';
type DebounceFunction = (() => void) & { clear(): void; } & { flush(): void; };
@@ -24,7 +25,7 @@ export default class Receive extends Extension {
*/
if (data.entity.isDevice() && this.debouncers[data.entity.ieeeAddr] &&
data.stateChangeReason !== 'publishDebounce' && data.stateChangeReason !== 'lastSeenChanged') {
for (const key of Object.keys(data.message)) {
for (const key of Object.keys(data.payload)) {
delete this.debouncers[data.entity.ieeeAddr].payload[key];
}
}
@@ -76,7 +77,8 @@ export default class Receive extends Extension {
logger.warn(
`Received message from unsupported device with Zigbee model '${data.device.zh.modelID}' ` +
`and manufacturer name '${data.device.zh.manufacturerName}'`);
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`);
// eslint-disable-next-line max-len
logger.warn(`Please see: https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`);
}
return false;
@@ -106,7 +108,11 @@ export default class Receive extends Extension {
data = {...data, device: this.zigbee.deviceByNetworkAddress(data.groupID)};
}
if (!this.shouldProcess(data)) return;
if (!this.shouldProcess(data)) {
utils.publishLastSeen({device: data.device, reason: 'messageEmitted'},
settings.get(), true, this.publishEntityState);
return;
}
const converters = data.device.definition.fromZigbee.filter((c) => {
const type = Array.isArray(c.type) ? c.type.includes(data.type) : c.type === data.type;
@@ -114,7 +120,7 @@ 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'];
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)}'`);
@@ -162,6 +168,9 @@ export default class Receive extends Extension {
if (Object.keys(payload).length) {
publish(payload);
} else {
utils.publishLastSeen({device: data.device, reason: 'messageEmitted'},
settings.get(), true, this.publishEntityState);
}
}
}
+2
View File
@@ -69,6 +69,8 @@ export default class MQTT {
return new Promise((resolve, reject) => {
this.client = mqtt.connect(mqttSettings.server, options);
// @ts-ignore https://github.com/Koenkk/zigbee2mqtt/issues/9822
this.client.stream.setMaxListeners(0);
const onConnect = this.onConnect;
this.client.on('connect', async () => {
+5 -3
View File
@@ -95,7 +95,7 @@ declare global {
}
interface DefinitionExposeFeature {name: string, endpoint?: string,
property: string, value_max?: number, value_min?: number,
property: string, value_max?: number, value_min?: number, unit?: string,
value_off?: string, value_on?: string, value_step?: number, values: string[], access: number}
interface DefinitionExpose {
@@ -136,7 +136,8 @@ declare global {
type StateChange = {
entity: Device | Group, from: KeyValue, to: KeyValue, reason: string | null, update: KeyValue };
type PermitJoinChanged = ZHEvents.PermitJoinChangedPayload;
type LastSeenChanged = { device: Device };
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' };
@@ -145,7 +146,8 @@ declare global {
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 };
type PublishEntityState = {entity: Group | Device, message: KeyValue, stateChangeReason: StateChangeReason,
payload: KeyValue};
type DeviceMessage = {
type: ZHEvents.MessagePayloadType;
device: Device;
+9
View File
@@ -0,0 +1,9 @@
declare module 'zigbee2mqtt-frontend' {
export function getPath(): string;
}
declare module 'connect-gzip-static' {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type RequestHandler = (req: any, res: any) => void;
export default function gzipStatic(root: string, options?: Record<string, unknown>): RequestHandler;
}
-3
View File
@@ -1,3 +0,0 @@
declare module 'zigbee2mqtt-frontend' {
export function getPath(): string;
}
+2 -2
View File
@@ -22,8 +22,8 @@ function getPath(): string {
}
// eslint-disable-next-line camelcase
function __testingOnly_reload(): void {
function testingOnlyReload(): void {
load();
}
export default {joinPath, getPath, __testingOnly_reload};
export default {joinPath, getPath, testingOnlyReload};
+3 -3
View File
@@ -55,7 +55,7 @@
"type": "object",
"title": "Active",
"requiresRestart": true,
"description": "Options for passive devices (routers/mains powered)",
"description": "Options for passive devices (mostly battery powered)",
"properties": {
"timeout": {
"type": "number",
@@ -71,7 +71,7 @@
],
"title": "Availability",
"requiresRestart": true,
"description": "Checks wether devices are online/offline"
"description": "Checks whether devices are online/offline"
},
"mqtt": {
"type": "object",
@@ -652,7 +652,7 @@
"type": "string",
"title": "Bind host",
"description": "Frontend binding host",
"default":" 0.0.0.0",
"default": "0.0.0.0",
"requiresRestart": true
},
"auth_token": {
+1 -1
View File
@@ -85,7 +85,7 @@ const defaults: RecursivePartial<Settings> = {
*
* Therefore Zigbee2MQTT BY DEFAULT caches all values and resend it with every message.
* advanced.cache_state in configuration.yaml allows to configure this.
* https://www.zigbee2mqtt.io/configuration/configuration.html
* https://www.zigbee2mqtt.io/guide/configuration/
*/
cache_state: true,
cache_state_persistent: true,
+17 -1
View File
@@ -70,6 +70,7 @@ async function getZigbee2MQTTVersion(includeCommitHash=true): Promise<{commitHas
commitHash = commit.shortHash;
}
commitHash = commitHash.trim();
resolve({commitHash, version});
});
});
@@ -280,11 +281,26 @@ const hours = (hours: number): number => 1000 * 60 * 60 * hours;
const minutes = (minutes: number): number => 1000 * 60 * minutes;
const seconds = (seconds: number): number => 1000 * seconds;
function publishLastSeen(data: eventdata.LastSeenChanged, settings: Settings, allowMessageEmitted: boolean,
publishEntityState: PublishEntityState): 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
* message based on the received zigbee message. In this case allowMessageEmitted has to be true.
* - In case reason !== messageEmitted, controller.ts will call this based on the zigbee-herdsman
* lastSeenChanged event.
*/
const allow = data.reason !== 'messageEmitted' || (data.reason === 'messageEmitted' && allowMessageEmitted);
if (settings.advanced.last_seen && settings.advanced.last_seen !== 'disable' && allow) {
publishEntityState(data.device, {}, 'lastSeenChanged');
}
}
export default {
endpointNames, capitalize, getZigbee2MQTTVersion, getDependencyVersion, formatDate, objectHasProperties,
equalsPartial, getObjectProperty, getResponse, parseJSON, loadModuleFromText, loadModuleFromFile,
getExternalConvertersDefinitions, removeNullPropertiesFromObject, toNetworkAddressHex, toSnakeCase,
parseEntityID, isEndpoint, isZHGroup, hours, minutes, seconds, validateFriendlyName, sleep,
sanitizeImageParameter, isAvailabilityEnabledForDevice,
sanitizeImageParameter, isAvailabilityEnabledForDevice, publishLastSeen,
};
+6 -8
View File
@@ -63,7 +63,7 @@ export default class Zigbee {
this.herdsman.on('adapterDisconnected', () => this.eventBus.emitAdapterDisconnected());
this.herdsman.on('lastSeenChanged', (data: ZHEvents.LastSeenChangedPayload) => {
this.eventBus.emitLastSeenChanged({device: this.resolveDevice(data.device.ieeeAddr)});
this.eventBus.emitLastSeenChanged({device: this.resolveDevice(data.device.ieeeAddr), reason: data.reason});
});
this.herdsman.on('permitJoinChanged', (data: ZHEvents.PermitJoinChangedPayload) => {
this.eventBus.emitPermitJoinChanged(data);
@@ -98,7 +98,9 @@ export default class Zigbee {
const device = this.resolveDevice(data.device.ieeeAddr);
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}` : ``));
(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});
});
@@ -128,11 +130,6 @@ export default class Zigbee {
}
}
// Check if we have to turn off the led
if (settings.get().serial.disable_led) {
await this.herdsman.setLED(false);
}
// Check if we have to set a transmit power
if (settings.get().experimental.hasOwnProperty('transmit_power')) {
const transmitPower = settings.get().experimental.transmit_power;
@@ -154,7 +151,8 @@ export default class Zigbee {
} else {
logger.warn(`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/how_tos/how_to_support_new_devices.html`);
// eslint-disable-next-line max-len
`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`);
+4029 -4156
View File
File diff suppressed because it is too large Load Diff
+23 -24
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "1.22.0",
"version": "1.22.0-dev",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"repository": {
@@ -35,9 +35,10 @@
},
"homepage": "https://koenkk.github.io/zigbee2mqtt",
"dependencies": {
"ajv": "^8.6.3",
"ajv": "^8.8.2",
"bind-decorator": "^1.0.11",
"core-js": "^3.18.2",
"connect-gzip-static": "^2.1.1",
"core-js": "^3.19.1",
"debounce": "^1.2.1",
"deep-object-diff": "^1.1.0",
"fast-deep-equal": "^3.1.3",
@@ -52,38 +53,36 @@
"object-assign-deep": "^0.4.0",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"serve-static": "^1.14.1",
"source-map-support": "^0.5.20",
"source-map-support": "^0.5.21",
"winston": "^3.3.3",
"winston-syslog": "^2.4.4",
"ws": "^8.2.3",
"zigbee-herdsman": "0.13.164",
"zigbee-herdsman-converters": "14.0.303",
"zigbee2mqtt-frontend": "0.6.30"
"ws": "^8.3.0",
"zigbee-herdsman": "0.13.176",
"zigbee-herdsman-converters": "14.0.336",
"zigbee2mqtt-frontend": "0.6.46"
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/plugin-proposal-decorators": "^7.15.4",
"@babel/preset-env": "^7.15.6",
"@babel/preset-typescript": "^7.15.0",
"@babel/core": "^7.16.0",
"@babel/plugin-proposal-decorators": "^7.16.4",
"@babel/preset-env": "^7.16.4",
"@babel/preset-typescript": "^7.16.0",
"@types/debounce": "^1.2.1",
"@types/finalhandler": "^1.1.1",
"@types/humanize-duration": "^3.25.1",
"@types/jest": "^27.0.2",
"@types/js-yaml": "^4.0.3",
"@types/humanize-duration": "^3.27.0",
"@types/jest": "^27.0.3",
"@types/js-yaml": "^4.0.5",
"@types/object-assign-deep": "^0.4.0",
"@types/rimraf": "^3.0.2",
"@types/serve-static": "^1.13.10",
"@types/ws": "^8.2.0",
"@typescript-eslint/eslint-plugin": "^4.32.0",
"@typescript-eslint/parser": "^4.32.0",
"babel-jest": "^27.2.4",
"eslint": "^7.32.0",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"babel-jest": "^27.3.1",
"eslint": "^8.3.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-jest": "^24.5.0",
"jest": "^27.2.4",
"eslint-plugin-jest": "^25.3.0",
"jest": "^27.3.1",
"tmp": "^0.2.1",
"typescript": "^4.4.3"
"typescript": "^4.5.2"
},
"jest": {
"coverageThreshold": {
+14
View File
@@ -205,6 +205,20 @@ describe('Availability', () => {
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
});
it('Should stop pinging device when it is removed', async () => {
await resetExtension();
MQTT.publish.mockClear();
await advancedTime(utils.minutes(9));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: "bulb_color"}));
await flushPromises();
await advancedTime(utils.minutes(3));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
});
it('Should allow to be disabled', async () => {
settings.set(['availability'], false);
await resetExtension();
+21 -9
View File
@@ -43,7 +43,6 @@ describe('Controller', () => {
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"}}, logger);
expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.setLED).toHaveBeenCalledTimes(0);
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined);
@@ -229,13 +228,6 @@ describe('Controller', () => {
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined);
});
it('Start controller with disable_led', async () => {
settings.set(['serial', 'disable_led'], true);
await controller.start();
expect(zigbeeHerdsman.setLED).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.setLED).toHaveBeenCalledWith(false);
});
it('Start controller with transmit power', async () => {
settings.set(['experimental', 'transmit_power'], 14);
await controller.start();
@@ -652,9 +644,29 @@ describe('Controller', () => {
await flushPromises();
MQTT.publish.mockClear();
const device = zigbeeHerdsman.devices.remote;
await zigbeeHerdsman.events.lastSeenChanged({device});
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));
});
it('Should not publish last seen changes when reason is messageEmitted', async () => {
settings.set(['advanced', 'last_seen'], 'epoch');
await controller.start();
await flushPromises();
MQTT.publish.mockClear();
const device = zigbeeHerdsman.devices.remote;
await zigbeeHerdsman.events.lastSeenChanged({device, reason: 'messageEmitted'});
expect(MQTT.publish).toHaveBeenCalledTimes(0);
});
it('Ignore messages from coordinator', async () => {
// 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}};
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`);
});
});
+2 -2
View File
@@ -15,12 +15,12 @@ describe('Data', () => {
it('Should return correct path when ZIGBEE2MQTT_DATA set', () => {
const expected = tmp.dirSync().name;
process.env.ZIGBEE2MQTT_DATA = expected;
data.__testingOnly_reload();
data.testingOnlyReload();
const actual = data.getPath();
expect(actual).toBe(expected);
expect(data.joinPath('test')).toStrictEqual(path.join(expected, 'test'));
delete process.env.ZIGBEE2MQTT_DATA;
data.__testingOnly_reload();
data.testingOnlyReload();
});
});
});
+1 -1
View File
@@ -50,7 +50,7 @@ jest.mock('http', () => ({
}),
}));
jest.mock("serve-static", () =>
jest.mock("connect-gzip-static", () =>
jest.fn().mockImplementation((path) => {
mockNodeStatic.variables.path = path
return mockNodeStatic.implementation
+41 -35
View File
@@ -77,7 +77,7 @@ describe('HomeAssistant extension', () => {
"device":{
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
"name":"ha_discovery_group",
"sw_version":version,
"sw_version": version,
},
"max_mireds": 454,
"min_mireds": 250,
@@ -105,7 +105,7 @@ describe('HomeAssistant extension', () => {
"device":{
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
"name":"ha_discovery_group",
"sw_version":version,
"sw_version": version,
},
"json_attributes_topic":"zigbee2mqtt/ha_discovery_group",
"name":"ha_discovery_group",
@@ -135,7 +135,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -163,7 +163,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -190,7 +190,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -214,10 +214,11 @@ describe('HomeAssistant extension', () => {
'name': 'weather_sensor_battery',
'unique_id': '0x0017880104e45522_battery_zigbee2mqtt',
'enabled_by_default': true,
'entity_category': 'diagnostic',
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -234,6 +235,7 @@ describe('HomeAssistant extension', () => {
payload = {
'icon': 'mdi:signal',
'enabled_by_default': false,
'entity_category': 'diagnostic',
'unit_of_measurement': 'lqi',
'state_class': 'measurement',
'value_template': '{{ value_json.linkquality }}',
@@ -244,7 +246,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -268,7 +270,7 @@ describe('HomeAssistant extension', () => {
"manufacturer":"Xiaomi",
"model":"Aqara double key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG03LM)",
"name":"wall_switch_double",
"sw_version":version
"sw_version": null
},
"json_attributes_topic":"zigbee2mqtt/wall_switch_double",
"name":"wall_switch_double_left",
@@ -296,7 +298,7 @@ describe('HomeAssistant extension', () => {
"manufacturer":"Xiaomi",
"model":"Aqara double key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG03LM)",
"name":"wall_switch_double",
"sw_version":version
"sw_version": null
},
"json_attributes_topic":"zigbee2mqtt/wall_switch_double",
"name":"wall_switch_double_right",
@@ -330,7 +332,7 @@ describe('HomeAssistant extension', () => {
"manufacturer":"IKEA",
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
"name":"bulb",
"sw_version":version,
"sw_version": null,
},
"effect":true,
"effect_list":[
@@ -383,7 +385,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -410,7 +412,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -437,7 +439,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -522,7 +524,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'custom model',
'manufacturer': 'Not from Xiaomi',
},
@@ -570,7 +572,7 @@ describe('HomeAssistant extension', () => {
"manufacturer": "Xiaomi",
"model": "Aqara single key wired wall switch without neutral wire. Doesn't work as a router and doesn't support power meter (QBKG04LM)",
"name": "my_switch",
"sw_version": version
"sw_version": null
},
"json_attributes_topic": "zigbee2mqtt/my_switch",
"name": "my_light_name_override",
@@ -647,7 +649,7 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x0017880104e45548"
],
"name":"fan",
"sw_version":version,
"sw_version": null,
"model":"Universal wink enabled white ceiling fan premier remote control (99432)",
"manufacturer":"Hampton Bay"
},
@@ -679,7 +681,7 @@ describe('HomeAssistant extension', () => {
"manufacturer":"TuYa",
"model":"Radiator valve with thermostat (TS0601_thermostat)",
"name":"TS0601_thermostat",
"sw_version": version,
"sw_version": null,
},
"hold_command_topic":"zigbee2mqtt/TS0601_thermostat/set/preset",
"hold_modes":[
@@ -734,7 +736,7 @@ describe('HomeAssistant extension', () => {
{
identifiers: [ 'zigbee2mqtt_0x0017880104e45551' ],
name: 'smart vent',
sw_version: version,
sw_version: null,
model: 'Smart vent (SV01)',
manufacturer: 'Keen Home'
},
@@ -768,7 +770,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -895,7 +897,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -1012,7 +1014,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -1092,7 +1094,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor_renamed',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -1126,7 +1128,7 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x0017880104e45522"
],
"name":"weather_sensor_renamed",
"sw_version": version,
"sw_version": null,
"model":"Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)",
"manufacturer":"Xiaomi"
}
@@ -1161,7 +1163,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor_renamed',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -1180,8 +1182,8 @@ describe('HomeAssistant extension', () => {
const payload = {
"payload_on":true,
"payload_off":false,
"value_template":"{{ value_json.update_available}}",
"enabled_by_default": false,
"value_template":`{{ value_json['update']['state'] == "available" }}`,
"enabled_by_default": true,
"state_topic":"zigbee2mqtt/bulb",
"json_attributes_topic":"zigbee2mqtt/bulb",
"name":"bulb update available",
@@ -1191,11 +1193,13 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x000b57fffec6a5b2"
],
"name":"bulb",
'sw_version': version,
'sw_version': null,
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
"manufacturer":"IKEA"
},
'availability': [{topic: 'zigbee2mqtt/bridge/state'}],
'device_class': 'update',
'entity_category': 'diagnostic'
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -1230,7 +1234,7 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x0017880104e45520"
],
"name":"button",
"sw_version": version,
"sw_version": null,
"model":"Aqara wireless switch (WXKG11LM)",
"manufacturer":"Xiaomi"
}
@@ -1254,7 +1258,7 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x0017880104e45520"
],
"name":"button",
"sw_version": version,
"sw_version": null,
"model":"Aqara wireless switch (WXKG11LM)",
"manufacturer":"Xiaomi"
}
@@ -1418,7 +1422,7 @@ describe('HomeAssistant extension', () => {
"zigbee2mqtt_0x0017880104e45520"
],
"name":"button",
"sw_version": version,
"sw_version": null,
"model":"Aqara wireless switch (WXKG11LM)",
"manufacturer":"Xiaomi"
}
@@ -1609,7 +1613,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
},
@@ -1638,7 +1642,7 @@ describe('HomeAssistant extension', () => {
"device":{
"identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"],
"name":"ha_discovery_group",
"sw_version":version,
"sw_version": version,
},
"json_attributes_topic":"zigbee2mqtt/ha_discovery_group",
"max_mireds": 454,
@@ -1678,7 +1682,7 @@ describe('HomeAssistant extension', () => {
"manufacturer":"IKEA",
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
"name":"bulb",
"sw_version":version
"sw_version": null
},
"enabled_by_default":false,
"icon":"mdi:clock",
@@ -1686,7 +1690,9 @@ describe('HomeAssistant extension', () => {
"name":"bulb last seen",
"state_topic":"zigbee2mqtt/bulb",
"unique_id":"0x000b57fffec6a5b2_last_seen_zigbee2mqtt",
"value_template":"{{ value_json.last_seen }}"
"value_template":"{{ value_json.last_seen }}",
"device_class": "timestamp",
"entity_category": "diagnostic"
};
expect(MQTT.publish).toHaveBeenCalledWith(
@@ -1718,7 +1724,7 @@ describe('HomeAssistant extension', () => {
'device': {
'identifiers': ['zigbee2mqtt_0x0017880104e45522'],
'name': 'weather_sensor',
'sw_version': version,
'sw_version': null,
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
'manufacturer': 'Xiaomi',
'configuration_url': 'http://zigbee.mqtt/#/device/0x0017880104e45522/info'
+22
View File
@@ -265,6 +265,28 @@ describe('OTA update', () => {
);
});
it('Should respond with NO_IMAGE_AVAILABLE when update available request fails', async () => {
const device = zigbeeHerdsman.devices.bulb;
device.endpoints[0].commandResponse.mockClear();
const data = {imageType: 12382};
const mapped = 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};
logger.info.mockClear();
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, logger, {"imageType": 12382});
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x98});
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({"update_available":false,"update":{"state":"idle"}}),
{retain: true, qos: 0}, expect.any(Function)
);
});
it('Should check for update when device requests it and it is not available', async () => {
const device = zigbeeHerdsman.devices.bulb;
device.endpoints[0].commandResponse.mockClear();
+1 -20
View File
@@ -242,25 +242,6 @@ describe('Receive', () => {
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
});
it('WSDCGQ11LM pressure precision from non ZCL properties', async () => {
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 1);
MQTT.publish.mockClear();
let payload = {data: {"65281":{"1":2985,"4":5032,"5":9,"6":[0,1],"10":0,"100":2345,"101":4608,"102":91552}}, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"battery":91,"voltage":2985,"temperature":23.5,"humidity":46.08,"pressure":915.5});
MQTT.publish.mockClear();
payload = {data: {"16":9354,"20":-1,"measuredValue":915}, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"battery":91,"voltage":2985,"temperature":23.5,"humidity":46.08,"pressure":935.4});
});
it('Should handle a zigbee message with voltage 3010', async () => {
const device = zigbeeHerdsman.devices.WXKG02LM_rev1;
const data = {'65281': {'1': 3010}}
@@ -422,7 +403,7 @@ describe('Receive', () => {
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(logger.warn).toHaveBeenCalledWith(`Received message from unsupported device with Zigbee model 'notSupportedModelID' and manufacturer name 'notSupportedMfg'`);
expect(logger.warn).toHaveBeenCalledWith(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`);
expect(logger.warn).toHaveBeenCalledWith(`Please see: https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html`);
expect(MQTT.publish).toHaveBeenCalledTimes(0);
});
+1
View File
@@ -6,6 +6,7 @@ const mock = {
subscribe: jest.fn(),
reconnecting: false,
on: jest.fn(),
stream: {setMaxListeners: jest.fn()}
};
const mockConnect = jest.fn().mockReturnValue(mock);
-1
View File
@@ -215,7 +215,6 @@ const mock = {
events[type] = handler;
},
stop: jest.fn(),
setLED: jest.fn(),
getDevices: jest.fn().mockImplementation(() => {
return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr));
}),