Merge branch 'feat/add-product-space' into feat/add-space-position

This commit is contained in:
hannathkadher
2024-11-20 21:46:33 +04:00
38 changed files with 821 additions and 23 deletions

View File

@ -8,8 +8,15 @@ import config from './config';
import { EmailService } from './util/email.service'; import { EmailService } from './util/email.service';
import { ErrorMessageService } from 'src/error-message/error-message.service'; import { ErrorMessageService } from 'src/error-message/error-message.service';
import { TuyaService } from './integrations/tuya/services/tuya.service'; import { TuyaService } from './integrations/tuya/services/tuya.service';
import { SceneDeviceRepository } from './modules/scene-device/repositories';
@Module({ @Module({
providers: [CommonService, EmailService, ErrorMessageService, TuyaService], providers: [
CommonService,
EmailService,
ErrorMessageService,
TuyaService,
SceneDeviceRepository,
],
exports: [ exports: [
CommonService, CommonService,
TuyaService, TuyaService,
@ -17,6 +24,7 @@ import { TuyaService } from './integrations/tuya/services/tuya.service';
AuthModule, AuthModule,
EmailService, EmailService,
ErrorMessageService, ErrorMessageService,
SceneDeviceRepository,
], ],
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({

View File

@ -7,3 +7,13 @@ export enum ActionExecutorEnum {
export enum EntityTypeEnum { export enum EntityTypeEnum {
DEVICE_REPORT = 'device_report', DEVICE_REPORT = 'device_report',
} }
export const AUTOMATION_CONFIG = {
DEFAULT_START_TIME: '00:00',
DEFAULT_END_TIME: '23:59',
DEFAULT_LOOPS: '1111111',
DECISION_EXPR: 'and',
CONDITION_TYPE: 'device_report',
ACTION_EXECUTOR: 'rule_trigger',
COMPARATOR: '==',
SCENE_STATUS_VALUE: 'scene',
};

View File

@ -16,4 +16,6 @@ export enum ProductType {
GD = 'GD', GD = 'GD',
CUR = 'CUR', CUR = 'CUR',
PC = 'PC', PC = 'PC',
FOUR_S = '4S',
SIX_S = '6S',
} }

View File

@ -0,0 +1,8 @@
export enum SceneSwitchesTypeEnum {
SCENE_1 = 'scene_1',
SCENE_2 = 'scene_2',
SCENE_3 = 'scene_3',
SCENE_4 = 'scene_4',
SCENE_5 = 'scene_5',
SCENE_6 = 'scene_6',
}

View File

@ -8,7 +8,11 @@ import { UserOtpEntity } from '../modules/user/entities';
import { ProductEntity } from '../modules/product/entities'; import { ProductEntity } from '../modules/product/entities';
import { DeviceEntity } from '../modules/device/entities'; import { DeviceEntity } from '../modules/device/entities';
import { PermissionTypeEntity } from '../modules/permission/entities'; import { PermissionTypeEntity } from '../modules/permission/entities';
import { SpaceEntity, SpaceLinkEntity, SubspaceEntity } from '../modules/space/entities'; import {
SpaceEntity,
SpaceLinkEntity,
SubspaceEntity,
} from '../modules/space/entities';
import { UserSpaceEntity } from '../modules/user/entities'; import { UserSpaceEntity } from '../modules/user/entities';
import { DeviceUserPermissionEntity } from '../modules/device/entities'; import { DeviceUserPermissionEntity } from '../modules/device/entities';
import { UserRoleEntity } from '../modules/user/entities'; import { UserRoleEntity } from '../modules/user/entities';
@ -21,6 +25,8 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
import { CommunityEntity } from '../modules/community/entities'; import { CommunityEntity } from '../modules/community/entities';
import { DeviceStatusLogEntity } from '../modules/device-status-log/entities'; import { DeviceStatusLogEntity } from '../modules/device-status-log/entities';
import { SceneEntity, SceneIconEntity } from '../modules/scene/entities'; import { SceneEntity, SceneIconEntity } from '../modules/scene/entities';
import { SceneDeviceEntity } from '../modules/scene-device/entities';
import { SpaceProductEntity } from '../modules/space/entities/space-product.entity';
@Module({ @Module({
imports: [ imports: [
@ -47,6 +53,7 @@ import { SceneEntity, SceneIconEntity } from '../modules/scene/entities';
SpaceEntity, SpaceEntity,
SpaceLinkEntity, SpaceLinkEntity,
SubspaceEntity, SubspaceEntity,
SpaceProductEntity,
UserSpaceEntity, UserSpaceEntity,
DeviceUserPermissionEntity, DeviceUserPermissionEntity,
UserRoleEntity, UserRoleEntity,
@ -59,6 +66,7 @@ import { SceneEntity, SceneIconEntity } from '../modules/scene/entities';
DeviceStatusLogEntity, DeviceStatusLogEntity,
SceneEntity, SceneEntity,
SceneIconEntity, SceneIconEntity,
SceneDeviceEntity,
], ],
namingStrategy: new SnakeNamingStrategy(), namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),

View File

@ -1,4 +1,12 @@
import { Column, Entity, ManyToOne, OneToMany, Unique, Index, JoinColumn } from 'typeorm'; import {
Column,
Entity,
ManyToOne,
OneToMany,
Unique,
Index,
JoinColumn,
} from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { DeviceDto, DeviceUserPermissionDto } from '../dtos/device.dto'; import { DeviceDto, DeviceUserPermissionDto } from '../dtos/device.dto';
import { SpaceEntity, SubspaceEntity } from '../../space/entities'; import { SpaceEntity, SubspaceEntity } from '../../space/entities';
@ -6,6 +14,7 @@ import { ProductEntity } from '../../product/entities';
import { UserEntity } from '../../user/entities'; import { UserEntity } from '../../user/entities';
import { DeviceNotificationDto } from '../dtos'; import { DeviceNotificationDto } from '../dtos';
import { PermissionTypeEntity } from '../../permission/entities'; import { PermissionTypeEntity } from '../../permission/entities';
import { SceneDeviceEntity } from '../../scene-device/entities';
@Entity({ name: 'device' }) @Entity({ name: 'device' })
@Unique(['deviceTuyaUuid']) @Unique(['deviceTuyaUuid'])
@ -62,6 +71,9 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
@Column({ nullable: false }) @Column({ nullable: false })
uuid: string; uuid: string;
@OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {})
sceneDevices: SceneDeviceEntity[];
constructor(partial: Partial<DeviceEntity>) { constructor(partial: Partial<DeviceEntity>) {
super(); super();
Object.assign(this, partial); Object.assign(this, partial);

View File

@ -2,6 +2,7 @@ import { Column, Entity, OneToMany } from 'typeorm';
import { ProductDto } from '../dtos'; import { ProductDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { DeviceEntity } from '../../device/entities'; import { DeviceEntity } from '../../device/entities';
import { SpaceProductEntity } from '../../space/entities/space-product.entity';
@Entity({ name: 'product' }) @Entity({ name: 'product' })
export class ProductEntity extends AbstractEntity<ProductDto> { export class ProductEntity extends AbstractEntity<ProductDto> {
@ -26,6 +27,9 @@ export class ProductEntity extends AbstractEntity<ProductDto> {
}) })
public prodType: string; public prodType: string;
@OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.product)
spaceProducts: SpaceProductEntity[];
@OneToMany( @OneToMany(
() => DeviceEntity, () => DeviceEntity,
(devicesProductEntity) => devicesProductEntity.productDevice, (devicesProductEntity) => devicesProductEntity.productDevice,

View File

@ -0,0 +1 @@
export * from './scene-device.dto';

View File

@ -0,0 +1,20 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class SceneDeviceDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public deviceUuid: string;
@IsString()
@IsNotEmpty()
public sceneUuid: string;
@IsString()
@IsNotEmpty()
public switchName: string;
@IsString()
@IsNotEmpty()
public automationTuyaUuid: string;
}

View File

@ -0,0 +1 @@
export * from './scene-device.entity';

View File

@ -0,0 +1,51 @@
import { Column, Entity, JoinColumn, ManyToOne, Unique } from 'typeorm';
import { SceneDeviceDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum';
import { DeviceEntity } from '../../device/entities';
import { SceneEntity } from '../../scene/entities';
@Entity({ name: 'scene-device' })
@Unique(['device', 'switchName'])
export class SceneDeviceEntity extends AbstractEntity<SceneDeviceDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@ManyToOne(() => DeviceEntity, (device) => device.sceneDevices, {
nullable: false,
})
@JoinColumn({ name: 'device_uuid' })
device: DeviceEntity;
@Column({
nullable: false,
})
sceneUuid: string;
@Column({
nullable: false,
type: 'enum',
enum: SceneSwitchesTypeEnum,
})
switchName: SceneSwitchesTypeEnum;
@Column({
nullable: false,
})
automationTuyaUuid: string;
@ManyToOne(() => SceneEntity, (scene) => scene.sceneDevices, {
nullable: false,
})
@JoinColumn({ name: 'scene_uuid' })
scene: SceneEntity;
constructor(partial: Partial<SceneDeviceEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1 @@
export * from './scene-device.repository';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { SceneDeviceEntity } from '../entities';
@Injectable()
export class SceneDeviceRepository extends Repository<SceneDeviceEntity> {
constructor(private dataSource: DataSource) {
super(SceneDeviceEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SceneDeviceEntity } from './entities';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([SceneDeviceEntity])],
})
export class SceneDeviceRepositoryModule {}

View File

@ -2,6 +2,7 @@ import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
import { SceneDto, SceneIconDto } from '../dtos'; import { SceneDto, SceneIconDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SceneIconType } from '@app/common/constants/secne-icon-type.enum'; import { SceneIconType } from '@app/common/constants/secne-icon-type.enum';
import { SceneDeviceEntity } from '../../scene-device/entities';
// Define SceneIconEntity before SceneEntity // Define SceneIconEntity before SceneEntity
@Entity({ name: 'scene-icon' }) @Entity({ name: 'scene-icon' })
@ -59,6 +60,9 @@ export class SceneEntity extends AbstractEntity<SceneDto> {
}) })
sceneIcon: SceneIconEntity; sceneIcon: SceneIconEntity;
@OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.scene)
sceneDevices: SceneDeviceEntity[];
constructor(partial: Partial<SceneEntity>) { constructor(partial: Partial<SceneEntity>) {
super(); super();
Object.assign(this, partial); Object.assign(this, partial);

View File

@ -0,0 +1,32 @@
import { Column, Entity, ManyToOne, JoinColumn } from 'typeorm';
import { SpaceEntity } from './space.entity';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ProductEntity } from '../../product/entities';
@Entity({ name: 'space-product' })
export class SpaceProductEntity extends AbstractEntity<SpaceProductEntity> {
@ManyToOne(() => SpaceEntity, (space) => space.spaceProducts, {
nullable: false,
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'space_uuid' })
space: SpaceEntity;
@ManyToOne(() => ProductEntity, (product) => product.spaceProducts, {
nullable: false,
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'product_uuid' })
product: ProductEntity;
@Column({
nullable: false,
type: 'int',
})
productCount: number;
constructor(partial: Partial<SpaceProductEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -13,6 +13,7 @@ import { DeviceEntity } from '../../device/entities';
import { CommunityEntity } from '../../community/entities'; import { CommunityEntity } from '../../community/entities';
import { SubspaceEntity } from './subspace.entity'; import { SubspaceEntity } from './subspace.entity';
import { SpaceLinkEntity } from './space-link.entity'; import { SpaceLinkEntity } from './space-link.entity';
import { SpaceProductEntity } from './space-product.entity';
@Entity({ name: 'space' }) @Entity({ name: 'space' })
@Unique(['invitationCode']) @Unique(['invitationCode'])
@ -86,6 +87,8 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
type: 'text', type: 'text',
}) })
public icon: string; public icon: string;
@OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.space)
spaceProducts: SpaceProductEntity[];
constructor(partial: Partial<SpaceEntity>) { constructor(partial: Partial<SpaceEntity>) {
super(); super();

View File

@ -1,5 +1,6 @@
import { DataSource, Repository } from 'typeorm'; import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { SpaceProductEntity } from '../entities/space-product.entity';
import { SpaceEntity, SpaceLinkEntity, SubspaceEntity } from '../entities'; import { SpaceEntity, SpaceLinkEntity, SubspaceEntity } from '../entities';
@Injectable() @Injectable()
@ -21,3 +22,9 @@ export class SpaceLinkRepository extends Repository<SpaceLinkEntity> {
super(SpaceLinkEntity, dataSource.createEntityManager()); super(SpaceLinkEntity, dataSource.createEntityManager());
} }
} }
@Injectable()
export class SpaceProductRepository extends Repository<SpaceProductEntity> {
constructor(private dataSource: DataSource) {
super(SpaceProductEntity, dataSource.createEntityManager());
}
}

View File

@ -9,6 +9,12 @@ import { DeviceRepository } from '@app/common/modules/device/repositories';
import { ProductRepository } from '@app/common/modules/product/repositories'; import { ProductRepository } from '@app/common/modules/product/repositories';
import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/devices-status.module'; import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/devices-status.module';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { SceneService } from 'src/scene/services';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
@Module({ @Module({
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
@ -20,6 +26,10 @@ import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service
DeviceService, DeviceService,
DeviceRepository, DeviceRepository,
ProductRepository, ProductRepository,
SceneService,
SceneIconRepository,
SceneRepository,
SceneDeviceRepository,
], ],
exports: [AutomationService], exports: [AutomationService],
}) })

View File

@ -30,6 +30,7 @@ import {
} from '@app/common/constants/automation.enum'; } from '@app/common/constants/automation.enum';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { ConvertedAction } from '@app/common/integrations/tuya/interfaces'; import { ConvertedAction } from '@app/common/integrations/tuya/interfaces';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
@Injectable() @Injectable()
export class AutomationService { export class AutomationService {
@ -39,6 +40,7 @@ export class AutomationService {
private readonly spaceRepository: SpaceRepository, private readonly spaceRepository: SpaceRepository,
private readonly deviceService: DeviceService, private readonly deviceService: DeviceService,
private readonly tuyaService: TuyaService, private readonly tuyaService: TuyaService,
private readonly sceneDeviceRepository: SceneDeviceRepository,
) { ) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY'); const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY'); const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
@ -304,6 +306,16 @@ export class AutomationService {
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
); );
} }
const existingSceneDevice = await this.sceneDeviceRepository.findOne({
where: { automationTuyaUuid: automationUuid },
});
if (existingSceneDevice) {
await this.sceneDeviceRepository.delete({
automationTuyaUuid: automationUuid,
});
}
const response = this.tuyaService.deleteAutomation( const response = this.tuyaService.deleteAutomation(
automation.spaceId, automation.spaceId,
automationUuid, automationUuid,
@ -323,6 +335,15 @@ export class AutomationService {
async delete(tuyaSpaceId: string, automationUuid: string) { async delete(tuyaSpaceId: string, automationUuid: string) {
try { try {
const existingSceneDevice = await this.sceneDeviceRepository.findOne({
where: { automationTuyaUuid: automationUuid },
});
if (existingSceneDevice) {
await this.sceneDeviceRepository.delete({
automationTuyaUuid: automationUuid,
});
}
const response = await this.tuyaService.deleteAutomation( const response = await this.tuyaService.deleteAutomation(
tuyaSpaceId, tuyaSpaceId,
automationUuid, automationUuid,

View File

@ -12,19 +12,26 @@ import {
Put, Put,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AddDeviceDto, UpdateDeviceInSpaceDto } from '../dtos/add.device.dto'; import {
AddDeviceDto,
AddSceneToFourSceneDeviceDto,
UpdateDeviceDto,
UpdateDeviceInSpaceDto,
} from '../dtos/add.device.dto';
import { GetDeviceLogsDto } from '../dtos/get.device.dto'; import { GetDeviceLogsDto } from '../dtos/get.device.dto';
import { import {
ControlDeviceDto, ControlDeviceDto,
BatchControlDevicesDto, BatchControlDevicesDto,
BatchStatusDevicesDto, BatchStatusDevicesDto,
BatchFactoryResetDevicesDto, BatchFactoryResetDevicesDto,
GetSceneFourSceneDeviceDto,
} from '../dtos/control.device.dto'; } from '../dtos/control.device.dto';
import { CheckRoomGuard } from 'src/guards/room.guard'; import { CheckRoomGuard } from 'src/guards/room.guard';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { CheckDeviceGuard } from 'src/guards/device.guard'; import { CheckDeviceGuard } from 'src/guards/device.guard';
import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { CheckFourAndSixSceneDeviceTypeGuard } from 'src/guards/scene.device.type.guard';
@ApiTags('Device Module') @ApiTags('Device Module')
@Controller({ @Controller({
@ -90,6 +97,26 @@ export class DeviceController {
userUuid, userUuid,
); );
} }
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put(':deviceUuid')
async updateDevice(
@Param('deviceUuid') deviceUuid: string,
@Body() updateDeviceDto: UpdateDeviceDto,
) {
const device = await this.deviceService.updateDevice(
deviceUuid,
updateDeviceDto,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'device updated successfully',
data: device,
};
}
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get(':deviceUuid/functions') @Get(':deviceUuid/functions')
@ -184,4 +211,35 @@ export class DeviceController {
powerClampUuid, powerClampUuid,
); );
} }
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard)
@Post(':deviceUuid/scenes')
async addSceneToSceneDevice(
@Param('deviceUuid') deviceUuid: string,
@Body() addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto,
) {
const device = await this.deviceService.addSceneToSceneDevice(
deviceUuid,
addSceneToFourSceneDeviceDto,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: `scene added successfully to device ${deviceUuid}`,
data: device,
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckFourAndSixSceneDeviceTypeGuard)
@Get(':deviceUuid/scenes')
async getScenesBySceneDevice(
@Param('deviceUuid') deviceUuid: string,
@Query() getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto,
) {
return await this.deviceService.getScenesBySceneDevice(
deviceUuid,
getSceneFourSceneDeviceDto,
);
}
} }

View File

@ -11,9 +11,18 @@ import { SpaceRepository } from '@app/common/modules/space/repositories';
import { DeviceUserPermissionRepository } from '@app/common/modules/device/repositories'; import { DeviceUserPermissionRepository } from '@app/common/modules/device/repositories';
import { UserRepository } from '@app/common/modules/user/repositories'; import { UserRepository } from '@app/common/modules/user/repositories';
import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/devices-status.module'; import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/devices-status.module';
import { SpaceModule } from 'src/space/space.module';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { SceneService } from 'src/scene/services';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
@Module({ @Module({
imports: [ imports: [
ConfigModule, ConfigModule,
SpaceModule,
ProductRepositoryModule, ProductRepositoryModule,
DeviceRepositoryModule, DeviceRepositoryModule,
DeviceStatusFirebaseModule, DeviceStatusFirebaseModule,
@ -27,6 +36,11 @@ import { DeviceStatusFirebaseModule } from '@app/common/firebase/devices-status/
SpaceRepository, SpaceRepository,
DeviceRepository, DeviceRepository,
UserRepository, UserRepository,
TuyaService,
SceneService,
SceneIconRepository,
SceneRepository,
SceneDeviceRepository,
], ],
exports: [DeviceService], exports: [DeviceService],
}) })

View File

@ -1,5 +1,6 @@
import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
export class AddDeviceDto { export class AddDeviceDto {
@ApiProperty({ @ApiProperty({
@ -35,3 +36,36 @@ export class UpdateDeviceInSpaceDto {
@IsNotEmpty() @IsNotEmpty()
public spaceUuid: string; public spaceUuid: string;
} }
export class AddSceneToFourSceneDeviceDto {
@ApiProperty({
description: 'switchName',
required: true,
})
@IsEnum(SceneSwitchesTypeEnum)
@IsNotEmpty()
switchName: SceneSwitchesTypeEnum;
@ApiProperty({
description: 'sceneUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public sceneUuid: string;
@ApiProperty({
description: 'spaceUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public spaceUuid: string;
}
export class UpdateDeviceDto {
@ApiProperty({
description: 'deviceName',
required: true,
})
@IsString()
@IsNotEmpty()
public deviceName: string;
}

View File

@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsNotEmpty, IsString } from 'class-validator'; import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class ControlDeviceDto { export class ControlDeviceDto {
@ApiProperty({ @ApiProperty({
@ -54,3 +54,12 @@ export class BatchFactoryResetDevicesDto {
@IsNotEmpty() @IsNotEmpty()
public devicesUuid: [string]; public devicesUuid: [string];
} }
export class GetSceneFourSceneDeviceDto {
@ApiProperty({
description: 'switchName',
required: false,
})
@IsString()
@IsOptional()
public switchName?: string;
}

View File

@ -90,3 +90,9 @@ export interface GetPowerClampFunctionsStatusInterface {
success: boolean; success: boolean;
msg: string; msg: string;
} }
export interface GetMacAddressInterface {
uuid: string;
mac: string;
sn: string;
id: string;
}

View File

@ -5,15 +5,23 @@ import {
HttpStatus, HttpStatus,
NotFoundException, NotFoundException,
BadRequestException, BadRequestException,
forwardRef,
Inject,
} from '@nestjs/common'; } from '@nestjs/common';
import { TuyaContext } from '@tuya/tuya-connector-nodejs'; import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { AddDeviceDto, UpdateDeviceInSpaceDto } from '../dtos/add.device.dto'; import {
AddDeviceDto,
AddSceneToFourSceneDeviceDto,
UpdateDeviceDto,
UpdateDeviceInSpaceDto,
} from '../dtos/add.device.dto';
import { import {
DeviceInstructionResponse, DeviceInstructionResponse,
GetDeviceDetailsFunctionsInterface, GetDeviceDetailsFunctionsInterface,
GetDeviceDetailsFunctionsStatusInterface, GetDeviceDetailsFunctionsStatusInterface,
GetDeviceDetailsInterface, GetDeviceDetailsInterface,
GetMacAddressInterface,
GetPowerClampFunctionsStatusInterface, GetPowerClampFunctionsStatusInterface,
controlDeviceInterface, controlDeviceInterface,
getDeviceLogsInterface, getDeviceLogsInterface,
@ -28,6 +36,7 @@ import {
BatchFactoryResetDevicesDto, BatchFactoryResetDevicesDto,
BatchStatusDevicesDto, BatchStatusDevicesDto,
ControlDeviceDto, ControlDeviceDto,
GetSceneFourSceneDeviceDto,
} from '../dtos/control.device.dto'; } from '../dtos/control.device.dto';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter'; import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { DeviceRepository } from '@app/common/modules/device/repositories'; import { DeviceRepository } from '@app/common/modules/device/repositories';
@ -40,6 +49,12 @@ import { DeviceStatuses } from '@app/common/constants/device-status.enum';
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
import { BatteryStatus } from '@app/common/constants/battery-status.enum'; import { BatteryStatus } from '@app/common/constants/battery-status.enum';
import { SpaceEntity } from '@app/common/modules/space/entities'; import { SpaceEntity } from '@app/common/modules/space/entities';
import { SceneService } from 'src/scene/services';
import { AddAutomationDto } from 'src/automation/dtos';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
import { SceneSwitchesTypeEnum } from '@app/common/constants/scene-switch-type.enum';
import { AUTOMATION_CONFIG } from '@app/common/constants/automation.enum';
@Injectable() @Injectable()
export class DeviceService { export class DeviceService {
@ -47,9 +62,13 @@ export class DeviceService {
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly deviceRepository: DeviceRepository, private readonly deviceRepository: DeviceRepository,
private readonly sceneDeviceRepository: SceneDeviceRepository,
private readonly productRepository: ProductRepository, private readonly productRepository: ProductRepository,
private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService, private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService,
private readonly spaceRepository: SpaceRepository, private readonly spaceRepository: SpaceRepository,
@Inject(forwardRef(() => SceneService))
private readonly sceneService: SceneService,
private readonly tuyaService: TuyaService,
) { ) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY'); const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY'); const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
@ -64,11 +83,15 @@ export class DeviceService {
deviceUuid: string, deviceUuid: string,
withProductDevice: boolean = true, withProductDevice: boolean = true,
) { ) {
return await this.deviceRepository.findOne({ const relations = ['subspace'];
where: {
uuid: deviceUuid, if (withProductDevice) {
}, relations.push('productDevice');
...(withProductDevice && { relations: ['productDevice'] }), }
return this.deviceRepository.findOne({
where: { uuid: deviceUuid },
relations,
}); });
} }
@ -274,6 +297,26 @@ export class DeviceService {
); );
} }
} }
async updateDeviceNameTuya(
deviceId: string,
deviceName: string,
): Promise<controlDeviceInterface> {
try {
const path = `/v2.0/cloud/thing/${deviceId}/attribute`;
const response = await this.tuya.request({
method: 'POST',
path,
body: { type: 1, data: deviceName },
});
return response as controlDeviceInterface;
} catch (error) {
throw new HttpException(
'Error updating device name from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async controlDevice(controlDeviceDto: ControlDeviceDto, deviceUuid: string) { async controlDevice(controlDeviceDto: ControlDeviceDto, deviceUuid: string) {
try { try {
const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid, false); const deviceDetails = await this.getDeviceByDeviceUuid(deviceUuid, false);
@ -519,6 +562,9 @@ export class DeviceService {
const response = await this.getDeviceDetailsByDeviceIdTuya( const response = await this.getDeviceDetailsByDeviceIdTuya(
deviceDetails.deviceTuyaUuid, deviceDetails.deviceTuyaUuid,
); );
const macAddress = await this.getMacAddressByDeviceIdTuya(
deviceDetails.deviceTuyaUuid,
);
return { return {
...response, ...response,
@ -526,6 +572,8 @@ export class DeviceService {
productUuid: deviceDetails.productDevice.uuid, productUuid: deviceDetails.productDevice.uuid,
productType: deviceDetails.productDevice.prodType, productType: deviceDetails.productDevice.prodType,
permissionType: userDevicePermission, permissionType: userDevicePermission,
macAddress: macAddress.mac,
subspace: deviceDetails.subspace ? deviceDetails.subspace : {},
}; };
} catch (error) { } catch (error) {
throw new HttpException( throw new HttpException(
@ -534,6 +582,27 @@ export class DeviceService {
); );
} }
} }
async updateDevice(deviceUuid: string, updateDeviceDto: UpdateDeviceDto) {
try {
const device = await this.getDeviceByDeviceUuid(deviceUuid);
if (device.deviceTuyaUuid) {
await this.updateDeviceNameTuya(
device.deviceTuyaUuid,
updateDeviceDto.deviceName,
);
}
return {
uuid: device.uuid,
deviceName: updateDeviceDto.deviceName,
};
} catch (error) {
throw new HttpException(
'Error updating device',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getDeviceDetailsByDeviceIdTuya( async getDeviceDetailsByDeviceIdTuya(
deviceId: string, deviceId: string,
): Promise<GetDeviceDetailsInterface> { ): Promise<GetDeviceDetailsInterface> {
@ -566,7 +635,24 @@ export class DeviceService {
); );
} }
} }
async getMacAddressByDeviceIdTuya(
deviceId: string,
): Promise<GetMacAddressInterface> {
try {
const path = `/v1.0/devices/factory-infos?device_ids=${deviceId}`;
const response = await this.tuya.request({
method: 'GET',
path,
});
return response.result[0];
} catch (error) {
throw new HttpException(
'Error fetching mac address device from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async getDeviceInstructionByDeviceId( async getDeviceInstructionByDeviceId(
deviceUuid: string, deviceUuid: string,
): Promise<DeviceInstructionResponse> { ): Promise<DeviceInstructionResponse> {
@ -630,7 +716,6 @@ export class DeviceService {
status: deviceStatus.result[0].status, status: deviceStatus.result[0].status,
}; };
} catch (error) { } catch (error) {
console.log(error);
throw new HttpException( throw new HttpException(
'Error fetching device functions status', 'Error fetching device functions status',
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
@ -1136,4 +1221,175 @@ export class DeviceService {
return descendants; return descendants;
} }
async addSceneToSceneDevice(
deviceUuid: string,
addSceneToFourSceneDeviceDto: AddSceneToFourSceneDeviceDto,
) {
try {
const { spaceUuid, sceneUuid, switchName } = addSceneToFourSceneDeviceDto;
if (!spaceUuid || !sceneUuid || !switchName) {
throw new BadRequestException('Missing required fields in DTO');
}
const [sceneData, spaceData, deviceData] = await Promise.all([
this.sceneService.findScene(sceneUuid),
this.sceneService.getSpaceByUuid(spaceUuid),
this.getDeviceByDeviceUuid(deviceUuid),
]);
const shortUuid = deviceUuid.slice(0, 6); // First 6 characters of the UUID
const timestamp = Math.floor(Date.now() / 1000); // Current timestamp in seconds
const automationName = `Auto_${shortUuid}_${timestamp}`;
const addAutomationData: AddAutomationDto = {
spaceUuid: spaceData.spaceTuyaUuid,
automationName,
decisionExpr: AUTOMATION_CONFIG.DECISION_EXPR,
effectiveTime: {
start: AUTOMATION_CONFIG.DEFAULT_START_TIME,
end: AUTOMATION_CONFIG.DEFAULT_END_TIME,
loops: AUTOMATION_CONFIG.DEFAULT_LOOPS,
},
conditions: [
{
code: 1,
entityId: deviceData.deviceTuyaUuid,
entityType: AUTOMATION_CONFIG.CONDITION_TYPE,
expr: {
comparator: AUTOMATION_CONFIG.COMPARATOR,
statusCode: switchName,
statusValue: AUTOMATION_CONFIG.SCENE_STATUS_VALUE,
},
},
],
actions: [
{
actionExecutor: AUTOMATION_CONFIG.ACTION_EXECUTOR,
entityId: sceneData.sceneTuyaUuid,
},
],
};
const automation = await this.tuyaService.createAutomation(
addAutomationData.spaceUuid,
addAutomationData.automationName,
addAutomationData.effectiveTime,
addAutomationData.decisionExpr,
addAutomationData.conditions,
addAutomationData.actions,
);
if (automation.success) {
const existingSceneDevice = await this.sceneDeviceRepository.findOne({
where: {
device: { uuid: deviceUuid },
switchName: switchName,
},
relations: ['scene', 'device'],
});
if (existingSceneDevice) {
await this.tuyaService.deleteAutomation(
spaceData.spaceTuyaUuid,
existingSceneDevice.automationTuyaUuid,
);
existingSceneDevice.automationTuyaUuid = automation.result.id;
existingSceneDevice.scene = sceneData;
existingSceneDevice.device = deviceData;
existingSceneDevice.switchName = switchName;
return await this.sceneDeviceRepository.save(existingSceneDevice);
} else {
const sceneDevice = await this.sceneDeviceRepository.save({
scene: sceneData,
device: deviceData,
automationTuyaUuid: automation.result.id,
switchName: switchName,
});
return sceneDevice;
}
}
} catch (err) {
const errorMessage = err.message || 'Error creating automation';
const errorStatus = err.status || HttpStatus.INTERNAL_SERVER_ERROR;
throw new HttpException(errorMessage, errorStatus);
}
}
async getScenesBySceneDevice(
deviceUuid: string,
getSceneFourSceneDeviceDto: GetSceneFourSceneDeviceDto,
): Promise<any> {
try {
if (getSceneFourSceneDeviceDto.switchName) {
// Query for a single record directly when switchName is provided
const sceneDevice = await this.sceneDeviceRepository.findOne({
where: {
device: { uuid: deviceUuid },
switchName:
getSceneFourSceneDeviceDto.switchName as SceneSwitchesTypeEnum,
},
relations: ['device', 'scene'],
});
if (!sceneDevice) {
throw new HttpException(
`No scene found for device with UUID ${deviceUuid} and switch name ${getSceneFourSceneDeviceDto.switchName}`,
HttpStatus.NOT_FOUND,
);
}
const sceneDetails = await this.sceneService.getSceneByUuid(
sceneDevice.scene.uuid,
);
return {
switchName: sceneDevice.switchName,
createdAt: sceneDevice.createdAt,
updatedAt: sceneDevice.updatedAt,
deviceUuid: sceneDevice.device.uuid,
scene: sceneDetails.data,
};
}
// Query for multiple records if switchName is not provided
const sceneDevices = await this.sceneDeviceRepository.find({
where: { device: { uuid: deviceUuid } },
relations: ['device', 'scene'],
});
if (!sceneDevices.length) {
throw new HttpException(
`No scenes found for device with UUID ${deviceUuid}`,
HttpStatus.NOT_FOUND,
);
}
const results = await Promise.all(
sceneDevices.map(async (sceneDevice) => {
const sceneDetails = await this.sceneService.getSceneByUuid(
sceneDevice.scene.uuid,
);
return {
switchName: sceneDevice.switchName,
createdAt: sceneDevice.createdAt,
updatedAt: sceneDevice.updatedAt,
deviceUuid: sceneDevice.device.uuid,
scene: sceneDetails.data,
};
}),
);
return results;
} catch (error) {
throw new HttpException(
error.message || 'Failed to fetch scenes for device',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
} }

View File

@ -11,6 +11,13 @@ import { ProductRepository } from '@app/common/modules/product/repositories';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceRepository } from '@app/common/modules/space/repositories';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { SceneService } from 'src/scene/services';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
@Module({ @Module({
imports: [ConfigModule, DeviceRepositoryModule], imports: [ConfigModule, DeviceRepositoryModule],
controllers: [DoorLockController], controllers: [DoorLockController],
@ -24,6 +31,11 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log
DeviceStatusFirebaseService, DeviceStatusFirebaseService,
SpaceRepository, SpaceRepository,
DeviceStatusLogRepository, DeviceStatusLogRepository,
TuyaService,
SceneService,
SceneIconRepository,
SceneRepository,
SceneDeviceRepository,
], ],
exports: [DoorLockService], exports: [DoorLockService],
}) })

View File

@ -120,7 +120,6 @@ export class GroupService {
throw new HttpException('No devices found', HttpStatus.NOT_FOUND); throw new HttpException('No devices found', HttpStatus.NOT_FOUND);
return devices.flat(); // Flatten the array since flatMap was used return devices.flat(); // Flatten the array since flatMap was used
} catch (error) { } catch (error) {
console.log(error);
throw new HttpException( throw new HttpException(
'This space does not have any devices for the specified group name', 'This space does not have any devices for the specified group name',
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,

View File

@ -0,0 +1,42 @@
import { ProductType } from '@app/common/constants/product-type.enum';
import {
Injectable,
CanActivate,
ExecutionContext,
BadRequestException,
HttpException,
} from '@nestjs/common';
import { DeviceService } from 'src/device/services';
@Injectable()
export class CheckFourAndSixSceneDeviceTypeGuard implements CanActivate {
constructor(private readonly deviceService: DeviceService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const deviceUuid = request.params.deviceUuid;
if (!deviceUuid) {
throw new BadRequestException('Device UUID is required');
}
try {
const deviceDetails =
await this.deviceService.getDeviceByDeviceUuid(deviceUuid);
if (
deviceDetails.productDevice.prodType !== ProductType.FOUR_S &&
deviceDetails.productDevice.prodType !== ProductType.SIX_S
) {
throw new BadRequestException('The device type is not supported');
}
return true;
} catch (error) {
throw new HttpException(
error.message || 'An error occurred',
error.status || 500,
);
}
}
}

View File

@ -13,6 +13,7 @@ import {
SceneRepository, SceneRepository,
} from '@app/common/modules/scene/repositories'; } from '@app/common/modules/scene/repositories';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
@Module({ @Module({
imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule], imports: [ConfigModule, SpaceRepositoryModule, DeviceStatusFirebaseModule],
@ -26,6 +27,7 @@ import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service
ProductRepository, ProductRepository,
SceneIconRepository, SceneIconRepository,
SceneRepository, SceneRepository,
SceneDeviceRepository,
], ],
exports: [SceneService], exports: [SceneService],
}) })

View File

@ -3,6 +3,8 @@ import {
HttpException, HttpException,
HttpStatus, HttpStatus,
BadRequestException, BadRequestException,
forwardRef,
Inject,
} from '@nestjs/common'; } from '@nestjs/common';
import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceRepository } from '@app/common/modules/space/repositories';
import { import {
@ -14,7 +16,6 @@ import {
UpdateSceneTapToRunDto, UpdateSceneTapToRunDto,
} from '../dtos'; } from '../dtos';
import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter'; import { convertKeysToSnakeCase } from '@app/common/helper/snakeCaseConverter';
import { DeviceService } from 'src/device/services';
import { import {
AddTapToRunSceneInterface, AddTapToRunSceneInterface,
DeleteTapToRunSceneInterface, DeleteTapToRunSceneInterface,
@ -37,6 +38,7 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { HttpStatusCode } from 'axios'; import { HttpStatusCode } from 'axios';
import { ConvertedAction } from '@app/common/integrations/tuya/interfaces'; import { ConvertedAction } from '@app/common/integrations/tuya/interfaces';
import { DeviceService } from 'src/device/services';
@Injectable() @Injectable()
export class SceneService { export class SceneService {
@ -44,8 +46,9 @@ export class SceneService {
private readonly spaceRepository: SpaceRepository, private readonly spaceRepository: SpaceRepository,
private readonly sceneIconRepository: SceneIconRepository, private readonly sceneIconRepository: SceneIconRepository,
private readonly sceneRepository: SceneRepository, private readonly sceneRepository: SceneRepository,
private readonly deviceService: DeviceService,
private readonly tuyaService: TuyaService, private readonly tuyaService: TuyaService,
@Inject(forwardRef(() => DeviceService))
private readonly deviceService: DeviceService,
) {} ) {}
async createScene( async createScene(
@ -178,7 +181,10 @@ export class SceneService {
}); });
if (!scenesData.length) { if (!scenesData.length) {
return []; throw new HttpException(
`No scenes found for space UUID ${spaceUuid} with showInHomePage ${showInHomePage} `,
HttpStatus.NOT_FOUND,
);
} }
const scenes = await Promise.all( const scenes = await Promise.all(
@ -438,7 +444,6 @@ export class SceneService {
if (err instanceof BadRequestException) { if (err instanceof BadRequestException) {
throw err; throw err;
} else { } else {
console.log(err);
throw new HttpException( throw new HttpException(
`An error occurred while retrieving scene details for ${scene.uuid}`, `An error occurred while retrieving scene details for ${scene.uuid}`,
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
@ -454,9 +459,6 @@ export class SceneService {
const space = await this.getSpaceByUuid(scene.spaceUuid); const space = await this.getSpaceByUuid(scene.spaceUuid);
await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid); await this.delete(scene.sceneTuyaUuid, space.spaceTuyaUuid);
await this.sceneRepository.remove(scene);
return new SuccessResponseDto({ return new SuccessResponseDto({
message: `Scene with ID ${sceneUuid} deleted successfully`, message: `Scene with ID ${sceneUuid} deleted successfully`,
}); });
@ -564,7 +566,6 @@ export class SceneService {
}; };
} catch (err) { } catch (err) {
if (err instanceof BadRequestException) { if (err instanceof BadRequestException) {
console.log(err);
throw err; throw err;
} else { } else {
throw new HttpException( throw new HttpException(

View File

@ -1,11 +1,14 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { import {
IsArray,
IsBoolean, IsBoolean,
IsNotEmpty, IsNotEmpty,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString, IsString,
IsUUID, IsUUID,
ValidateNested,
} from 'class-validator'; } from 'class-validator';
export class AddSpaceDto { export class AddSpaceDto {
@ -50,6 +53,11 @@ export class AddSpaceDto {
@IsString() @IsString()
@IsOptional() @IsOptional()
direction: string; direction: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => ProductAssignmentDto)
products: ProductAssignmentDto[];
} }
export class AddUserSpaceDto { export class AddUserSpaceDto {
@ -87,7 +95,16 @@ export class AddUserSpaceUsingCodeDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
public inviteCode: string; public inviteCode: string;
constructor(dto: Partial<AddUserSpaceDto>) { constructor(dto: Partial<AddUserSpaceDto>) {
Object.assign(this, dto); Object.assign(this, dto);
} }
} }
class ProductAssignmentDto {
@IsNotEmpty()
productId: string;
@IsNotEmpty()
count: number;
}

View File

@ -4,3 +4,4 @@ export * from './space-device.service';
export * from './subspace'; export * from './subspace';
export * from './space-link'; export * from './space-link';
export * from './space-scene.service'; export * from './space-scene.service';
export * from './space-products';

View File

@ -0,0 +1 @@
export * from './space-products.service';

View File

@ -0,0 +1,95 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { SpaceEntity } from '@app/common/modules/space/entities';
import { SpaceProductEntity } from '@app/common/modules/space/entities/space-product.entity';
import { SpaceProductRepository } from '@app/common/modules/space/repositories';
import { In } from 'typeorm';
@Injectable()
export class SpaceProductService {
constructor(
private readonly productRepository: ProductRepository,
private readonly spaceProductRepository: SpaceProductRepository,
) {}
async assignProductsToSpace(
space: SpaceEntity,
products: { productId: string; count: number }[],
): Promise<SpaceProductEntity[]> {
try {
const uniqueProducts = this.validateUniqueProducts(products);
const productEntities = await this.getProductEntities(uniqueProducts);
const spaceProductEntities = uniqueProducts.map(
({ productId, count }) => {
const product = productEntities.get(productId);
if (!product) {
throw new HttpException(
`Product with ID ${productId} not found`,
HttpStatus.NOT_FOUND,
);
}
return this.spaceProductRepository.create({
space,
product,
productCount: count,
});
},
);
return await this.spaceProductRepository.save(spaceProductEntities);
} catch (error) {
console.error('Error assigning products to space:', error);
if (!(error instanceof HttpException)) {
throw new HttpException(
'An error occurred while assigning products to the space',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
throw error;
}
}
private validateUniqueProducts(
products: { productId: string; count: number }[],
): { productId: string; count: number }[] {
const productIds = new Set();
const uniqueProducts = [];
for (const product of products) {
if (productIds.has(product.productId)) {
throw new HttpException(
`Duplicate product ID found: ${product.productId}`,
HttpStatus.BAD_REQUEST,
);
}
productIds.add(product.productId);
uniqueProducts.push(product);
}
return uniqueProducts;
}
private async getProductEntities(
products: { productId: string; count: number }[],
): Promise<Map<string, any>> {
try {
const productIds = products.map((p) => p.productId);
const productEntities = await this.productRepository.find({
where: { prodId: In(productIds) },
});
return new Map(productEntities.map((p) => [p.prodId, p]));
} catch (error) {
console.error('Error fetching product entities:', error);
throw new HttpException(
'Failed to fetch product entities',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -12,6 +12,7 @@ import { CommunityRepository } from '@app/common/modules/community/repositories'
import { SpaceEntity } from '@app/common/modules/space/entities'; import { SpaceEntity } from '@app/common/modules/space/entities';
import { generateRandomString } from '@app/common/helper/randomString'; import { generateRandomString } from '@app/common/helper/randomString';
import { SpaceLinkService } from './space-link'; import { SpaceLinkService } from './space-link';
import { SpaceProductService } from './space-products';
@Injectable() @Injectable()
export class SpaceService { export class SpaceService {
@ -19,13 +20,14 @@ export class SpaceService {
private readonly spaceRepository: SpaceRepository, private readonly spaceRepository: SpaceRepository,
private readonly communityRepository: CommunityRepository, private readonly communityRepository: CommunityRepository,
private readonly spaceLinkService: SpaceLinkService, private readonly spaceLinkService: SpaceLinkService,
private readonly spaceProductService: SpaceProductService,
) {} ) {}
async createSpace( async createSpace(
addSpaceDto: AddSpaceDto, addSpaceDto: AddSpaceDto,
communityId: string, communityId: string,
): Promise<BaseResponseDto> { ): Promise<BaseResponseDto> {
const { parentUuid, direction } = addSpaceDto; const { parentUuid, direction, products } = addSpaceDto;
const community = await this.validateCommunity(communityId); const community = await this.validateCommunity(communityId);
@ -47,6 +49,13 @@ export class SpaceService {
); );
} }
if (products && products.length > 0) {
await this.spaceProductService.assignProductsToSpace(
newSpace,
products,
);
}
return new SuccessResponseDto({ return new SuccessResponseDto({
statusCode: HttpStatus.CREATED, statusCode: HttpStatus.CREATED,
data: newSpace, data: newSpace,

View File

@ -12,6 +12,7 @@ import {
import { import {
SpaceDeviceService, SpaceDeviceService,
SpaceLinkService, SpaceLinkService,
SpaceProductService,
SpaceSceneService, SpaceSceneService,
SpaceService, SpaceService,
SpaceUserService, SpaceUserService,
@ -19,6 +20,7 @@ import {
SubSpaceService, SubSpaceService,
} from './services'; } from './services';
import { import {
SpaceProductRepository,
SpaceRepository, SpaceRepository,
SubspaceRepository, SubspaceRepository,
SpaceLinkRepository, SpaceLinkRepository,
@ -39,6 +41,7 @@ import {
import { DeviceService } from 'src/device/services'; import { DeviceService } from 'src/device/services';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service'; import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
@Module({ @Module({
imports: [ConfigModule, SpaceRepositoryModule], imports: [ConfigModule, SpaceRepositoryModule],
@ -73,6 +76,9 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log
DeviceService, DeviceService,
DeviceStatusFirebaseService, DeviceStatusFirebaseService,
DeviceStatusLogRepository, DeviceStatusLogRepository,
SceneDeviceRepository,
SpaceProductService,
SpaceProductRepository,
], ],
exports: [SpaceService], exports: [SpaceService],
}) })

View File

@ -13,6 +13,13 @@ import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status
import { SpaceRepository } from '@app/common/modules/space/repositories'; import { SpaceRepository } from '@app/common/modules/space/repositories';
import { VisitorPasswordRepository } from '@app/common/modules/visitor-password/repositories'; import { VisitorPasswordRepository } from '@app/common/modules/visitor-password/repositories';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories'; import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { SceneService } from 'src/scene/services';
import {
SceneIconRepository,
SceneRepository,
} from '@app/common/modules/scene/repositories';
import { SceneDeviceRepository } from '@app/common/modules/scene-device/repositories';
@Module({ @Module({
imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule], imports: [ConfigModule, DeviceRepositoryModule, DoorLockModule],
controllers: [VisitorPasswordController], controllers: [VisitorPasswordController],
@ -27,6 +34,11 @@ import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log
DeviceRepository, DeviceRepository,
VisitorPasswordRepository, VisitorPasswordRepository,
DeviceStatusLogRepository, DeviceStatusLogRepository,
TuyaService,
SceneService,
SceneIconRepository,
SceneRepository,
SceneDeviceRepository,
], ],
exports: [VisitorPasswordService], exports: [VisitorPasswordService],
}) })