|
| 1 | +import { ServiceClient } from "nodecg-io-core"; |
| 2 | +import fetch from "node-fetch"; |
| 3 | +import { Response } from "node-fetch"; |
| 4 | +import { Color, ColoredPanel, PanelEffect } from "./interfaces"; |
| 5 | +import { NanoleafQueue } from "./nanoleafQueue"; |
| 6 | +import { NanoleafUtils } from "./nanoleafUtils"; |
| 7 | + |
| 8 | +export class NanoleafClient implements ServiceClient<NanoleafClient> { |
| 9 | + // Important: Does only remember colors which were directly set by using setPanelColor(s) |
| 10 | + private colors: Map<number, Color> = new Map<number, Color>(); |
| 11 | + |
| 12 | + // This queue is used to queue effects |
| 13 | + private queue: NanoleafQueue = new NanoleafQueue(); |
| 14 | + |
| 15 | + /** |
| 16 | + * Returns the client-specific effect queue. |
| 17 | + */ |
| 18 | + getQueue(): NanoleafQueue { |
| 19 | + return this.queue; |
| 20 | + } |
| 21 | + |
| 22 | + getNativeClient(): NanoleafClient { |
| 23 | + return this; // yolo |
| 24 | + } |
| 25 | + |
| 26 | + constructor(private ipAddress: string, private authToken: string) {} |
| 27 | + |
| 28 | + private async callGET(relativePath: string) { |
| 29 | + return fetch(NanoleafUtils.buildBaseRequestAddress(this.ipAddress, this.authToken) + relativePath, { |
| 30 | + method: "GET", |
| 31 | + }); |
| 32 | + } |
| 33 | + |
| 34 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 35 | + private async callPUT(relativePath: string, body: any) { |
| 36 | + return fetch(NanoleafUtils.buildBaseRequestAddress(this.ipAddress, this.authToken) + relativePath, { |
| 37 | + method: "PUT", |
| 38 | + headers: { |
| 39 | + "Content-Type": "application/json", |
| 40 | + }, |
| 41 | + body: JSON.stringify(body), |
| 42 | + }); |
| 43 | + } |
| 44 | + |
| 45 | + /** |
| 46 | + * Returns information about all panels, e.g. available effects, position data, ... |
| 47 | + */ |
| 48 | + async getAllPanelInfo(): Promise<Response> { |
| 49 | + return this.callGET(""); |
| 50 | + } |
| 51 | + |
| 52 | + /** |
| 53 | + * Returns the IDs of all panels which are connected to the nanoleaf controller |
| 54 | + * @param sortedByY the IDs are sorted by y level if true, otherwise sorted by x level |
| 55 | + */ |
| 56 | + async getAllPanelIDs(sortedByY: boolean): Promise<Array<number>> { |
| 57 | + const response = await this.getAllPanelInfo(); |
| 58 | + |
| 59 | + if (response.status !== 200) { |
| 60 | + return []; |
| 61 | + } |
| 62 | + |
| 63 | + const json = await response.json(); |
| 64 | + const positionData: Array<{ x: number; y: number; panelId: number }> = json.panelLayout?.layout?.positionData; |
| 65 | + const panels = sortedByY ? positionData.sort((a, b) => a.y - b.y) : positionData.sort((a, b) => a.x - b.x); |
| 66 | + const panelIDs = panels?.map((entry: { panelId: number }) => entry.panelId); |
| 67 | + const panelIDsWithoutController = panelIDs.filter((entry: number) => entry !== 0); |
| 68 | + |
| 69 | + return panelIDsWithoutController; |
| 70 | + } |
| 71 | + |
| 72 | + /** |
| 73 | + * Sets the color of the specified panel directly using a raw effect write call. Not compatible with global effects. |
| 74 | + * @param panelId the panel ID. Use getAllPanelIDs() to retrieve all possible IDs. |
| 75 | + * @param color the color to send |
| 76 | + */ |
| 77 | + async setPanelColor(panelId: number, color: Color): Promise<void> { |
| 78 | + await this.setPanelColors([{ panelId: panelId, color: color }]); |
| 79 | + } |
| 80 | + |
| 81 | + /** |
| 82 | + * Sets the colors of all specified panels directly using a raw effect write call. |
| 83 | + * @param data An array of ColoredPanel objects which hold information about panel IDs and colors. |
| 84 | + */ |
| 85 | + async setPanelColors(data: ColoredPanel[]): Promise<void> { |
| 86 | + data.forEach((panel) => this.colors.set(panel.panelId, panel.color)); |
| 87 | + |
| 88 | + if (data.length >= 1) { |
| 89 | + // This creates an simple short transition effect to the specified colors |
| 90 | + const panelData: PanelEffect[] = data.map((entry) => ({ |
| 91 | + panelId: entry.panelId, |
| 92 | + frames: [{ color: entry.color, transitionTime: 1 }], |
| 93 | + })); |
| 94 | + |
| 95 | + await this.writeRawEffect("display", "static", false, panelData); |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + /** |
| 100 | + * This bad boy function does more than every nanoleaf documentaion ever delivered. This is the pure decoding of awesomeness. |
| 101 | + * The raw effect write call is used to generate custom effects at runtime. Everything you ever dreamed of is possible. |
| 102 | + * @param command 'add' overlays the effect, 'display' overwrites the effect, 'displayTemp' overrides for a specified duration |
| 103 | + * @param animType 'static' for single colors, 'custom' for advanced animations |
| 104 | + * @param loop 'true' if the effect shall be looped after every frame was played |
| 105 | + * @param panelData an array of PanelEffect objects consisting of a panel id and an array of frames |
| 106 | + * @param duration optional, only used if command is set to 'displayTemp' |
| 107 | + */ |
| 108 | + async writeRawEffect( |
| 109 | + command: "add" | "display" | "displayTemp", |
| 110 | + animType: "static" | "custom", |
| 111 | + loop: boolean, |
| 112 | + panelData: PanelEffect[], |
| 113 | + duration = 0, |
| 114 | + ): Promise<void> { |
| 115 | + if (panelData.every((panel) => panel.frames.length >= 1)) { |
| 116 | + // Create animData by mapping the PanelEffect objects to a data stream which is compliant to the nanoleaf documentation §3.2.6.1. |
| 117 | + const animData = |
| 118 | + `${panelData.length}` + panelData.map((entry) => this.mapPanelEffectToAnimData(entry)).join(""); |
| 119 | + |
| 120 | + const json = { |
| 121 | + write: { |
| 122 | + command: command, |
| 123 | + duration: duration, |
| 124 | + animType: animType, |
| 125 | + animData: animData, |
| 126 | + loop: loop, |
| 127 | + palette: [], |
| 128 | + }, |
| 129 | + }; |
| 130 | + |
| 131 | + await this.callPUT("/effects", json); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + private mapPanelEffectToAnimData(panelEffect: PanelEffect): string { |
| 136 | + return ` ${panelEffect.panelId} ${panelEffect.frames.length}${panelEffect.frames |
| 137 | + .map((frame) => this.mapFrameToAnimData(frame.color, frame.transitionTime)) |
| 138 | + .join("")}`; |
| 139 | + } |
| 140 | + |
| 141 | + private mapFrameToAnimData(color: Color, transitionTime: number): string { |
| 142 | + return ` ${color.red} ${color.green} ${color.blue} 0 ${transitionTime}`; |
| 143 | + } |
| 144 | + |
| 145 | + /** |
| 146 | + * Returns the cached color of the specified panel. Please note, this returns only colors which have been set by using setPanelColor(s). |
| 147 | + * @param panelId a valid panel id |
| 148 | + */ |
| 149 | + getPanelColor(panelId: number): Color { |
| 150 | + return this.colors.get(panelId) || { red: 0, blue: 0, green: 0 }; |
| 151 | + } |
| 152 | + |
| 153 | + /** |
| 154 | + * Returns the cached color of all panels. Please note, this returns only colors which have been set by using setPanelColor(s). |
| 155 | + */ |
| 156 | + getAllPanelColors(): Map<number, Color> { |
| 157 | + return this.colors; |
| 158 | + } |
| 159 | + |
| 160 | + /** |
| 161 | + * Sets the brightness of all panels. |
| 162 | + * @param level a number between 0 - 100 |
| 163 | + */ |
| 164 | + async setBrightness(level: number): Promise<void> { |
| 165 | + const data = { brightness: { value: level } }; |
| 166 | + await this.callPUT("/state", data); |
| 167 | + } |
| 168 | + |
| 169 | + /** |
| 170 | + * Sets the state of all panels. |
| 171 | + * @param on true, if the nanoleaf shall shine. false, if you're sad and boring |
| 172 | + */ |
| 173 | + async setState(on: boolean): Promise<void> { |
| 174 | + const data = { on: { value: on } }; |
| 175 | + await this.callPUT("/state", data); |
| 176 | + } |
| 177 | + |
| 178 | + /** |
| 179 | + * Sets the hue of all panels. |
| 180 | + * @param hue a number between 0 - 360 |
| 181 | + */ |
| 182 | + async setHue(hue: number): Promise<void> { |
| 183 | + const data = { hue: { value: hue } }; |
| 184 | + await this.callPUT("/state", data); |
| 185 | + } |
| 186 | + |
| 187 | + /** |
| 188 | + * Sets the saturation of all panels. |
| 189 | + * @param sat a number between 0 - 100 |
| 190 | + */ |
| 191 | + async setSaturation(sat: number): Promise<void> { |
| 192 | + const data = { sat: { value: sat } }; |
| 193 | + await this.callPUT("/state", data); |
| 194 | + } |
| 195 | + |
| 196 | + /** |
| 197 | + * Sets the color temperature of all panels. |
| 198 | + * @param temperature a number between 1200 - 6500 |
| 199 | + */ |
| 200 | + async setColorTemperature(temperature: number): Promise<void> { |
| 201 | + const data = { ct: { value: temperature } }; |
| 202 | + await this.callPUT("/state", data); |
| 203 | + } |
| 204 | +} |
0 commit comments