implement sse
This commit is contained in:
28
package-lock.json
generated
28
package-lock.json
generated
@@ -26,8 +26,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@dpu/shared": {
|
"node_modules/@dpu/shared": {
|
||||||
"version": "1.5.5",
|
"version": "1.6.2",
|
||||||
"resolved": "git+https://git.dariusbag.dev/DarDarBinks/dpu-shared.git#921882054daa8ef862ee25fc098a295d2c7f0e04",
|
"resolved": "git+https://git.dariusbag.dev/DarDarBinks/dpu-shared.git#ceadd4e5a2db3e94234a455d66338fb94fccea40",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
@@ -752,9 +752,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.10.10",
|
"version": "24.10.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.11.tgz",
|
||||||
"integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==",
|
"integrity": "sha512-/Af7O8r1frCVgOz0I62jWUtMohJ0/ZQU/ZoketltOJPZpnb17yoNc9BSoVuV9qlaIXJiPNOpsfq4ByFajSArNQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -944,9 +944,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "17.2.3",
|
"version": "17.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz",
|
||||||
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
"integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -1325,9 +1325,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-tsconfig": {
|
"node_modules/get-tsconfig": {
|
||||||
"version": "4.13.1",
|
"version": "4.13.3",
|
||||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.3.tgz",
|
||||||
"integrity": "sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==",
|
"integrity": "sha512-vp8Cj/+9Q/ibZUrq1rhy8mCTQpCk31A3uu9wc1C50yAb3x2pFHOsGdAZQ7jD86ARayyxZUViYeIztW+GE8dcrg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1820,9 +1820,9 @@
|
|||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
API_HA_DeskPosition,
|
type API_HA_DeskPosition,
|
||||||
BaseService,
|
BaseService,
|
||||||
type HomeAssistantDeskPositionResult,
|
type HomeAssistantDeskPositionResult,
|
||||||
type HomeAssistantEntity,
|
type HomeAssistantEntity,
|
||||||
@@ -75,12 +75,14 @@ export class HomeAssistantService extends BaseService<HomeAssistantClient> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
convertPosResultToApiAnswer(position: HomeAssistantDeskPositionResult): API_HA_DeskPosition {
|
convertPosResultToApiAnswer(
|
||||||
|
position: HomeAssistantDeskPositionResult,
|
||||||
|
): API_HA_DeskPosition {
|
||||||
return {
|
return {
|
||||||
position: position.as_text(),
|
position: position.as_text(),
|
||||||
is_standing: position.as_boolean,
|
is_standing: position.as_boolean,
|
||||||
last_changed: position.last_changed.toReadable(true),
|
last_changed: position.last_changed.toReadable(true),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTemperatureText(): Promise<ServiceResult<string>> {
|
async getTemperatureText(): Promise<ServiceResult<string>> {
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyInstance } from "fastify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { TidalService } from "../tidal/service.js";
|
import { type HomepageService } from "./service.js";
|
||||||
import { HomeAssistantService } from "../homeassistant/service.js";
|
import { type API_HA_DeskPosition, type TidalGetCurrent } from "@dpu/shared";
|
||||||
import { HomepageService } from "./service.js";
|
|
||||||
import { API_HA_DeskPosition, TidalGetCurrent } from "@dpu/shared";
|
|
||||||
|
|
||||||
export async function homepageRoutes(
|
export async function homepageRoutes(
|
||||||
fastify: FastifyInstance,
|
fastify: FastifyInstance,
|
||||||
{
|
{
|
||||||
hpService,
|
hpService,
|
||||||
verifyAPIKey,
|
|
||||||
}: {
|
}: {
|
||||||
hpService: HomepageService
|
hpService: HomepageService;
|
||||||
verifyAPIKey: (
|
|
||||||
request: FastifyRequest,
|
|
||||||
reply: FastifyReply,
|
|
||||||
) => Promise<void>;
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
fastify.get(
|
fastify.get(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
BaseService,
|
BaseService,
|
||||||
FullInformation,
|
FullInformation,
|
||||||
HomeAssistantDeskPositionResult,
|
HomeAssistantDeskPositionResult,
|
||||||
|
SseClientChangeEvent,
|
||||||
SseService,
|
SseService,
|
||||||
TidalGetCurrent,
|
TidalGetCurrent,
|
||||||
type ServiceResult,
|
type ServiceResult,
|
||||||
@@ -14,12 +15,20 @@ export class HomepageService extends BaseService<null> {
|
|||||||
private haService: HomeAssistantService;
|
private haService: HomeAssistantService;
|
||||||
private tidalService: TidalService;
|
private tidalService: TidalService;
|
||||||
private sseService: SseService;
|
private sseService: SseService;
|
||||||
|
private pollingInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
private songEndTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private lastPoll: FullInformation | null = null;
|
||||||
|
|
||||||
constructor(haService: HomeAssistantService, tidalService: TidalService, sseService: SseService) {
|
constructor(
|
||||||
|
haService: HomeAssistantService,
|
||||||
|
tidalService: TidalService,
|
||||||
|
sseService: SseService,
|
||||||
|
) {
|
||||||
super(null);
|
super(null);
|
||||||
this.haService = haService;
|
this.haService = haService;
|
||||||
this.tidalService = tidalService;
|
this.tidalService = tidalService;
|
||||||
this.sseService = sseService;
|
this.sseService = sseService;
|
||||||
|
this.listenForClientChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFullInformation(): Promise<ServiceResult<FullInformation | string>> {
|
async getFullInformation(): Promise<ServiceResult<FullInformation | string>> {
|
||||||
@@ -27,20 +36,114 @@ export class HomepageService extends BaseService<null> {
|
|||||||
const [desk, temp, song] = await Promise.all([
|
const [desk, temp, song] = await Promise.all([
|
||||||
this.haService.getDeskPosition(),
|
this.haService.getDeskPosition(),
|
||||||
this.haService.getTemperatureText(),
|
this.haService.getTemperatureText(),
|
||||||
this.tidalService.getSong()
|
this.tidalService.getSong(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
ha_desk_position: desk.successful ? this.haService.convertPosResultToApiAnswer(desk.result as HomeAssistantDeskPositionResult) : null,
|
ha_desk_position: desk.successful
|
||||||
|
? this.haService.convertPosResultToApiAnswer(
|
||||||
|
desk.result as HomeAssistantDeskPositionResult,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
ha_temp: temp.successful ? temp.result : null,
|
ha_temp: temp.successful ? temp.result : null,
|
||||||
tidal_current: song ? song.result as TidalGetCurrent : null,
|
tidal_current: song ? (song.result as TidalGetCurrent) : null,
|
||||||
}
|
};
|
||||||
|
|
||||||
return this.getSuccessfulResult(result);
|
return this.getSuccessfulResult(this.updateLastPoll(result));
|
||||||
} catch {
|
} catch {
|
||||||
const error_message = "error getting all information";
|
const error_message = "error getting all information";
|
||||||
logWarning(error_message);
|
logWarning(error_message);
|
||||||
return this.getErrorResult(error_message);
|
return this.getErrorResult(error_message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateLastPoll(newPoll: FullInformation) {
|
||||||
|
const updates = [];
|
||||||
|
if (
|
||||||
|
this.lastPoll?.ha_desk_position?.is_standing !==
|
||||||
|
newPoll.ha_desk_position?.is_standing
|
||||||
|
) {
|
||||||
|
updates.push({
|
||||||
|
component: "ha_desk_position",
|
||||||
|
data: newPoll.ha_desk_position,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.lastPoll?.ha_temp !== newPoll.ha_temp) {
|
||||||
|
updates.push({
|
||||||
|
component: "ha_temp",
|
||||||
|
data: newPoll.ha_temp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.lastPoll?.tidal_current?.title !== newPoll.tidal_current?.title ||
|
||||||
|
this.lastPoll?.tidal_current?.status !== newPoll.tidal_current?.status
|
||||||
|
) {
|
||||||
|
updates.push({
|
||||||
|
component: "tidal_current",
|
||||||
|
data: newPoll.tidal_current,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sseService.notifyClients({
|
||||||
|
type: "update",
|
||||||
|
data: updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lastPoll = newPoll;
|
||||||
|
this.scheduleSongEndPoll(newPoll.tidal_current);
|
||||||
|
return newPoll;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleSongEndPoll(tidal: TidalGetCurrent | null): void {
|
||||||
|
this.clearSongEndPoll();
|
||||||
|
|
||||||
|
if (!tidal || tidal.status === "paused") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingSeconds = tidal.durationInSeconds - tidal.currentInSeconds;
|
||||||
|
if (remainingSeconds > 0) {
|
||||||
|
this.songEndTimeout = setTimeout(
|
||||||
|
() => {
|
||||||
|
this.songEndTimeout = null;
|
||||||
|
this.getFullInformation();
|
||||||
|
},
|
||||||
|
(remainingSeconds + 1) * 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearSongEndPoll(): void {
|
||||||
|
if (this.songEndTimeout) {
|
||||||
|
clearTimeout(this.songEndTimeout);
|
||||||
|
this.songEndTimeout = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listenForClientChange(): void {
|
||||||
|
this.sseService.onClientChange((clientChange: SseClientChangeEvent) => {
|
||||||
|
if (clientChange.clientCount === 0) {
|
||||||
|
this.stopPolling();
|
||||||
|
} else {
|
||||||
|
if (!this.pollingInterval) {
|
||||||
|
this.startPolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startPolling(): void {
|
||||||
|
this.pollingInterval = setInterval(() => {
|
||||||
|
this.getFullInformation();
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopPolling(): void {
|
||||||
|
if (this.pollingInterval) {
|
||||||
|
clearInterval(this.pollingInterval);
|
||||||
|
}
|
||||||
|
this.clearSongEndPoll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ const port = parseInt(Config.port, 10);
|
|||||||
// Register routes
|
// Register routes
|
||||||
await fastify.register(homeAssistantRoutes, { haService, verifyAPIKey });
|
await fastify.register(homeAssistantRoutes, { haService, verifyAPIKey });
|
||||||
await fastify.register(tidalRoutes, { tidalService, verifyAPIKey });
|
await fastify.register(tidalRoutes, { tidalService, verifyAPIKey });
|
||||||
await fastify.register(sseRoutes, { sseService, verifyAPIKey });
|
await fastify.register(sseRoutes, { sseService });
|
||||||
await fastify.register(homepageRoutes, { hpService, verifyAPIKey });
|
await fastify.register(homepageRoutes, { hpService });
|
||||||
|
|
||||||
fastify.get(
|
fastify.get(
|
||||||
"/ping",
|
"/ping",
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyInstance } from "fastify";
|
||||||
import { logInfo, SseEvent, SseService } from "@dpu/shared";
|
import { logInfo, type SseEvent, type SseService } from "@dpu/shared";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
export async function sseRoutes(
|
export async function sseRoutes(
|
||||||
fastify: FastifyInstance,
|
fastify: FastifyInstance,
|
||||||
{
|
{
|
||||||
sseService,
|
sseService,
|
||||||
verifyAPIKey,
|
|
||||||
}: {
|
}: {
|
||||||
sseService: SseService;
|
sseService: SseService;
|
||||||
verifyAPIKey: (
|
|
||||||
request: FastifyRequest,
|
|
||||||
reply: FastifyReply,
|
|
||||||
) => Promise<void>;
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
fastify.get(
|
fastify.get(
|
||||||
@@ -21,29 +16,29 @@ export async function sseRoutes(
|
|||||||
schema: {
|
schema: {
|
||||||
description: "Register for SSE",
|
description: "Register for SSE",
|
||||||
tags: ["sse"],
|
tags: ["sse"],
|
||||||
hide: true
|
hide: true,
|
||||||
},
|
},
|
||||||
sse: true
|
sse: true,
|
||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (_request, reply) => {
|
||||||
reply.sse.keepAlive()
|
reply.sse.keepAlive();
|
||||||
|
|
||||||
const clientId = randomUUID();
|
const clientId = randomUUID();
|
||||||
const sendEvent = (data: SseEvent) => {
|
const sendEvent = (data: SseEvent) => {
|
||||||
reply.sse.send({
|
reply.sse.send({
|
||||||
data: JSON.stringify(data)
|
data: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
sseService.addClient({ id: clientId, send: sendEvent });
|
sseService.addClient({ id: clientId, send: sendEvent });
|
||||||
|
|
||||||
await reply.sse.send({ data: 'Connected' });
|
await reply.sse.send({ data: "Connected" });
|
||||||
logInfo(`Connection for client ${clientId} established`);
|
logInfo(`Connection for client ${clientId} established`);
|
||||||
|
|
||||||
reply.sse.onClose(() => {
|
reply.sse.onClose(() => {
|
||||||
sseService.removeClient(clientId);
|
sseService.removeClient(clientId);
|
||||||
logInfo(`Connection for client ${clientId} closed`);
|
logInfo(`Connection for client ${clientId} closed`);
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user