Skip to content

Commit d402e1f

Browse files
committed
Add adoption online check, DTO validation, remove dead schema, fix lastSeen null
1. Add isCoordinatorOnline check to adoptDevice: adoption now fails early with a validation error if the coordinator is offline, preventing creation of Smart Panel devices from stale cached registry entries that are not currently reachable. 2. Add @isdefined to ReqZhAdoptDeviceDto and ReqZhPermitJoinDto: sending {} or omitting the data field now returns a structured validation error instead of a 500 TypeError from dereferencing undefined body.data. 3. Remove unused ZigbeeHerdsmanDeviceAddSimpleFormSchema and its inferred type IZigbeeHerdsmanDeviceAddSimpleForm — neither was used outside their own definition files. 4. Fix devices with lastSeen=null staying online forever: the connectivity checker was skipping these devices entirely, leaving them reported as online indefinitely (registered with available=true but never entering timeout evaluation). Now treats lastSeen=null as offline. Config schema camelCase issue confirmed invalid (4th time) — snakeToCamel transform in transformConfigPluginResponse handles it. https://claude.ai/code/session_014bjB9Cn1WKASNLBeCuSbom
1 parent 6ba136c commit d402e1f

5 files changed

Lines changed: 31 additions & 32 deletions

File tree

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,5 @@
1-
import { z } from 'zod';
2-
31
import { DeviceAddFormSchema, DeviceEditFormSchema } from '../../../modules/devices';
4-
import { DevicesModuleDeviceCategory } from '../../../openapi.constants';
52

63
export const ZigbeeHerdsmanDeviceAddFormSchema = DeviceAddFormSchema;
74

85
export const ZigbeeHerdsmanDeviceEditFormSchema = DeviceEditFormSchema;
9-
10-
export const ZigbeeHerdsmanDeviceAddSimpleFormSchema = z.object({
11-
id: z.string().uuid(),
12-
type: z.string(),
13-
ieeeAddress: z.string().min(1),
14-
name: z.string().min(1),
15-
category: z.nativeEnum(DevicesModuleDeviceCategory),
16-
description: z
17-
.string()
18-
.trim()
19-
.transform((val) => (val === '' ? null : val))
20-
.nullable()
21-
.optional(),
22-
enabled: z.boolean().default(true),
23-
});
Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import type { z } from 'zod';
22

3-
import type {
4-
ZigbeeHerdsmanDeviceAddFormSchema,
5-
ZigbeeHerdsmanDeviceAddSimpleFormSchema,
6-
ZigbeeHerdsmanDeviceEditFormSchema,
7-
} from './devices.schemas';
3+
import type { ZigbeeHerdsmanDeviceAddFormSchema, ZigbeeHerdsmanDeviceEditFormSchema } from './devices.schemas';
84

95
export type IZigbeeHerdsmanDeviceAddForm = z.infer<typeof ZigbeeHerdsmanDeviceAddFormSchema>;
106
export type IZigbeeHerdsmanDeviceEditForm = z.infer<typeof ZigbeeHerdsmanDeviceEditFormSchema>;
11-
export type IZigbeeHerdsmanDeviceAddSimpleForm = z.infer<typeof ZigbeeHerdsmanDeviceAddSimpleFormSchema>;

apps/backend/src/plugins/devices-zigbee-herdsman/dto/mapping-preview.dto.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
import { Expose, Type } from 'class-transformer';
2-
import { IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, Max, Min, ValidateNested } from 'class-validator';
2+
import {
3+
IsArray,
4+
IsBoolean,
5+
IsDefined,
6+
IsEnum,
7+
IsInt,
8+
IsOptional,
9+
IsString,
10+
Max,
11+
Min,
12+
ValidateNested,
13+
} from 'class-validator';
314

415
import { ApiProperty, ApiPropertyOptional, ApiSchema } from '@nestjs/swagger';
516

@@ -226,6 +237,7 @@ export class ReqZhMappingPreviewDto {
226237
export class ReqZhAdoptDeviceDto {
227238
@ApiProperty({ description: 'Device adoption request data', type: () => ZhAdoptDeviceRequestDto })
228239
@Expose()
240+
@IsDefined({ message: '[{"field":"data","reason":"Request body must contain a data property."}]' })
229241
@ValidateNested()
230242
@Type(() => ZhAdoptDeviceRequestDto)
231243
data: ZhAdoptDeviceRequestDto;
@@ -235,6 +247,7 @@ export class ReqZhAdoptDeviceDto {
235247
export class ReqZhPermitJoinDto {
236248
@ApiProperty({ description: 'Permit join request data', type: () => ZhPermitJoinRequestDto })
237249
@Expose()
250+
@IsDefined({ message: '[{"field":"data","reason":"Request body must contain a data property."}]' })
238251
@ValidateNested()
239252
@Type(() => ZhPermitJoinRequestDto)
240253
data: ZhPermitJoinRequestDto;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export class ZhDeviceAdoptionService {
5858
async adoptDevice(request: ZhAdoptDeviceRequestDto): Promise<ZigbeeHerdsmanDeviceEntity> {
5959
this.logger.debug(`Adopting device ieeeAddress=${request.ieeeAddress}`);
6060

61+
// Verify coordinator is online — don't adopt from stale cached data
62+
if (!this.zigbeeHerdsmanService.isCoordinatorOnline()) {
63+
throw new DevicesZigbeeHerdsmanValidationException('Cannot adopt device: Zigbee coordinator is offline');
64+
}
65+
6166
// Verify device exists on network
6267
const discovered = this.zigbeeHerdsmanService.getDiscoveredDevice(request.ieeeAddress);
6368
if (!discovered) {

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,23 @@ export class ZhDeviceConnectivityService implements OnModuleDestroy {
7272
const updates: { ieeeAddress: string; online: boolean }[] = [];
7373

7474
for (const discovered of discoveredDevices) {
75+
let isOnline: boolean;
76+
7577
if (!discovered.lastSeen) {
76-
continue;
78+
// Device was registered with available=true but has never sent a message.
79+
// Treat as offline — it should report in before we consider it reachable.
80+
isOnline = false;
81+
} else {
82+
const isBattery = discovered.powerSource?.toLowerCase().includes('battery');
83+
const timeout = isBattery ? batteryTimeout : mainsTimeout;
84+
const elapsed = (now - discovered.lastSeen.getTime()) / 1000;
85+
isOnline = elapsed < timeout;
7786
}
7887

79-
const isBattery = discovered.powerSource?.toLowerCase().includes('battery');
80-
const timeout = isBattery ? batteryTimeout : mainsTimeout;
81-
const elapsed = (now - discovered.lastSeen.getTime()) / 1000;
82-
const isOnline = elapsed < timeout;
83-
8488
const lastWritten = this.lastDbState.get(discovered.ieeeAddress);
8589
if (lastWritten !== isOnline) {
8690
if (!isOnline) {
87-
this.logger.debug(`Device ${discovered.ieeeAddress} went offline (last seen ${Math.round(elapsed)}s ago)`);
91+
this.logger.debug(`Device ${discovered.ieeeAddress} went offline`);
8892
} else {
8993
this.logger.debug(`Device ${discovered.ieeeAddress} came back online`);
9094
}

0 commit comments

Comments
 (0)