Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(effects-controller): add support for simple effects controller with preconfigured buttons #40

Merged
merged 7 commits into from
Feb 3, 2025
1 change: 1 addition & 0 deletions src/helpers/security-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const securityGroups = {
},
effects: {
base: baseSecurityGroups,
privileged: [SecurityGroup.ADMIN],
},
poster: {
base: allSecuritySubscriberGroups,
Expand Down
6 changes: 3 additions & 3 deletions src/modules/handlers/lights/effect-sequence-handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import BaseLightsHandler from '../base-lights-handler';
import { BeatEvent, TrackChangeEvent } from '../../events/music-emitter-events';
import { LightsGroup } from '../../lights/entities';
import { LightsPredefinedEffect } from '../../lights/entities/sequences/lights-predefined-effect';
import { LightsTrackEffect } from '../../lights/entities/sequences/lights-track-effect';
import LightsEffect from '../../lights/effects/lights-effect';
import dataSource from '../../../database';
import { MusicEmitter } from '../../events';
Expand All @@ -26,7 +26,7 @@ interface LightsGroupEffect extends LightsGroupEffectBase {
}

export default class EffectSequenceHandler extends BaseLightsHandler {
private sequence: LightsPredefinedEffect[] = [];
private sequence: LightsTrackEffect[] = [];

private sequenceStart: Date = new Date(0);

Expand Down Expand Up @@ -171,7 +171,7 @@ export default class EffectSequenceHandler extends BaseLightsHandler {
this.stopSequence(true);

dataSource
.getRepository(LightsPredefinedEffect)
.getRepository(LightsTrackEffect)
.find({
where: { trackUri: event.trackURI },
relations: { lightGroups: true },
Expand Down
30 changes: 30 additions & 0 deletions src/modules/handlers/lights/effects-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import BaseLightsHandler from '../base-lights-handler';
import { LightsGroup } from '../../lights/entities';
import LightsEffect from '../../lights/effects/lights-effect';
import { BeatEvent } from '../../events/music-emitter-events';
import { RgbColor } from '../../lights/color-definitions';

export type GroupEffectsMap = Map<LightsGroup, LightsEffect | LightsEffect[] | null>;

Expand Down Expand Up @@ -112,4 +113,33 @@ export default abstract class EffectsHandler extends BaseLightsHandler {
}
});
}

/**
* Change the color of the given lights group's effects
* @param entityCopy
* @param colors
*/
updateColors(entityCopy: LightsGroup, colors: RgbColor[]): void {
const entity: LightsGroup | undefined = this.entities.find((e) => e.id === entityCopy.id);
if (!entity) return;

const effect = this.groupColorEffects.get(entity);
if (Array.isArray(effect)) {
effect.forEach((e) => e.setColors(colors));
} else {
effect?.setColors(colors);
}
}

/**
* Change the color of the effects of the lights group with the given ID
* @param id
* @param colors
*/
updateColorsById(id: number, colors: RgbColor[]): void {
const entity: LightsGroup | undefined = this.entities.find((e) => e.id === id);
if (!entity) return;

this.updateColors(entity, colors);
}
}
101 changes: 99 additions & 2 deletions src/modules/handlers/lights/set-effects-controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller } from '@tsoa/runtime';
import { Body, Post, Request, Route, Security, Tags } from 'tsoa';
import { Controller, Patch, TsoaResponse } from '@tsoa/runtime';
import { Body, Delete, Get, Post, Request, Res, Route, Security, Tags } from 'tsoa';
import { Request as ExpressRequest } from 'express';
import SetEffectsHandler from './set-effects-handler';
import HandlerManager from '../../root/handler-manager';
Expand All @@ -9,6 +9,17 @@ import { LightsEffectsColorCreateParams } from '../../lights/effects/color';
import { LightsEffectsMovementCreateParams } from '../../lights/effects/movement';
import logger from '../../../logger';
import { securityGroups } from '../../../helpers/security-groups';
import SetEffectsService, {
LightsPredefinedEffectCreateParams,
LightsPredefinedEffectResponse,
LightsPredefinedEffectUpdateParams,
} from './set-effects-service';
import { RgbColor } from '../../lights/color-definitions';
import { HttpStatusCode } from 'axios';

interface ColorsRequest {
colors: RgbColor[];
}

@Route('handler/lights/set-effects')
@Tags('Handlers')
Expand Down Expand Up @@ -50,6 +61,41 @@ export class SetEffectsController extends Controller {
return { message: 'success' };
}

/**
* Change the colors of the given lights group's color effects
* @param id
* @param req
* @param colors
* @param notFoundResponse
*/
@Security(SecurityNames.LOCAL, securityGroups.effects.base)
@Post('{id}/color/colors')
public async updateLightsEffectColorColors(
id: number,
@Request() req: ExpressRequest,
@Body() colors: ColorsRequest,
@Res() notFoundResponse: TsoaResponse<HttpStatusCode.NotFound, { message: string }>,
) {
const handler: SetEffectsHandler | undefined = HandlerManager.getInstance()
.getHandlers(LightsGroup)
.find((h) => h.constructor.name === SetEffectsHandler.name) as SetEffectsHandler | undefined;
if (!handler) throw new Error('SetEffectsHandler not found');

const lightsGroup = handler.entities.find((e) => e.id === id);
if (lightsGroup === undefined) {
return notFoundResponse(HttpStatusCode.NotFound, {
message: 'LightsGroup not found in SetEffectsHandler',
});
}

logger.audit(
req.user,
`Change colors of lights group "${lightsGroup?.name}"'s effects (id: ${id}).`,
);

handler.updateColors(lightsGroup, colors.colors);
}

/**
* Given a list of movement effects to create, add the given effects to the lightsgroup with the
* given ID. Remove all movement effects if an empty array is given
Expand Down Expand Up @@ -86,4 +132,55 @@ export class SetEffectsController extends Controller {

return { message: 'success' };
}

/**
* Get all existing predefined effects
*/
@Security(SecurityNames.LOCAL, securityGroups.effects.base)
@Get('predefined')
public async getAllPredefinedLightsEffects(): Promise<LightsPredefinedEffectResponse[]> {
const effects = await new SetEffectsService().getAllPredefinedEffects();
return effects.map(SetEffectsService.toLightsEffectPredefinedEffectResponse);
}

/**
* Create a new predefined effect
*/
@Security(SecurityNames.LOCAL, securityGroups.effects.privileged)
@Post('predefined')
public async createPredefinedLightsEffect(
@Request() req: ExpressRequest,
@Body() predefinedEffect: LightsPredefinedEffectCreateParams,
): Promise<LightsPredefinedEffectResponse> {
const effect = await new SetEffectsService().createPredefinedEffect(predefinedEffect);

logger.audit(req.user, `Create new predefined effect on button "${predefinedEffect.buttonId}"`);

return SetEffectsService.toLightsEffectPredefinedEffectResponse(effect);
}

@Security(SecurityNames.LOCAL, securityGroups.effects.privileged)
@Patch('predefined/{id}')
public async updatePredefinedLightsEffect(
id: number,
@Request() req: ExpressRequest,
@Body() predefinedEffect: LightsPredefinedEffectUpdateParams,
): Promise<LightsPredefinedEffectResponse> {
const effect = await new SetEffectsService().updatePredefinedEffect(id, predefinedEffect);

logger.audit(req.user, `Update new predefined effect with id "${id}"`);

return SetEffectsService.toLightsEffectPredefinedEffectResponse(effect);
}

@Security(SecurityNames.LOCAL, securityGroups.effects.privileged)
@Delete('predefined/{id}')
public async deletePredefinedLightsEffect(
id: number,
@Request() req: ExpressRequest,
): Promise<void> {
await new SetEffectsService().deletePredefinedEffect(id);

logger.audit(req.user, `Delete predefined effect with id "${id}"`);
}
}
1 change: 1 addition & 0 deletions src/modules/handlers/lights/set-effects-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { LightsEffectBuilder } from '../../lights/effects/lights-effect';
import { LIGHTS_EFFECTS, LightsEffectsCreateParams } from '../../lights/effects';
import { LightsEffectsColorCreateParams } from '../../lights/effects/color';
import { LightsEffectsMovementCreateParams } from '../../lights/effects/movement';
import { RgbColor } from '../../lights/color-definitions';

export default class SetEffectsHandler extends EffectsHandler {
/**
Expand Down
105 changes: 105 additions & 0 deletions src/modules/handlers/lights/set-effects-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Repository } from 'typeorm';
import LightsPredefinedEffect, {
LightsPredefinedEffectProperties,
} from '../../lights/entities/scenes/lights-predefined-effect';
import dataSource from '../../../database';
import { HttpApiException } from '../../../helpers/custom-error';
import { HttpStatusCode } from 'axios';

export interface LightsPredefinedEffectResponse {
id: number;
createdAt: string;
updatedAt: string;
buttonId: number;
icon?: string | null;
name?: string | null;
properties: LightsPredefinedEffectProperties;
}

export interface LightsPredefinedEffectCreateParams
extends Pick<LightsPredefinedEffect, 'buttonId' | 'properties' | 'icon' | 'name'> {}

export interface LightsPredefinedEffectUpdateParams
extends Partial<LightsPredefinedEffectCreateParams> {}

export default class SetEffectsService {
private repo: Repository<LightsPredefinedEffect>;

constructor(repo?: Repository<LightsPredefinedEffect>) {
this.repo = repo ?? dataSource.getRepository(LightsPredefinedEffect);
}

public static toLightsEffectPredefinedEffectResponse(
e: LightsPredefinedEffect,
): LightsPredefinedEffectResponse {
return {
id: e.id,
createdAt: e.createdAt.toISOString(),
updatedAt: e.updatedAt.toISOString(),
buttonId: e.buttonId,
name: e.name,
icon: e.icon,
properties: e.properties,
};
}

public async getAllPredefinedEffects(): Promise<LightsPredefinedEffect[]> {
return this.repo.find();
}

public async getSinglePredefinedEffect({
id,
buttonId,
}: {
id?: number;
buttonId?: number;
}): Promise<LightsPredefinedEffect | null> {
return this.repo.findOne({ where: { id, buttonId } });
}

public async createPredefinedEffect(
params: LightsPredefinedEffectCreateParams,
): Promise<LightsPredefinedEffect> {
const existing = await this.getSinglePredefinedEffect({ buttonId: params.buttonId });
if (existing) {
throw new HttpApiException(
HttpStatusCode.BadRequest,
`Effect with button ID "${params.buttonId}" already exists.`,
);
}

return this.repo.save(params);
}

public async updatePredefinedEffect(
id: number,
params: LightsPredefinedEffectUpdateParams,
): Promise<LightsPredefinedEffect> {
const existing = await this.getSinglePredefinedEffect({ id });
if (!existing) {
throw new HttpApiException(HttpStatusCode.NotFound, `Effect with ID "${id}" not found.`);
}

// New button ID
if (params.buttonId !== undefined && existing.buttonId !== params.buttonId) {
const buttonMatch = await this.getSinglePredefinedEffect({ buttonId: params.buttonId });
if (buttonMatch) {
throw new HttpApiException(
HttpStatusCode.BadRequest,
`Effect with button ID "${params.buttonId}" already exists."`,
);
}
existing.buttonId = params.buttonId;
}

if (params.properties) existing.properties = params.properties;
if (params.name) existing.name = params.name;
if (params.icon) existing.icon = params.icon;

return this.repo.save(existing);
}

public async deletePredefinedEffect(id: number): Promise<void> {
await this.repo.delete(id);
}
}
5 changes: 5 additions & 0 deletions src/modules/lights/effects/color/beat-fade-out.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import EffectProgressionStrategy from '../progression-strategies/effect-progression-strategy';
import LightsGroupFixture from '../../entities/lights-group-fixture';
import EffectProgressionMapFactory from '../progression-strategies/mappers/effect-progression-map-factory';
import { RgbColor } from '../../color-definitions';

export interface BeatFadeOutProps extends BaseLightsEffectProps, BaseLightsEffectProgressionProps {
/**
Expand Down Expand Up @@ -92,6 +93,10 @@ export default class BeatFadeOut extends LightsEffect<BeatFadeOutProps> {
return (lightsGroup: LightsGroup) => new BeatFadeOut(lightsGroup, props);
}

setColors(colors: RgbColor[]) {
this.props.colors = colors;
}

destroy(): void {}

beat(event: BeatEvent): void {
Expand Down
5 changes: 5 additions & 0 deletions src/modules/lights/effects/color/sparkle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import LightsEffect, {
} from '../lights-effect';
import { LightsGroup } from '../../entities';
import { ColorEffects } from './color-effects';
import { RgbColor } from '../../color-definitions';

export interface SparkleProps extends BaseLightsEffectProps {
/**
Expand Down Expand Up @@ -64,6 +65,10 @@ export default class Sparkle extends LightsEffect<SparkleProps> {
return (lightsGroup: LightsGroup) => new Sparkle(lightsGroup, props);
}

setColors(colors: RgbColor[]) {
this.props.colors = colors;
}

/**
* Enable a subset of lights, i.e. set their brightness back to 1
* @private
Expand Down
4 changes: 4 additions & 0 deletions src/modules/lights/effects/color/static-color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export default class StaticColor extends LightsEffect<StaticColorProps> {
return (lightsGroup) => new StaticColor(lightsGroup, props);
}

setColors(colors: RgbColor[]) {
this.props.color = colors[0];
}

beat(): void {
if (!this.props.beatToggle) return;

Expand Down
5 changes: 5 additions & 0 deletions src/modules/lights/effects/color/wave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { LightsGroup, LightsGroupMovingHeadRgbs, LightsGroupPars } from '../../e
import { ColorEffects } from './color-effects';
import { EffectProgressionTickStrategy } from '../progression-strategies';
import EffectProgressionMapFactory from '../progression-strategies/mappers/effect-progression-map-factory';
import { RgbColor } from '../../color-definitions';

export interface WaveProps extends BaseLightsEffectProps, BaseLightsEffectProgressionProps {
/**
Expand Down Expand Up @@ -56,6 +57,10 @@ export default class Wave extends LightsEffect<WaveProps> {
return (lightsGroup) => new Wave(lightsGroup, props);
}

setColors(colors: RgbColor[]) {
this.props.colors = colors;
}

destroy(): void {}

beat(): void {}
Expand Down
Loading