Skip to content

Commit d401ca7

Browse files
committed
Implement fromZigbee converter processing for real-time state updates
processIncomingMessage was receiving ZCL attribute data from device messages but discarding everything except a debug log of link quality. Adopted devices never reflected real-time state changes, making the integration effectively write-only. Now implements the full fromZigbee pipeline: 1. Iterates fromZigbee converters from the device definition, matching by cluster name (same pattern as zigbee-herdsman-converters expects) 2. Each converter's convert() translates raw ZCL data into a state object (e.g. { temperature: 22.5, humidity: 65.3 }) 3. Adds linkquality to the state if present in the message 4. Fetches all channel properties for the adopted device 5. Matches state keys to property identifiers (set to zigbee expose names during adoption) and writes values via PropertyValueService 6. PropertyValueService handles persistence, change detection, and WebSocket broadcast The CI failure (devices-shelly-ng resetReconnectInterval) is a pre-existing issue on main, not related to this plugin. https://claude.ai/code/session_014bjB9Cn1WKASNLBeCuSbom
1 parent d0732fd commit d401ca7

1 file changed

Lines changed: 77 additions & 7 deletions

File tree

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

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
1+
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment */
22
import { Injectable } from '@nestjs/common';
33

44
import { ExtensionLoggerService, createExtensionLogger } from '../../../common/logger';
55
import { ConfigService } from '../../../modules/config/services/config.service';
66
import { ConnectionState } from '../../../modules/devices/devices.constants';
7+
import { ChannelPropertyEntity } from '../../../modules/devices/entities/devices.entity';
8+
import { ChannelsPropertiesService } from '../../../modules/devices/services/channels.properties.service';
79
import { DeviceConnectivityService } from '../../../modules/devices/services/device-connectivity.service';
810
import { DevicesService } from '../../../modules/devices/services/devices.service';
11+
import { PropertyValueService } from '../../../modules/devices/services/property-value.service';
912
import {
1013
ConfigChangeResult,
1114
IManagedPluginService,
@@ -40,6 +43,8 @@ export class ZigbeeHerdsmanService implements IManagedPluginService {
4043
private readonly configService: ConfigService,
4144
private readonly configValidator: ZigbeeHerdsmanConfigValidatorService,
4245
private readonly devicesService: DevicesService,
46+
private readonly channelsPropertiesService: ChannelsPropertiesService,
47+
private readonly propertyValueService: PropertyValueService,
4348
private readonly deviceConnectivityService: DeviceConnectivityService,
4449
private readonly zhConnectivityService: ZhDeviceConnectivityService,
4550
) {}
@@ -187,8 +192,8 @@ export class ZigbeeHerdsmanService implements IManagedPluginService {
187192

188193
private async processIncomingMessage(
189194
ieeeAddress: string,
190-
_cluster: string,
191-
_data: Record<string, unknown>,
195+
cluster: string,
196+
data: Record<string, unknown>,
192197
linkquality?: number,
193198
): Promise<void> {
194199
try {
@@ -200,16 +205,81 @@ export class ZigbeeHerdsmanService implements IManagedPluginService {
200205
return; // Device not adopted yet
201206
}
202207

203-
// Process fromZigbee converters via discovered device definition
204208
const discovered = this.adapterService.getDiscoveredDevice(ieeeAddress);
205209
if (!discovered?.definition) {
206210
return;
207211
}
208212

209-
// Link quality update is the primary use case for now
210-
// Full fromZigbee converter integration requires property value write service
213+
// Run fromZigbee converters to translate raw ZCL data into a state object
214+
// (e.g. { temperature: 22.5, humidity: 65.3 })
215+
let convertedState: Record<string, unknown> = {};
216+
217+
const herdsmanDevice = this.adapterService.getHerdsmanDevice(ieeeAddress);
218+
for (const converter of discovered.definition.fromZigbee as any[]) {
219+
try {
220+
if (!converter.cluster || (converter.cluster !== cluster && converter.cluster !== '*')) {
221+
continue;
222+
}
223+
if (typeof converter.convert !== 'function') {
224+
continue;
225+
}
226+
227+
const result = converter.convert(
228+
herdsmanDevice,
229+
data,
230+
{},
231+
{ state: convertedState, logger: this.logger, device: herdsmanDevice },
232+
{},
233+
);
234+
235+
if (result && typeof result === 'object') {
236+
convertedState = { ...convertedState, ...(result as Record<string, unknown>) };
237+
}
238+
} catch {
239+
// Individual converter failure is not critical
240+
}
241+
}
242+
243+
// Add linkquality to the state if present
211244
if (linkquality !== undefined) {
212-
this.logger.debug(`Device ${ieeeAddress} LQI: ${linkquality}`);
245+
convertedState['linkquality'] = linkquality;
246+
}
247+
248+
if (Object.keys(convertedState).length === 0) {
249+
return;
250+
}
251+
252+
// Write converted values to matching channel properties
253+
// Property identifiers are set to zigbee expose names during adoption
254+
const allProperties = await this.channelsPropertiesService.findAll<ChannelPropertyEntity>(
255+
undefined,
256+
DEVICES_ZIGBEE_HERDSMAN_TYPE,
257+
);
258+
259+
// Filter to properties belonging to this device's channels
260+
const deviceChannelIds = new Set(device.channels?.map((c) => c.id) ?? []);
261+
const deviceProperties = allProperties.filter((p) => {
262+
const channelId = typeof p.channel === 'string' ? p.channel : p.channel?.id;
263+
return channelId && deviceChannelIds.has(channelId);
264+
});
265+
266+
for (const property of deviceProperties) {
267+
const stateKey = property.identifier;
268+
if (!stateKey || !(stateKey in convertedState)) {
269+
continue;
270+
}
271+
272+
const value = convertedState[stateKey];
273+
if (value === undefined || value === null) {
274+
continue;
275+
}
276+
277+
try {
278+
const writeValue = typeof value === 'object' ? JSON.stringify(value) : (value as string | number | boolean);
279+
await this.propertyValueService.write(property, writeValue);
280+
} catch {
281+
// Non-critical — individual property write failures are logged by PropertyValueService
282+
}
213283
}
214284
} catch (error) {
215285
const err = error as Error;

0 commit comments

Comments
 (0)