Skip to content

Commit b75a69b

Browse files
committed
Added topicGetAvailable for LWT
1 parent 2813ca1 commit b75a69b

24 files changed

Lines changed: 185 additions & 45 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
All notable changes to homebridge-dummy will be documented in this file.
44

5+
## 1.5.16-beta.0 ()
6+
7+
### Added
8+
- Added `topicGetAvailable` for "Last Will and Testament" (LWT) before an accessory goes offline
9+
- If value does not match `valueAvailable`, then accessory will show "No Response" in HomeKit until the next successful get event
10+
11+
### Changed
12+
- Renamed "Get Availability" to "Get Active Status" in config UI to avoid confusion with the new `topicGetAvailable` (see above)
13+
514
## 1.5.15 (2026-05-12)
615

716
### Fixed

config.schema.template.json

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@
240240
}
241241
]
242242
},
243+
"topicGetAvailable": {
244+
"type": "string",
245+
"title": "${config.title.topicGetAvailable}"
246+
},
243247
"topicGetBatteryLevel": {
244248
"type": "string",
245249
"title": "${config.title.topicGetBatteryLevel}"
@@ -688,6 +692,10 @@
688692
"type": "string",
689693
"title": "${config.title.topicGetVoltage}"
690694
},
695+
"valueAvailable": {
696+
"type": "string",
697+
"title": "${config.title.valueAvailable}"
698+
},
691699
"valueBatteryLow": {
692700
"type": "string",
693701
"title": "${config.title.valueBatteryLow}"
@@ -1460,6 +1468,19 @@
14601468
"required": ["valueSwingEnabled", "valueSwingDisabled"]
14611469
}
14621470
},
1471+
{
1472+
"if": {
1473+
"properties": {
1474+
"topicGetAvailable": {
1475+
"minLength": 1
1476+
}
1477+
},
1478+
"required": ["topicGetAvailable"]
1479+
},
1480+
"then": {
1481+
"required": ["valueAvailable"]
1482+
}
1483+
},
14631484
{
14641485
"if": {
14651486
"properties": {
@@ -3303,11 +3324,11 @@
33033324
"flex-direction": "row",
33043325
"items": [
33053326
{
3306-
"key": "accessories[].topicGetBatteryLevel",
3327+
"key": "accessories[].topicGetAvailable",
33073328
"flex": "0 0 50%"
33083329
},
33093330
{
3310-
"key": "accessories[].topicGetBatteryLow",
3331+
"key": "accessories[].topicGetStatusActive",
33113332
"flex": "0 0 50%"
33123333
}
33133334
]
@@ -3318,7 +3339,11 @@
33183339
"flex-direction": "row",
33193340
"items": [
33203341
{
3321-
"key": "accessories[].topicGetStatusActive",
3342+
"key": "accessories[].topicGetBatteryLevel",
3343+
"flex": "0 0 50%"
3344+
},
3345+
{
3346+
"key": "accessories[].topicGetBatteryLow",
33223347
"flex": "0 0 50%"
33233348
}
33243349
]
@@ -4044,12 +4069,16 @@
40444069
"flex-direction": "row",
40454070
"items": [
40464071
{
4047-
"key": "accessories[].valueBatteryLow",
4072+
"key": "accessories[].valueAvailable",
40484073
"flex": "0 0 25%"
40494074
},
40504075
{
40514076
"key": "accessories[].valueStatusActive",
40524077
"flex": "0 0 25%"
4078+
},
4079+
{
4080+
"key": "accessories[].valueBatteryLow",
4081+
"flex": "0 0 25%"
40534082
}
40544083
]
40554084
}

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"displayName": "Homebridge Easy MQTT",
55
"description": "Homebridge plugin for MQTT devices",
66
"type": "module",
7-
"version": "1.5.15",
7+
"version": "1.5.16-beta.0",
88
"homepage": "https://github.com/mpatfield/homebridge-easy-mqtt#readme",
99
"repository": {
1010
"type": "git",

src/accessory/abstract/base.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { strings } from '../../i18n/i18n.js';
1111
import { CharacteristicKey, HKCharacteristicKey } from '../../model/enums.js';
1212
import { BaseAccessoryConfig, MQTTAccessoryDependency } from '../../model/types.js';
1313

14-
import { LogType } from '../../tools/log.js';
1514
import getVersion from '../../tools/version.js';
1615

1716
export abstract class BaseAccessory<C extends BaseAccessoryConfig = BaseAccessoryConfig> extends MQTTAccessory<C> {
@@ -33,7 +32,7 @@ export abstract class BaseAccessory<C extends BaseAccessoryConfig = BaseAccessor
3332
this.setup(HKCharacteristicKey.StatusActive, true,
3433
'topicGetStatusActive',
3534
this.bindOnUpdateBooleanSingle(HKCharacteristicKey.StatusActive, 'valueStatusActive',
36-
strings.accessory.statusActive, strings.accessory.statusInactive, LogType.ALWAYS, LogType.WARNING),
35+
strings.accessory.statusActive, strings.accessory.statusInactive),
3736
false);
3837

3938
this.addTopicHandlers(Battery.topicHandlers(dependency.Service, this, dependency.config));

src/accessory/abstract/common.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { EveCharacteristic, isEveCharacteristic } from '../characteristic/eve.js
55
import { strings } from '../../i18n/i18n.js';
66

77
import { CharacteristicKey, TimeUnits } from '../../model/enums.js';
8-
import { CharacteristicType, TemperatureConfig, TimeoutConfig } from '../../model/types.js';
8+
import { CharacteristicType, HapStatusErrorType, TemperatureConfig, TimeoutConfig } from '../../model/types.js';
99

1010
import { debounce } from '../../tools/debounce.js';
1111
import { Log, LogType } from '../../tools/log.js';
@@ -23,6 +23,9 @@ export type PublishHandler = (topic: string, value: PrimitiveTypes) => void;
2323
type NumberCallback = (value: number) => void;
2424
type BooleanCallback = (value: boolean) => void
2525

26+
const AVAILABILITY_KEY = 'Available';
27+
const HAP_COMMUNICATION_FAILURE = -70402;
28+
2629
export abstract class Common<C extends Assertable> {
2730

2831
public readonly topicHandlers: TopicHandler[] = [];
@@ -33,6 +36,7 @@ export abstract class Common<C extends Assertable> {
3336

3437
protected abstract get service(): Service;
3538
protected abstract get Characteristic(): CharacteristicType;
39+
protected abstract get HapStatusError(): HapStatusErrorType;
3640

3741
protected abstract get log(): Log;
3842
protected abstract get disableLogging(): boolean;
@@ -93,6 +97,23 @@ export abstract class Common<C extends Assertable> {
9397
Properties.set(this.identifier, key, value);
9498
}
9599

100+
protected get isAvailable(): boolean {
101+
return Properties.get(this.identifier, AVAILABILITY_KEY) !== false;
102+
}
103+
104+
protected set isAvailable(value: boolean) {
105+
106+
if (!Properties.set(this.identifier, AVAILABILITY_KEY, value)) {
107+
return;
108+
}
109+
110+
if (value) {
111+
this.logIfDesired(LogType.ALWAYS, strings.accessory.available);
112+
} else {
113+
this.logIfDesired(LogType.WARNING, strings.accessory.unavailable);
114+
}
115+
}
116+
96117
protected setup(
97118
characteristicKey: CharacteristicKey, defaultValue: CharacteristicValue,
98119
getTopicKey: keyof C, onUpdateHandler: OnUpdateHandler, assertGetTopic: boolean,
@@ -143,10 +164,18 @@ export abstract class Common<C extends Assertable> {
143164
this.setProperty(characteristicKey, startingValue);
144165

145166
characteristic.onGet( async (): Promise<Nullable<CharacteristicValue>> => {
167+
if (!this.isAvailable) {
168+
throw new this.HapStatusError(HAP_COMMUNICATION_FAILURE);
169+
}
146170
return this.getProperty(characteristicKey) ?? null;
147171
});
148172

149-
this.topicHandlers.push({ topic: this.config[getTopicKey] as string, handler: onUpdateHandler });
173+
const onUpdateHandlerWrapper: OnUpdateHandler = async (topic, value) => {
174+
this.isAvailable = true;
175+
await onUpdateHandler(topic, value);
176+
};
177+
178+
this.topicHandlers.push({ topic: this.config[getTopicKey] as string, handler: onUpdateHandlerWrapper });
150179

151180
return characteristic;
152181
}

src/accessory/abstract/mqtt.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { CharacteristicKeys } from '../characteristic/characteristic.js';
77
import { CustomCharacteristic } from '../characteristic/custom.js';
88

99
import { AccessoryType, HKCharacteristicKey } from '../../model/enums.js';
10+
import { HistoryEntry, HistoryType } from '../../model/history.js';
1011
import { MQTT } from '../../model/mqtt.js';
11-
import { CharacteristicType, MQTTAccessoryConfig, MQTTAccessoryDependency } from '../../model/types.js';
12+
import { CharacteristicType, HapStatusErrorType, MQTTAccessoryConfig, MQTTAccessoryDependency } from '../../model/types.js';
1213

13-
import { HistoryEntry, HistoryType } from '../../model/history.js';
1414
import { Log } from '../../tools/log.js';
1515

1616
export abstract class MQTTAccessory<C extends MQTTAccessoryConfig> extends Common<C> {
@@ -56,12 +56,17 @@ export abstract class MQTTAccessory<C extends MQTTAccessoryConfig> extends Commo
5656
}
5757

5858
this.setupCustomCharacteristics();
59+
this.setupAvailabilityHandler();
5960
}
6061

6162
public get Characteristic(): CharacteristicType {
6263
return this.dependency.Characteristic;
6364
}
6465

66+
public get HapStatusError(): HapStatusErrorType {
67+
return this.dependency.HapStatusError;
68+
}
69+
6570
public get platformAccessory(): PlatformAccessory {
6671
return this.dependency.platformAccessory;
6772
}
@@ -116,6 +121,17 @@ export abstract class MQTTAccessory<C extends MQTTAccessoryConfig> extends Commo
116121
}
117122
}
118123

124+
private setupAvailabilityHandler() {
125+
126+
if (this.config.topicGetAvailable === undefined) {
127+
return;
128+
}
129+
130+
this.addTopicHandlers([{ topic: this.config.topicGetAvailable, handler: (async (_topic: string, value: PrimitiveTypes) => {
131+
this.isAvailable = value === this.getPrimitiveValue('valueAvailable', true);
132+
}).bind(this) }]);
133+
}
134+
119135
public publish(topic: string, value: PrimitiveTypes) {
120136
this.mqttClient?.publish(this.identifier, topic, value);
121137
}

src/accessory/addons/addon.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Common } from '../abstract/common.js';
44
import { MQTTAccessory } from '../abstract/mqtt.js';
55

66
import { AddonType } from '../../model/enums.js';
7-
import { AddonConfig, CharacteristicType, MQTTAccessoryConfig } from '../../model/types.js';
7+
import { AddonConfig, CharacteristicType, HapStatusErrorType, MQTTAccessoryConfig } from '../../model/types.js';
88

99
import { Log } from '../../tools/log.js';
1010

@@ -61,6 +61,10 @@ export abstract class Addon<C extends AddonConfig> extends Common<C> {
6161
return this.parentAccessory.Characteristic;
6262
}
6363

64+
public get HapStatusError(): HapStatusErrorType {
65+
return this.parentAccessory.HapStatusError;
66+
}
67+
6468
protected get log(): Log {
6569
return this.parentAccessory.log;
6670
}

src/homebridge/platform.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export class HomebridgeEasyMQTT implements DynamicPlatformPlugin {
7979

8080
const Service = this.api.hap.Service;
8181
const Characteristic = this.api.hap.Characteristic;
82+
const HapStatusError = this.api.hap.HapStatusError;
8283

8384
const history = new History(this.api, this.log);
8485

@@ -103,7 +104,7 @@ export class HomebridgeEasyMQTT implements DynamicPlatformPlugin {
103104
const uuid = this.api.hap.uuid.generate(id);
104105

105106
const platformAccessory = this.createPlatformAccessory(accessoryConfig.info.name, uuid);
106-
const dependency: AccessoryDependency = { Service, Characteristic, platformAccessory, log: this.log, history: history };
107+
const dependency: AccessoryDependency = { Service, Characteristic, HapStatusError, platformAccessory, log: this.log, history: history };
107108

108109
const accessory = createAccessory(dependency, accessoryConfig);
109110

@@ -119,7 +120,7 @@ export class HomebridgeEasyMQTT implements DynamicPlatformPlugin {
119120

120121
const uuid = this.api.hap.uuid.generate(groupName);
121122
const platformAccessory = this.createPlatformAccessory(groupName, uuid);
122-
const dependency: AccessoryDependency = { Service, Characteristic, platformAccessory, log: this.log, history: history };
123+
const dependency: AccessoryDependency = { Service, Characteristic, HapStatusError, platformAccessory, log: this.log, history: history };
123124

124125
const configs = groups.get(groupName)!;
125126

src/i18n/da.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ const da = {
66
batteryLow: '%s batteri er lavt',
77
batteryNotLow: '%s batteri er okay',
88
missingRequired: '%s mangler påkrævet konfigurationsvariabel %s',
9-
statusActive: '%s er nu tilgængelig',
10-
statusInactive: '%s er utilgængelig',
119
},
1210

1311
active: {
@@ -190,7 +188,6 @@ const da = {
190188
topicGetRotationSpeed: 'Hent Rotationshastighed',
191189
topicGetSaturation: 'Hent Mætning',
192190
topicGetSmokeDetected: 'Hent Røg Registreret*',
193-
topicGetStatusActive: 'Hent Tilgængelighed',
194191
topicGetStatusFault: 'Hent Fejl',
195192
topicGetStatusTampered: 'Hent Manipuleret',
196193
topicGetSulphurDioxideDensity: 'Hent Svovldioxid',
@@ -267,6 +264,7 @@ const da = {
267264
valueArmAway: 'Tilkobl Ude',
268265
valueArmNight: 'Tilkobl Nat',
269266
valueArmStay: 'Tilkobl Hjemme',
267+
valueAvailable: 'Tilgængelig/Opnåelig',
270268
valueBatteryLow: 'Lavt Batteri',
271269
valueCarbonDioxideDetected: 'CO2 Registreret*',
272270
valueCarbonMonoxideDetected: 'CO Registreret*',
@@ -323,7 +321,6 @@ const da = {
323321
valueSmokeDetected: 'Røg Registreret*',
324322
valueStateActive: 'Tilstand Aktiv*',
325323
valueStateInactive: 'Tilstand Inaktiv*',
326-
valueStatusActive: 'Tilgængelig/Opnåelig',
327324
valueSwingDisabled: 'Svingning Deaktiveret',
328325
valueSwingEnabled: 'Svingning Aktiveret',
329326
valueTampered: 'Manipuleret',

0 commit comments

Comments
 (0)