Skip to content

Commit 8df5a48

Browse files
authored
feat: Availability improvements (#26811)
1 parent fc31e0a commit 8df5a48

File tree

11 files changed

+506
-99
lines changed

11 files changed

+506
-99
lines changed

lib/extension/availability.ts

Lines changed: 148 additions & 67 deletions
Large diffs are not rendered by default.

lib/extension/bridge.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -592,9 +592,8 @@ export default class Bridge extends Extension {
592592
): Promise<Zigbee2MQTTResponse<T extends 'device' ? 'bridge/response/device/remove' : 'bridge/response/group/remove'>> {
593593
const ID = typeof message === 'object' ? message.id : message.trim();
594594
const entity = this.getEntity(entityType, ID);
595+
// note: entity.name is dynamically retrieved, will change once device is removed (friendly => ieee)
595596
const friendlyName = entity.name;
596-
const entityID = entity.ID;
597-
598597
let block = false;
599598
let force = false;
600599
let blockForceLog = '';
@@ -609,8 +608,7 @@ export default class Bridge extends Extension {
609608
}
610609

611610
try {
612-
logger.info(`Removing ${entityType} '${entity.name}'${blockForceLog}`);
613-
const name = entity.name;
611+
logger.info(`Removing ${entityType} '${friendlyName}'${blockForceLog}`);
614612

615613
if (entity instanceof Device) {
616614
if (block) {
@@ -623,21 +621,21 @@ export default class Bridge extends Extension {
623621
await entity.zh.removeFromNetwork();
624622
}
625623

626-
this.eventBus.emitEntityRemoved({id: entityID, name, type: 'device'});
627-
settings.removeDevice(entityID as string);
624+
this.eventBus.emitEntityRemoved({id: entity.ID, name: friendlyName, type: 'device'});
625+
settings.removeDevice(entity.ID as string);
628626
} else {
629627
if (force) {
630628
entity.zh.removeFromDatabase();
631629
} else {
632630
await entity.zh.removeFromNetwork();
633631
}
634632

635-
this.eventBus.emitEntityRemoved({id: entityID, name, type: 'group'});
636-
settings.removeGroup(entityID);
633+
this.eventBus.emitEntityRemoved({id: entity.ID, name: friendlyName, type: 'group'});
634+
settings.removeGroup(entity.ID);
637635
}
638636

639637
// Remove from state
640-
this.state.remove(entityID);
638+
this.state.remove(entity.ID);
641639

642640
// Clear any retained messages
643641
await this.mqtt.publish(friendlyName, '', {retain: true});
@@ -694,7 +692,7 @@ export default class Bridge extends Extension {
694692
zigbee_herdsman_converters: this.zigbeeHerdsmanConvertersVersion,
695693
zigbee_herdsman: this.zigbeeHerdsmanVersion,
696694
coordinator: {
697-
ieee_address: this.zigbee.firstCoordinatorEndpoint().getDevice().ieeeAddr,
695+
ieee_address: this.zigbee.firstCoordinatorEndpoint().deviceIeeeAddress,
698696
...this.coordinatorVersion,
699697
},
700698
network: {
@@ -732,7 +730,7 @@ export default class Bridge extends Extension {
732730

733731
for (const bind of endpoint.binds) {
734732
const target = utils.isZHEndpoint(bind.target)
735-
? {type: 'endpoint', ieee_address: bind.target.getDevice().ieeeAddr, endpoint: bind.target.ID}
733+
? {type: 'endpoint', ieee_address: bind.target.deviceIeeeAddress, endpoint: bind.target.ID}
736734
: {type: 'group', id: bind.target.groupID};
737735
data.bindings.push({cluster: bind.cluster.name, target});
738736
}
@@ -780,7 +778,7 @@ export default class Bridge extends Extension {
780778
const members = [];
781779

782780
for (const member of group.zh.members) {
783-
members.push({ieee_address: member.getDevice().ieeeAddr, endpoint: member.ID});
781+
members.push({ieee_address: member.deviceIeeeAddress, endpoint: member.ID});
784782
}
785783

786784
groups.push({

lib/extension/publish.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,7 @@ export default class Publish extends Extension {
134134
const entityState = this.state.get(re);
135135
const membersState =
136136
re instanceof Group
137-
? Object.fromEntries(
138-
re.zh.members.map((e) => [e.getDevice().ieeeAddr, this.state.get(this.zigbee.resolveEntity(e.getDevice().ieeeAddr)!)]),
139-
)
137+
? Object.fromEntries(re.zh.members.map((e) => [e.deviceIeeeAddress, this.state.get(this.zigbee.resolveEntity(e.deviceIeeeAddress)!)]))
140138
: undefined;
141139
const converters = this.getDefinitionConverters(definition);
142140

lib/model/group.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ export default class Group {
2626
return !!device.zh.endpoints.find((e) => this.zh.members.includes(e));
2727
}
2828

29-
membersDevices(): Device[] {
30-
return this.zh.members.map((d) => this.resolveDevice(d.getDevice().ieeeAddr)!);
29+
*membersDevices(): Generator<Device> {
30+
for (const member of this.zh.members) {
31+
yield this.resolveDevice(member.deviceIeeeAddress)!;
32+
}
3133
}
3234

3335
membersDefinitions(): zhc.Definition[] {

lib/types/types.d.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ declare global {
4848

4949
namespace eventdata {
5050
type EntityRenamed = {entity: Device | Group; homeAssisantRename: boolean; from: string; to: string};
51-
type EntityRemoved = {id: number | string; name: string; type: 'device' | 'group'};
51+
type EntityRemoved = {id: string; name: string; type: 'device'} | {id: number; name: string; type: 'group'};
5252
type MQTTMessage = {topic: string; message: string};
5353
type MQTTMessagePublished = {topic: string; payload: string; options: {retain: boolean; qos: number}};
5454
type StateChange = {
@@ -98,7 +98,12 @@ declare global {
9898
};
9999
availability: {
100100
enabled: boolean;
101-
active: {timeout: number};
101+
active: {
102+
timeout: number;
103+
max_jitter: number;
104+
backoff: boolean;
105+
pause_on_backoff_gt: number;
106+
};
102107
passive: {timeout: number};
103108
};
104109
mqtt: {
@@ -200,7 +205,14 @@ declare global {
200205
interface DeviceOptions {
201206
disabled?: boolean;
202207
retention?: number;
203-
availability?: boolean | {timeout: number};
208+
availability?:
209+
| boolean
210+
| {
211+
timeout: number;
212+
max_jitter?: number;
213+
backoff?: boolean;
214+
pause_on_backoff_gt?: number;
215+
};
204216
optimistic?: boolean;
205217
debounce?: number;
206218
debounce_ignore?: string[];

lib/util/settings.schema.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@
6868
"requiresRestart": true,
6969
"default": 10,
7070
"description": "Time after which an active device will be marked as offline in minutes"
71+
},
72+
"max_jitter": {
73+
"type": "number",
74+
"title": "Max jitter",
75+
"default": 30000,
76+
"minimum": 1000,
77+
"description": "Maximum jitter (in msec) allowed on timeout to avoid availability pings trying to trigger around the same time"
78+
},
79+
"backoff": {
80+
"type": "boolean",
81+
"title": "Enabled",
82+
"description": "Enable timeout backoff on failed availability pings (x1.5, x3, x6, x12...)",
83+
"default": true
84+
},
85+
"pause_on_backoff_gt": {
86+
"type": "number",
87+
"title": "Pause on backoff greater than",
88+
"default": 0,
89+
"minimum": 0,
90+
"description": "Pause availability pings when backoff reaches over this limit until a new Zigbee message is received from the device. A value of zero disables pausing."
7191
}
7292
},
7393
"required": ["timeout"]

lib/util/settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const defaults: RecursivePartial<Settings> = {
3838
},
3939
availability: {
4040
enabled: false,
41-
active: {timeout: 10},
41+
active: {timeout: 10, max_jitter: 30000, backoff: true, pause_on_backoff_gt: 0},
4242
passive: {timeout: 1500},
4343
},
4444
frontend: {

lib/util/utils.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,18 +247,20 @@ function isAvailabilityEnabledForEntity(entity: Device | Group, settings: Settin
247247
}
248248

249249
if (entity.isGroup()) {
250-
return !entity.membersDevices().some((d) => !isAvailabilityEnabledForEntity(d, settings));
250+
for (const memberDevice of entity.membersDevices()) {
251+
if (!isAvailabilityEnabledForEntity(memberDevice, settings)) {
252+
return false;
253+
}
254+
}
255+
256+
return true;
251257
}
252258

253259
if (entity.options.availability != null) {
254260
return !!entity.options.availability;
255261
}
256262

257-
if (!settings.availability.enabled) {
258-
return false;
259-
}
260-
261-
return true;
263+
return settings.availability.enabled;
262264
}
263265

264266
function isZHEndpoint(obj: unknown): obj is zh.Endpoint {

0 commit comments

Comments
 (0)