Skip to content

Commit ca6990a

Browse files
committed
Fix shelly-ng type error, fromZigbee converter args, and platform TOCTOU
1. Fix shelly-ng CI blocker: resetReconnectInterval() doesn't exist on the RpcHandler type. Merged the method into the existing cast type and call via optional chaining. This was a pre-existing type error on main that blocked generate:openapi. 2. Fix fromZigbee converter call signature: convert() expects (model, msg, publish, options, meta) but was called with (herdsmanDevice, data, {}, meta, {}). Now passes: - model: discovered.definition (the device definition) - msg: full message object with data, cluster, type, device, endpoint - publish: no-op function (required callback, unused in our context) - options: empty object - meta: { state, logger, device, options } 3. Await converter.convert() result: fromZigbee converters may be async. Without await, the Promise object was spread into convertedState instead of the actual converted values, silently losing all data. 4. Add TOCTOU guard in platform processBatch: re-check isStarted() per device in the loop to detect asynchronous adapter disconnections between the initial check and actual command execution. https://claude.ai/code/session_014bjB9Cn1WKASNLBeCuSbom
1 parent d401ca7 commit ca6990a

3 files changed

Lines changed: 35 additions & 10 deletions

File tree

apps/backend/src/plugins/devices-shelly-ng/delegates/shelly-device.delegate.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,14 @@ export class ShellyDeviceDelegate extends EventEmitter2 {
244244

245245
// Access the protected socket to force-terminate it.
246246
// TypeScript access modifiers are not enforced at runtime.
247-
const rpcHandler = this.shelly.rpcHandler as unknown as { socket?: { terminate?: () => void } };
247+
const rpcHandler = this.shelly.rpcHandler as unknown as {
248+
socket?: { terminate?: () => void };
249+
resetReconnectInterval?: () => void;
250+
};
248251

249252
// Reset reconnect backoff so the library uses the shortest interval
250253
// (first entry in reconnectInterval) instead of an escalated delay.
251-
this.shelly.rpcHandler.resetReconnectInterval();
254+
rpcHandler.resetReconnectInterval?.();
252255

253256
if (rpcHandler.socket?.terminate) {
254257
rpcHandler.socket.terminate();

apps/backend/src/plugins/devices-zigbee-herdsman/platforms/zigbee-herdsman.device.platform.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ export class ZigbeeHerdsmanDevicePlatform implements IDevicePlatform {
8484
let success = true;
8585

8686
for (const [ieeeAddress, { payload }] of deviceUpdates) {
87+
// Re-check adapter state per device — it may have disconnected asynchronously
88+
if (!this.adapterService.isStarted()) {
89+
this.logger.warn('Adapter disconnected during batch processing');
90+
return false;
91+
}
92+
8793
const discovered = this.adapterService.getDiscoveredDevice(ieeeAddress);
8894
if (!discovered?.definition) {
8995
this.logger.warn(`No device definition for ${ieeeAddress}, skipping command`);

apps/backend/src/plugins/devices-zigbee-herdsman/services/zigbee-herdsman.service.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,20 @@ export class ZigbeeHerdsmanService implements IManagedPluginService {
215215
let convertedState: Record<string, unknown> = {};
216216

217217
const herdsmanDevice = this.adapterService.getHerdsmanDevice(ieeeAddress);
218+
219+
// Build the message object that fromZigbee converters expect:
220+
// convert(model, msg, publish, options, meta)
221+
const msg = {
222+
data,
223+
cluster,
224+
type: 'attributeReport',
225+
device: herdsmanDevice,
226+
endpoint: herdsmanDevice?.getEndpoint?.(1) ?? null,
227+
linkquality: linkquality ?? 0,
228+
};
229+
230+
const publish = () => {};
231+
218232
for (const converter of discovered.definition.fromZigbee as any[]) {
219233
try {
220234
if (!converter.cluster || (converter.cluster !== cluster && converter.cluster !== '*')) {
@@ -224,15 +238,17 @@ export class ZigbeeHerdsmanService implements IManagedPluginService {
224238
continue;
225239
}
226240

227-
const result = converter.convert(
228-
herdsmanDevice,
229-
data,
230-
{},
231-
{ state: convertedState, logger: this.logger, device: herdsmanDevice },
232-
{},
233-
);
241+
const meta = {
242+
state: convertedState,
243+
logger: this.logger,
244+
device: herdsmanDevice,
245+
options: {},
246+
};
247+
248+
// fromZigbee converters may be sync or async
249+
const result = await converter.convert(discovered.definition, msg, publish, {}, meta);
234250

235-
if (result && typeof result === 'object') {
251+
if (result && typeof result === 'object' && !(result instanceof Promise)) {
236252
convertedState = { ...convertedState, ...(result as Record<string, unknown>) };
237253
}
238254
} catch {

0 commit comments

Comments
 (0)