Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Home Assistant template variable warnings #9088

Merged
merged 3 commits into from
Oct 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ class Controller {
this.exitCallback(1);
}

// Call extensions
await this.callExtensions('start', this.extensions);

// Send all cached states.
if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) {
for (const device of devices) {
Expand All @@ -154,9 +157,6 @@ class Controller {
}
}

// Call extensions
await this.callExtensions('start', this.extensions);

if (settings.get().advanced.last_seen && settings.get().advanced.last_seen !== 'disable') {
this.eventBus.onLastSeenChanged(this, (data) =>
this.publishEntityState(data.device, {}, 'lastSeenChanged'));
Expand Down
65 changes: 38 additions & 27 deletions lib/extension/homeassistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import Extension from './extension';
import bind from 'bind-decorator';

// eslint-disable-next-line camelcase
interface DiscoveryEntry {type: string, object_id: string, discovery_payload: KeyValue}
interface DiscoveryEntry {mockProperties: string[], type: string, object_id: string, discovery_payload: KeyValue}

const sensorClick = {
type: 'sensor',
object_id: 'click',
mockProperties: ['click'],
discovery_payload: {
icon: 'mdi:toggle-switch',
value_template: '{{ value_json.click }}',
Expand All @@ -36,7 +37,7 @@ const featurePropertyWithoutEndpoint = (feature: zhc.DefinitionExposeFeature): s
* This extensions handles integration with HomeAssistant
*/
export default class HomeAssistant extends Extension {
private discovered: {[s: string]: string[]} = {};
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;
Expand Down Expand Up @@ -91,7 +92,7 @@ export default class HomeAssistant extends Extension {
assert(entityType === 'device' || groupSupportedTypes.includes(firstExpose.type),
`Unsupported expose type ${firstExpose.type} for group`);

const discoveryEntries = [];
const discoveryEntries: DiscoveryEntry[] = [];
const endpoint = entityType === 'device' ? exposes[0].endpoint : undefined;
const getProperty = (feature: zhc.DefinitionExposeFeature): string => entityType === 'group' ?
featurePropertyWithoutEndpoint(feature) : feature.property;
Expand All @@ -102,10 +103,12 @@ export default class HomeAssistant extends Extension {
const hasColorHS = exposes.find((expose) => expose.features.find((e) => e.name === 'color_hs'));
const hasBrightness = exposes.find((expose) => expose.features.find((e) => e.name === 'brightness'));
const hasColorTemp = exposes.find((expose) => expose.features.find((e) => e.name === 'color_temp'));
const state = firstExpose.features.find((f) => f.name === 'state');

const discoveryEntry: DiscoveryEntry = {
type: 'light',
object_id: endpoint ? `light_${endpoint}` : 'light',
mockProperties: [state.property],
discovery_payload: {
brightness: !!hasBrightness,
schema: 'json',
Expand Down Expand Up @@ -149,6 +152,7 @@ export default class HomeAssistant extends Extension {
const discoveryEntry: DiscoveryEntry = {
type: 'switch',
object_id: endpoint ? `switch_${endpoint}` : 'switch',
mockProperties: [property],
discovery_payload: {
payload_off: state.value_off,
payload_on: state.value_on,
Expand Down Expand Up @@ -181,6 +185,7 @@ export default class HomeAssistant extends Extension {
const discoveryEntry: DiscoveryEntry = {
type: 'climate',
object_id: endpoint ? `climate_${endpoint}` : 'climate',
mockProperties: [],
discovery_payload: {
// Static
state_topic: false,
Expand Down Expand Up @@ -268,6 +273,7 @@ export default class HomeAssistant extends Extension {
const discoveryEntry: DiscoveryEntry = {
type: 'lock',
object_id: 'lock',
mockProperties: [state.property],
discovery_payload: {
command_topic: true,
value_template: `{{ value_json.${state.property} }}`,
Expand Down Expand Up @@ -304,6 +310,7 @@ export default class HomeAssistant extends Extension {

const discoveryEntry: DiscoveryEntry = {
type: 'cover',
mockProperties: [],
object_id: endpoint ? `cover_${endpoint}` : 'cover',
discovery_payload: {},
};
Expand Down Expand Up @@ -347,6 +354,7 @@ export default class HomeAssistant extends Extension {
const discoveryEntry: DiscoveryEntry = {
type: 'fan',
object_id: 'fan',
mockProperties: ['fan_state'],
discovery_payload: {
state_topic: true,
state_value_template: '{{ value_json.fan_state }}',
Expand Down Expand Up @@ -427,6 +435,7 @@ export default class HomeAssistant extends Extension {
if (firstExpose.access & ACCESS_SET) {
const discoveryEntry: DiscoveryEntry = {
type: 'switch',
mockProperties: [firstExpose.property],
object_id: endpoint ?
`switch_${firstExpose.name}_${endpoint}` :
`switch_${firstExpose.name}`,
Expand All @@ -447,6 +456,7 @@ export default class HomeAssistant extends Extension {
const discoveryEntry = {
type: 'binary_sensor',
object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`,
mockProperties: [firstExpose.property],
discovery_payload: {
value_template: `{{ value_json.${firstExpose.property} }}`,
payload_on: firstExpose.value_on,
Expand Down Expand Up @@ -506,6 +516,7 @@ export default class HomeAssistant extends Extension {
const discoveryEntry = {
type: 'sensor',
object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`,
mockProperties: [firstExpose.property],
discovery_payload: {
value_template: `{{ value_json.${firstExpose.property} }}`,
enabled_by_default: !allowsSet,
Expand All @@ -524,6 +535,7 @@ export default class HomeAssistant extends Extension {
const discoveryEntry = {
type: 'number',
object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`,
mockProperties: [firstExpose.property],
discovery_payload: {
value_template: `{{ value_json.${firstExpose.property} }}`,
command_topic: true,
Expand Down Expand Up @@ -561,6 +573,7 @@ export default class HomeAssistant extends Extension {
discoveryEntries.push({
type: 'sensor',
object_id: firstExpose.property,
mockProperties: [firstExpose.property],
discovery_payload: {
value_template: `{{ value_json.${firstExpose.property} }}`,
enabled_by_default: !(firstExpose.access & ACCESS_SET),
Expand All @@ -577,6 +590,7 @@ export default class HomeAssistant extends Extension {
discoveryEntries.push({
type: 'select',
object_id: firstExpose.property,
mockProperties: [firstExpose.property],
discovery_payload: {
value_template: `{{ value_json.${firstExpose.property} }}`,
state_topic: true,
Expand All @@ -598,6 +612,7 @@ export default class HomeAssistant extends Extension {
const discoveryEntry = {
type: 'sensor',
object_id: firstExpose.property,
mockProperties: [firstExpose.property],
discovery_payload: {
value_template: `{{ value_json.${firstExpose.property} }}`,
...lookup[firstExpose.name],
Expand Down Expand Up @@ -629,6 +644,7 @@ export default class HomeAssistant extends Extension {
// deprecated
this.mapping[def.model].push({
type: 'sensor',
mockProperties: ['brightness'],
object_id: 'brightness',
discovery_payload: {
unit_of_measurement: 'brightness',
Expand All @@ -653,7 +669,7 @@ export default class HomeAssistant extends Extension {

@bind onDeviceRemoved(data: eventdata.DeviceRemoved): void {
logger.debug(`Clearing Home Assistant discovery topic for '${data.name}'`);
this.discovered[data.ieeeAddr]?.forEach((topic) => {
this.discovered[data.ieeeAddr]?.topics.forEach((topic) => {
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic, false, false);
});

Expand Down Expand Up @@ -781,6 +797,7 @@ export default class HomeAssistant extends Extension {
configs.push({
type: 'sensor',
object_id: 'last_seen',
mockProperties: ['last_seen'],
discovery_payload: {
icon: 'mdi:clock',
value_template: '{{ value_json.last_seen }}',
Expand All @@ -790,9 +807,10 @@ export default class HomeAssistant extends Extension {
}

if (isDevice && entity.definition.hasOwnProperty('ota')) {
const updateStateSensor = {
const updateStateSensor: DiscoveryEntry = {
type: 'sensor',
object_id: 'update_state',
mockProperties: [],
discovery_payload: {
icon: 'mdi:update',
value_template: `{{ value_json['update']['state'] }}`,
Expand All @@ -805,6 +823,7 @@ export default class HomeAssistant extends Extension {
const updateAvailableSensor = {
type: 'binary_sensor',
object_id: 'update_available',
mockProperties: ['update_available'],
discovery_payload: {
payload_on: true,
payload_off: false,
Expand Down Expand Up @@ -842,9 +861,13 @@ export default class HomeAssistant extends Extension {
return configs;
}

private getDiscoverKey(entity: Device | Group): string | number {
return entity.isDevice() ? entity.ieeeAddr : entity.ID;
}

private discover(entity: Device | Group, force=false): void {
// Check if already discoverd and check if there are configs.
const discoverKey = entity.isDevice() ? entity.ieeeAddr : entity.ID;
const discoverKey = this.getDiscoverKey(entity);
const discover = force || !this.discovered[discoverKey];

if (entity.isGroup()) {
Expand All @@ -855,7 +878,7 @@ export default class HomeAssistant extends Extension {
return;
}

this.discovered[discoverKey] = [];
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}`;
Expand Down Expand Up @@ -1044,7 +1067,8 @@ export default class HomeAssistant extends Extension {

const topic = this.getDiscoveryTopic(config, entity);
this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic, false, false);
this.discovered[discoverKey].push(topic);
this.discovered[discoverKey].topics.add(topic);
config.mockProperties?.forEach((property) => this.discovered[discoverKey].mockProperties.add(property));
});
}

Expand Down Expand Up @@ -1139,26 +1163,12 @@ export default class HomeAssistant extends Extension {
}

override adjustMessageBeforePublish(entity: Device | Group, message: KeyValue): void {
// Set missing values of state to 'null': https://github.com/Koenkk/zigbee2mqtt/issues/6987
if (!entity.isDevice() || !entity.definition) return null;

const add = (expose: zhc.DefinitionExpose | zhc.DefinitionExposeFeature): void => {
if (!message.hasOwnProperty(expose.property) && expose.access & ACCESS_STATE) {
message[expose.property] = null;
}
};

for (const expose of entity.definition.exposes) {
if (expose.hasOwnProperty('features')) {
for (const feature of expose.features) {
if (feature.name === 'state') {
add(feature);
}
}
} else {
add(expose);
const discoverKey = this.getDiscoverKey(entity);
this.discovered[discoverKey]?.mockProperties?.forEach((property) => {
if (!message.hasOwnProperty(property)) {
message[property] = null;
}
}
});

// Copy hue -> h, saturation -> s to make homeassitant happy
if (message.hasOwnProperty('color')) {
Expand Down Expand Up @@ -1200,6 +1210,7 @@ export default class HomeAssistant extends Extension {
const config: DiscoveryEntry = {
type: 'device_automation',
object_id: `${key}_${value}`,
mockProperties: [],
discovery_payload: {
automation_type: 'trigger',
type: key,
Expand Down
4 changes: 2 additions & 2 deletions test/frontend.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ describe('Frontend', () => {
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({state: 'ON', linkquality: null}),
stringify({state: 'ON', linkquality: null, update_available: null}),
{ retain: false, qos: 0 },
expect.any(Function)
);
Expand All @@ -151,7 +151,7 @@ describe('Frontend', () => {

// Received message on socket
expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(1);
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', linkquality: null}}));
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', linkquality: null, update_available: null}}));

// Shouldnt set when not ready
mockWSClient.implementation.send.mockClear();
Expand Down
20 changes: 10 additions & 10 deletions test/homeassistant.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,7 @@ describe('HomeAssistant extension', () => {
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({"color":{"hue": 0, "saturation": 100, "h": 0, "s": 100}, "color_mode": "hs", "linkquality": null, "state": null}),
stringify({"color":{"hue": 0, "saturation": 100, "h": 0, "s": 100}, "color_mode": "hs", "linkquality": null, "state": null, "update_available": null}),
{ retain: false, qos: 0 },
expect.any(Function),
);
Expand All @@ -841,7 +841,7 @@ describe('HomeAssistant extension', () => {
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({"color": {"x": 0.4576,"y": 0.41}, "color_mode": "xy", "linkquality": null,"state": null}),
stringify({"color": {"x": 0.4576,"y": 0.41}, "color_mode": "xy", "linkquality": null,"state": null, "update_available": null}),
{ retain: false, qos: 0 },
expect.any(Function),
);
Expand All @@ -857,7 +857,7 @@ describe('HomeAssistant extension', () => {
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({"linkquality": null,"state": "ON"}),
stringify({"linkquality": null,"state": "ON", "update_available": null}),
{ retain: false, qos: 0 },
expect.any(Function),
);
Expand Down Expand Up @@ -932,13 +932,13 @@ describe('HomeAssistant extension', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null}),
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null}),
{ retain: true, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/remote',
stringify({"action":null,"battery":null,"brightness":255,"linkquality":null}),
stringify({"action":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null}),
{ retain: true, qos: 0 },
expect.any(Function)
);
Expand All @@ -956,13 +956,13 @@ describe('HomeAssistant extension', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null}),
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null}),
{ retain: true, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/remote',
stringify({"action":null,"battery":null,"brightness":255,"linkquality":null}),
stringify({"action":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null}),
{ retain: true, qos: 0 },
expect.any(Function)
);
Expand Down Expand Up @@ -1290,7 +1290,7 @@ describe('HomeAssistant extension', () => {

expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/button',
stringify({action: "", battery: null, linkquality: null, voltage: null}),
stringify({action: "", battery: null, linkquality: null, voltage: null, click: null}),
{ retain: false, qos: 0 },
expect.any(Function),
);
Expand Down Expand Up @@ -1477,10 +1477,10 @@ describe('HomeAssistant extension', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(4);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({action: 'single', battery: null, linkquality: null, voltage: null});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({action: 'single', click: null, battery: null, linkquality: null, voltage: null});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/button');
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({action: '', battery: null, linkquality: null, voltage: null});
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({action: '', click: null, battery: null, linkquality: null, voltage: null});
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('homeassistant/device_automation/0x0017880104e45520/action_single/config');
expect(MQTT.publish.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/action');
Expand Down