Skip to content
This repository was archived by the owner on Apr 13, 2025. It is now read-only.

elgato-light service for Keylights and light stripes. #247

Merged
merged 6 commits into from
Sep 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions nodecg-io-elgato-light/extension/elgatoLight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import fetch from "node-fetch";
import { LightData, LightValues } from "./lightData";
import { Response } from "node-fetch";

export type LightType = "KeyLight" | "LightStrip";

/**
* Represents an elgato light. Is never directly created but has subclasses for the different light types.
*/
export abstract class ElgatoLight {
constructor(public readonly ipAddress: string, public readonly name?: string) {}

/**
* Tests if the elgato light is reachable.
* @returns true if the test call returned success. false, otherwise
*/
public async validate(): Promise<boolean> {
const response = await this.callGET();
return response.status === 200;
}

private buildPath(): string {
return `http://${this.ipAddress}:9123/elgato/lights`;
}

private async callGET() {
return fetch(this.buildPath(), {
method: "GET",
});
}

/**
* Helper method to call HTTP PUT on the elgato light.
* @param body json data to send to the elgato light
* @returns the response of the elgato light
*/
protected async callPUT(body: LightData): Promise<Response> {
return fetch(this.buildPath(), {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
}

/**
* Helper method to call HTTP GET on the elgato light and ease the interpretation of the response.
* @returns the response of the elgato light or undefined
*/
protected async getLightData(): Promise<LightValues | undefined> {
const response = await this.callGET();

if (response.status !== 200) {
return undefined;
}

return ((await response.json()) as LightData).lights[0];
}

/**
*
* @returns Returns true if the light is switched on.
*/
async isLightOn(): Promise<boolean> {
return (await this.getLightData())?.on === 1;
}

/**
* Switches the elgato light on.
*/
async turnLightOn(): Promise<void> {
const lightData = ElgatoLight.createLightData({ on: 1 });
await this.callPUT(lightData);
}

/**
* Switches the elgato light off.
*/
async turnLightOff(): Promise<void> {
const lightData = ElgatoLight.createLightData({ on: 0 });
await this.callPUT(lightData);
}

/**
* Toggles the on/off state of the elgato light.
*/
async toggleLight(): Promise<void> {
const state = await this.isLightOn();
const lightData = ElgatoLight.createLightData({ on: state ? 0 : 1 });
await this.callPUT(lightData);
}

/**
* Sets the brightness of the elgato light.
* @param brightness a value between 0.0 and 100.0
*/
async setBrightness(brightness: number): Promise<void> {
const sanitizedValue = Math.max(0, Math.min(100, brightness));
const lightData = ElgatoLight.createLightData({ brightness: sanitizedValue });
await this.callPUT(lightData);
}

/**
* Returns the brightness of the elgato light.
* @returns a value between 0.0 and 100.0 or -1 if an error occurred
*/
async getBrightness(): Promise<number> {
return (await this.getLightData())?.brightness ?? -1;
}

protected static createLightData(data: LightValues): LightData {
return {
numberOfLights: 1,
lights: [data],
};
}
}

/**
* Represents an elgato key light, e.g., the key light or key light air.
*/
export class ElgatoKeyLight extends ElgatoLight {
private static readonly temperatureFactor = 1000000;

/**
* Sets the temperature of the elgato key light.
* @param temperature a value between 2900 and 7000 kelvin
*/
async setTemperature(temperature: number): Promise<void> {
const sanitizedValue = Math.max(143, Math.min(344, ElgatoKeyLight.temperatureFactor / temperature));
const lightData = ElgatoLight.createLightData({ temperature: sanitizedValue });
await this.callPUT(lightData);
}

/**
* Returns the temperature of the elgato key light.
* @returns a value between 2900 and 7000 or -1 if an error occurred
*/
async getTemperature(): Promise<number> {
const temperature = (await this.getLightData())?.temperature;

if (temperature !== undefined) {
return ElgatoKeyLight.temperatureFactor / temperature;
} else {
return -1;
}
}
}

/**
* Represents an elgato light stripe of any length.
*/
export class ElgatoLightStrip extends ElgatoLight {
/**
* Sets the hue of the elgato light stripe.
* @param hue a value between 0.0 and 360.0
*/
async setHue(hue: number): Promise<void> {
const sanitizedValue = Math.max(0, Math.min(360, hue));
const lightData = ElgatoLight.createLightData({ hue: sanitizedValue });
await this.callPUT(lightData);
}

/**
* Returns the hue of the elgato light stripe.
* @returns a value between 0.0 and 360.0 or -1 if an error occurred
*/
async getHue(): Promise<number> {
return (await this.getLightData())?.hue ?? -1;
}

/**
* Sets the saturation of the elgato light stripe.
* @param saturation a value between 0.0 and 100.0
*/
async setSaturation(saturation: number): Promise<void> {
const sanitizedValue = Math.max(0, Math.min(100, saturation));
const lightData = ElgatoLight.createLightData({ saturation: sanitizedValue });
await this.callPUT(lightData);
}

/**
* Returns the saturation of the elgato light stripe.
* @returns a value between 0.0 and 100.0 or -1 if an error occurred
*/
async getSaturation(): Promise<number> {
return (await this.getLightData())?.saturation ?? -1;
}
}
73 changes: 73 additions & 0 deletions nodecg-io-elgato-light/extension/elgatoLightClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ElgatoLight } from ".";
import { ElgatoKeyLight, ElgatoLightStrip, LightType } from "./elgatoLight";

export interface ElgatoLightConfig {
lights: [
{
ipAddress: string;
lightType: LightType;
name?: string;
},
];
}

/**
* The elgato light client is used to access all configured elgato lights. Just use the get methods.
*/
export class ElgatoLightClient {
private lights: ElgatoLight[] = [];

constructor(private config: ElgatoLightConfig) {
this.lights = this.config.lights.map((light) => this.createLight(light.ipAddress, light.lightType, light.name));
}

private createLight(ipAddress: string, lightType: LightType, name?: string) {
if (lightType === "KeyLight") {
return new ElgatoKeyLight(ipAddress, name);
} else {
return new ElgatoLightStrip(ipAddress, name);
}
}

/**
* Tries to reach all elgato lights contained in the config provided in the constructor.
* @returns an array of IP addresses of elgato lights that where configured but not reachable
*/
async identifyNotReachableLights(): Promise<Array<string>> {
const notReachableLights = [];

for (const light of this.lights) {
if (!(await light.validate())) {
notReachableLights.push(light.ipAddress);
}
}

return notReachableLights;
}

/**
* Returns all configured elgato lights.
* @returns an array of elgato lights (elgato key lights or light stripes)
*/
getAllLights(): ElgatoLight[] {
return [...this.lights];
}

/**
* Returns the specified elgato light (elgato key light or light stripe)
* @param name the name of the elgato light specified in the nodecg-io config
* @returns the specified elgato light instance or undefined if the name was not found
*/
getLightByName(name: string): ElgatoLight | undefined {
return this.lights.find((light) => light.name === name);
}

/**
* Returns the specified elgato light (elgato key light or light stripe)
* @param ipAddress the ip address of the elgato light as specified in the nodecg-io config
* @returns the specified elgato light instance or undefined if the address was not found
*/
getLightByAddress(ipAddress: string): ElgatoLight | undefined {
return this.lights.find((light) => light.ipAddress === ipAddress);
}
}
31 changes: 31 additions & 0 deletions nodecg-io-elgato-light/extension/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NodeCG } from "nodecg/types/server";
import { Result, emptySuccess, success, ServiceBundle, error } from "nodecg-io-core";
import { ElgatoLightClient, ElgatoLightConfig } from "./elgatoLightClient";

export { ElgatoLight, ElgatoKeyLight, ElgatoLightStrip, LightType } from "./elgatoLight";
export { ElgatoLightClient, ElgatoLightConfig } from "./elgatoLightClient";

module.exports = (nodecg: NodeCG) => {
new ElgatoLightService(nodecg, "elgato-light", __dirname, "../schema.json").register();
};

class ElgatoLightService extends ServiceBundle<ElgatoLightConfig, ElgatoLightClient> {
async validateConfig(config: ElgatoLightConfig): Promise<Result<void>> {
const notReachableLights = await new ElgatoLightClient(config).identifyNotReachableLights();
if (notReachableLights.length === 0) {
return emptySuccess();
}

return error(`Unable to connect to the lights with the following IPs: ${notReachableLights.join(", ")}`);
}

async createClient(config: ElgatoLightConfig): Promise<Result<ElgatoLightClient>> {
const client = new ElgatoLightClient(config);
this.nodecg.log.info("Successfully created Elgato light clients.");
return success(client);
}

stopClient(_: ElgatoLightClient): void {
this.nodecg.log.info("Successfully stopped Elgato light clients.");
}
}
15 changes: 15 additions & 0 deletions nodecg-io-elgato-light/extension/lightData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* DTO for elgato light http communication.
*/
export interface LightData {
numberOfLights: 1;
lights: LightValues[];
}

export interface LightValues {
on?: number;
hue?: number;
saturation?: number;
brightness?: number;
temperature?: number;
}
48 changes: 48 additions & 0 deletions nodecg-io-elgato-light/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "nodecg-io-elgato-light",
"version": "0.2.0",
"description": "Control your Elgato lights, e.g. key lights and light stripes.",
"homepage": "https://nodecg.io/RELEASE/samples/elgato-light",
"author": {
"name": "CodeOverflow team",
"url": "https://github.com/codeoverflow-org"
},
"repository": {
"type": "git",
"url": "https://github.com/codeoverflow-org/nodecg-io.git",
"directory": "nodecg-io-elgato-light"
},
"files": [
"**/*.js",
"**/*.js.map",
"**/*.d.ts",
"*.json"
],
"main": "extension/index",
"scripts": {
"build": "tsc -b",
"watch": "tsc -b -w",
"clean": "tsc -b --clean"
},
"keywords": [
"nodecg-io",
"nodecg-bundle"
],
"nodecg": {
"compatibleRange": "^1.1.1",
"bundleDependencies": {
"nodecg-io-core": "^0.2.0"
}
},
"license": "MIT",
"devDependencies": {
"@types/node": "^15.0.2",
"nodecg": "^1.8.1",
"typescript": "^4.2.4"
},
"dependencies": {
"@types/node-fetch": "^2.5.10",
"node-fetch": "^2.6.1",
"nodecg-io-core": "^0.2.0"
}
}
31 changes: 31 additions & 0 deletions nodecg-io-elgato-light/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"properties": {
"lights": {
"type": "array",
"description": "A list of elgato lights you want to control.",
"items": {
"type": "object",
"properties": {
"ipAddress": {
"type": "string",
"description": "The IP-Address of a elgato light in your local network."
},
"lightType": {
"type": "string",
"enum": ["KeyLight", "LightStrip"],
"description": "The type of light. Available types: 'KeyLight', 'LightStrip'."
},
"name": {
"type": "string",
"description": "An optional name for the light that can be used to identify it."
}
},
"required": ["ipAddress", "lightType"]
}
}
},
"required": ["lights"]
}
3 changes: 3 additions & 0 deletions nodecg-io-elgato-light/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../tsconfig.common.json"
}
Loading