From dedbf336af7d1762c2c191e785b783d815f6b6dc Mon Sep 17 00:00:00 2001 From: Jorge Schrauwen Date: Sat, 2 Jul 2022 20:33:04 +0200 Subject: [PATCH] Add filtered_cache option (#12988) * want a way to prevent attribute from being cached * Update filtered_cache description to be more clear * Fix emitStateChange was returning wrong payload in to parameter * Directly get filtered_cache from entity and forgo parameter to State.set() * Add tests to cover filtered_cache * Update state.ts * Update controller.test.js Co-authored-by: Koen Kanters --- lib/controller.ts | 2 +- lib/state.ts | 13 ++++++------ lib/types/types.d.ts | 6 ++++-- lib/util/settings.schema.json | 27 +++++++++++++++-------- test/controller.test.js | 40 +++++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 18 deletions(-) diff --git a/lib/controller.ts b/lib/controller.ts index 7d26829d1..31c946216 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -265,7 +265,7 @@ class Controller { extension.adjustMessageBeforePublish?.(entity, message); } - // filter mqtt message attributes + // Filter mqtt message attributes if (entity.options.filtered_attributes) { entity.options.filtered_attributes.forEach((a) => delete message[a]); } diff --git a/lib/state.ts b/lib/state.ts index 2b3e9e870..087aae014 100644 --- a/lib/state.ts +++ b/lib/state.ts @@ -74,17 +74,18 @@ class State { set(entity: Group | Device, update: KeyValue, reason: string=null): KeyValue { const fromState = this.state[entity.ID] || {}; const toState = objectAssignDeep({}, fromState, update); - const result = {...toState}; + const newCache = {...toState}; + const entityDontCacheProperties = entity.options.filtered_cache || []; - for (const property of Object.keys(toState)) { - if (dontCacheProperties.find((p) => property.match(p))) { - delete toState[property]; + for (const property of Object.keys(newCache)) { + if (dontCacheProperties.concat(entityDontCacheProperties).find((p) => property.match(p))) { + delete newCache[property]; } } - this.state[entity.ID] = toState; + this.state[entity.ID] = newCache; this.eventBus.emitStateChange({entity, from: fromState, to: toState, reason, update}); - return result; + return toState; } remove(ID: string | number): void { diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index efec9e46d..b264c62bd 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -280,11 +280,12 @@ declare global { retrieve_state?: boolean, debounce?: number, debounce_ignore?: string[], + filtered_attributes?: string[], + filtered_cache?: string[], filtered_optimistic?: string[], icon?: string, homeassistant?: KeyValue, legacy?: boolean, - filtered_attributes?: string[], friendly_name: string, description?: string, qos?: 0 | 1 | 2, @@ -295,10 +296,11 @@ declare global { ID?: number, optimistic?: boolean, off_state?: 'all_members_off' | 'last_member_state' + filtered_attributes?: string[], + filtered_cache?: string[], filtered_optimistic?: string[], retrieve_state?: boolean, homeassistant?: KeyValue, - filtered_attributes?: string[], friendly_name: string, description?: string, qos?: 0 | 1 | 2, diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index c13370722..4ffa20d5a 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -812,15 +812,6 @@ "description": "Publish optimistic state after set", "default": true }, - "filtered_optimistic": { - "type": "array", - "items": { - "type": "string" - }, - "examples": ["color_mode", "color_temp", "color"], - "title": "Filtered optimistic attributes", - "description": "Filter attributes from optimistic publish payload when calling /set. (This has no effect if optimistic is set to false)." - }, "filtered_attributes": { "type": "array", "items": { @@ -830,6 +821,24 @@ "title": "Filtered publish attributes", "description": "Filter attributes from publish payload." }, + "filtered_cache": { + "type": "array", + "items": { + "type": "string" + }, + "examples": ["action", "input_actions"], + "title": "Filtered attributes from cache", + "description": "Filter attributes from being added to the cache, this prevents the attribute from being in the published payload when the value didn't change." + }, + "filtered_optimistic": { + "type": "array", + "items": { + "type": "string" + }, + "examples": ["color_mode", "color_temp", "color"], + "title": "Filtered optimistic attributes", + "description": "Filter attributes from optimistic publish payload when calling /set. (This has no effect if optimistic is set to false)." + }, "icon": { "type": "string", "title": "Icon", diff --git a/test/controller.test.js b/test/controller.test.js index bf0719e6f..de3e65e4f 100644 --- a/test/controller.test.js +++ b/test/controller.test.js @@ -490,6 +490,46 @@ describe('Controller', () => { expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200}), {"qos": 0, "retain": true}, expect.any(Function)); }); + it('Publish entity state attribute_json output filtered cache', async () => { + await controller.start(); + settings.set(['advanced', 'output'], 'attribute_and_json'); + settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'filtered_cache'], ['linkquality']); + MQTT.publish.mockClear(); + + const device = controller.zigbee.resolveEntity('bulb'); + expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":50,"color_temp":370,"linkquality":99,"state":"ON"}); + + await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}); + await flushPromises(); + + expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":200,"color_temp":370,"state":"ON"}); + expect(MQTT.publish).toHaveBeenCalledTimes(5); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "87", {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200,"color_temp":370,"linkquality":87}), {"qos": 0, "retain": true}, expect.any(Function)); + }); + + it('Publish entity state attribute_json output filtered cache (device_options)', async () => { + await controller.start(); + settings.set(['advanced', 'output'], 'attribute_and_json'); + settings.set(['device_options', 'filtered_cache'], ['linkquality']); + MQTT.publish.mockClear(); + + const device = controller.zigbee.resolveEntity('bulb'); + expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":50,"color_temp":370,"linkquality":99,"state":"ON"}); + + await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}); + await flushPromises(); + + expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":200,"color_temp":370,"state":"ON"}); + expect(MQTT.publish).toHaveBeenCalledTimes(5); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "87", {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200,"color_temp":370,"linkquality":87}), {"qos": 0, "retain": true}, expect.any(Function)); + }); + it('Publish entity state with device information', async () => { await controller.start(); settings.set(['mqtt', 'include_device_information'], true);