change from poll to update from hooks

This commit is contained in:
Darius
2026-02-08 14:55:54 +01:00
parent c7faa4fc0a
commit b7daaab9cf
12 changed files with 206 additions and 133 deletions

View File

@@ -1,11 +1 @@
API_KEY=apiKey look into config.ts
PORT=
ENV_DEV=true
TIDAL_HOST=http://localhost:47836
HA_API_URL=http://homeassistant.com/api/states/
HA_API_TOKEN=Nina hätte hier jetzt ihr Token ausversehen stehen hihi
HA_ROOMTEMP_SENSOR_IDS=entityId(,separated)
HA_STANDING_WEBHOOK=webhookId
HA_DESK_SENSOR_BINARY=entityId

26
package-lock.json generated
View File

@@ -30,8 +30,8 @@
} }
}, },
"node_modules/@dpu/shared": { "node_modules/@dpu/shared": {
"version": "1.9.1", "version": "1.9.6",
"resolved": "git+https://git.dariusbag.dev/DarDarBinks/dpu-shared.git#2b85020d52e9f79956e2fde3833eef071f6b73af", "resolved": "git+https://git.dariusbag.dev/DarDarBinks/dpu-shared.git#872d3755d06c8e8c6452d8ab7212b6426d84943b",
"dependencies": { "dependencies": {
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"axios": "^1.7.9", "axios": "^1.7.9",
@@ -679,9 +679,9 @@
} }
}, },
"node_modules/@fastify/swagger": { "node_modules/@fastify/swagger": {
"version": "9.6.1", "version": "9.7.0",
"resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.6.1.tgz", "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.7.0.tgz",
"integrity": "sha512-fKlpJqFMWoi4H3EdUkDaMteEYRCfQMEkK0HJJ0eaf4aRlKd8cbq0pVkOfXDXmtvMTXYcnx3E+l023eFDBsA1HA==", "integrity": "sha512-Vp1SC1GC2Hrkd3faFILv86BzUNyFz5N4/xdExqtCgkGASOzn/x+eMe4qXIGq7cdT6wif/P/oa6r1Ruqx19paZA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -790,9 +790,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.10.11", "version": "24.10.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.11.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.12.tgz",
"integrity": "sha512-/Af7O8r1frCVgOz0I62jWUtMohJ0/ZQU/ZoketltOJPZpnb17yoNc9BSoVuV9qlaIXJiPNOpsfq4ByFajSArNQ==", "integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
@@ -872,13 +872,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.4", "version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.11",
"form-data": "^4.0.4", "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },

View File

@@ -6,7 +6,8 @@
"clean": "rimraf dist", "clean": "rimraf dist",
"build": "npm run clean && tsc", "build": "npm run clean && tsc",
"start": "node dist/index.js", "start": "node dist/index.js",
"dev": "tsx watch --inspect-brk src/index.ts" "dev": "tsx watch src/index.ts",
"devdbg": "tsx watch --inspect-brk src/index.ts"
}, },
"keywords": [], "keywords": [],
"author": "Darius", "author": "Darius",

View File

@@ -19,7 +19,7 @@ export const Config = {
api_token: process.env.HA_API_TOKEN || "", api_token: process.env.HA_API_TOKEN || "",
id_sensor_desk_binary: process.env.HA_SENSOR_DESK_BINARY || "", id_sensor_desk_binary: process.env.HA_SENSOR_DESK_BINARY || "",
id_sensors_roomtemp: process.env.HA_SENSORS_ROOMTEMP?.split(",") || [], id_sensor_roomtemp: process.env.HA_SENSOR_ROOMTEMP || "",
id_webhook_stand: process.env.HA_STANDING_WEBHOOK || "", id_webhook_stand: process.env.HA_STANDING_WEBHOOK || "",
}, },

View File

@@ -1,7 +1,6 @@
import { import {
BaseService, BaseService,
type GristRecord_PersonalGoals, type GristRecord_PersonalGoals,
logWarning,
type ServiceResult, type ServiceResult,
} from "@dpu/shared"; } from "@dpu/shared";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
@@ -25,14 +24,12 @@ export class GristService extends BaseService<GristClient> {
this.transformToPersonalGoals(response.records[0].fields), this.transformToPersonalGoals(response.records[0].fields),
); );
} else { } else {
const error_message = "error finding record from grist"; return this.getErrorResult(
logWarning(error_message); "error getting record from grist. record not found.",
return this.getErrorResult(error_message); );
} }
} catch { } catch (error) {
const error_message = "error getting record from grist"; return this.getErrorResult("error getting record from grist.", error);
logWarning(error_message);
return this.getErrorResult(error_message);
} }
} }

View File

@@ -2,16 +2,6 @@ import { BaseClient, type HomeAssistantEntity } from "@dpu/shared";
import { printNetworkError } from "@dpu/shared/dist/logger.js"; import { printNetworkError } from "@dpu/shared/dist/logger.js";
export class HomeAssistantClient extends BaseClient { export class HomeAssistantClient extends BaseClient {
async getEntityStates(entityIds: string[]): Promise<HomeAssistantEntity[]> {
try {
const promises = entityIds.map((id) => this.getEntityState(id));
return await Promise.all(promises);
} catch (error) {
printNetworkError(error);
throw error;
}
}
async getEntityState(entityId: string): Promise<HomeAssistantEntity> { async getEntityState(entityId: string): Promise<HomeAssistantEntity> {
try { try {
const response = await this.getAxios().get<HomeAssistantEntity>( const response = await this.getAxios().get<HomeAssistantEntity>(

View File

@@ -93,7 +93,7 @@ export async function homeAssistantRoutes(
}, },
}, },
async (_request, reply) => { async (_request, reply) => {
const service_result = await haService.getTemperatureText(); const service_result = await haService.getTemperature();
if (!service_result.successful) { if (!service_result.successful) {
reply.code(418); reply.code(418);

View File

@@ -5,7 +5,6 @@ import {
type HomeAssistantEntity, type HomeAssistantEntity,
type ServiceResult, type ServiceResult,
} from "@dpu/shared"; } from "@dpu/shared";
import { logWarning } from "@dpu/shared/dist/logger.js";
import { calculateSecondsBetween } from "@dpu/shared/dist/timehelper.js"; import { calculateSecondsBetween } from "@dpu/shared/dist/timehelper.js";
import { Config } from "../config.js"; import { Config } from "../config.js";
import type { HomeAssistantClient } from "./client.js"; import type { HomeAssistantClient } from "./client.js";
@@ -37,9 +36,7 @@ export class HomeAssistantService extends BaseService<HomeAssistantClient> {
return this.getSuccessfulResult(result); return this.getSuccessfulResult(result);
} catch (error) { } catch (error) {
const error_message = `error starting stand automation. ${error instanceof Error ? error.message : error}`; return this.getErrorResult("error starting stand automation.", error);
logWarning(error_message);
return this.getErrorResult(error_message);
} }
} }
@@ -51,25 +48,27 @@ export class HomeAssistantService extends BaseService<HomeAssistantClient> {
Config.homeassistant.id_sensor_desk_binary, Config.homeassistant.id_sensor_desk_binary,
); );
const position = Number(raw.state); return this.getSuccessfulResult(this.convertHaEntityToPosResult(raw));
const result = {
raw,
as_boolean: position === 1,
last_changed: calculateSecondsBetween(
new Date(raw.last_changed).getTime(),
Date.now(),
),
};
return this.getSuccessfulResult(result);
} catch (error) { } catch (error) {
const error_message = "error getting desk position"; return this.getErrorResult("error getting desk position.", error);
logWarning(error_message, error);
return this.getErrorResult(error_message);
} }
} }
convertHaEntityToPosResult(
raw: HomeAssistantEntity,
): HomeAssistantDeskPositionResult {
const position = Number(raw.state);
return {
raw,
as_boolean: position === 1,
last_changed: calculateSecondsBetween(
new Date(raw.last_changed).getTime(),
Date.now(),
),
};
}
convertPosResultToApiAnswer( convertPosResultToApiAnswer(
position: HomeAssistantDeskPositionResult, position: HomeAssistantDeskPositionResult,
): API_HA_DeskPosition { ): API_HA_DeskPosition {
@@ -80,33 +79,15 @@ export class HomeAssistantService extends BaseService<HomeAssistantClient> {
}; };
} }
async getTemperatureText(): Promise<ServiceResult<string>> { async getTemperature(): Promise<ServiceResult<string>> {
try { try {
const entities = await this.getTemperatures(); const entity = await this.getClient().getEntityState(
const values = entities Config.homeassistant.id_sensor_roomtemp,
.map((entity) => parseFloat(entity.state))
.filter((value) => !Number.isNaN(value));
const average =
values.length > 0
? values.reduce((sum, value) => sum + value, 0) / values.length
: 0;
const result = average.toFixed(2);
return this.getSuccessfulResult(result);
} catch (error) {
const error_message = "error getting temperature as text";
logWarning(error_message, error);
return this.getErrorResult(error_message);
}
}
private async getTemperatures(): Promise<HomeAssistantEntity[]> {
try {
return await this.getClient().getEntityStates(
Config.homeassistant.id_sensors_roomtemp,
); );
return this.getSuccessfulResult(entity.state);
} catch (error) { } catch (error) {
logWarning("error getting temperatures:", error); return this.getErrorResult("error getting temperature.", error);
return [];
} }
} }
} }

View File

@@ -1,18 +1,31 @@
import type { import type {
API_HA_DeskPosition, API_HA_DeskPosition,
ComponentUpdate,
GristRecord_PersonalGoals, GristRecord_PersonalGoals,
HA_Update,
HomeAssistantEntity,
TidalGetCurrent, TidalGetCurrent,
} from "@dpu/shared"; } from "@dpu/shared";
import type { FastifyInstance } from "fastify"; import type { FastifyInstance, FastifyReply } from "fastify";
import type { FastifyRequest } from "fastify/types/request.js";
import { z } from "zod"; import { z } from "zod";
import { Config } from "../config.js";
import type { HomeAssistantService } from "../homeassistant/service.js";
import type { HomepageService } from "./service.js"; import type { HomepageService } from "./service.js";
export async function homepageRoutes( export async function homepageRoutes(
fastify: FastifyInstance, fastify: FastifyInstance,
{ {
hpService, hpService,
haService,
verifyAPIKey,
}: { }: {
hpService: HomepageService; hpService: HomepageService;
haService: HomeAssistantService;
verifyAPIKey: (
request: FastifyRequest,
reply: FastifyReply,
) => Promise<void>;
}, },
) { ) {
fastify.get( fastify.get(
@@ -47,4 +60,96 @@ export async function homepageRoutes(
return service_result.result; return service_result.result;
}, },
); );
fastify.post(
"/homepage/update/homeassistant",
{
preHandler: verifyAPIKey,
schema: {
description: "Update information for component on dpu status page",
tags: ["homepage"],
body: z.custom<unknown>(),
response: {
200: z.string(),
418: z.object({
error: z.string(),
}),
},
hide: true,
},
},
async (request, reply) => {
const ha_update = request.body as HA_Update;
const ha_entity: HomeAssistantEntity = {
entity_id: ha_update.entity_id,
state: ha_update.state,
attributes: ha_update.attributes,
last_changed: ha_update.timestamp,
};
const updates: ComponentUpdate[] = [];
switch (ha_update.entity_id) {
case Config.homeassistant.id_sensor_desk_binary: {
updates.push({
component: "desk",
data: haService.convertPosResultToApiAnswer(
haService.convertHaEntityToPosResult(ha_entity),
),
});
break;
}
case Config.homeassistant.id_sensor_roomtemp:
updates.push({
component: "desk",
data: ha_entity.state,
});
break;
default:
}
const service_result = await hpService.updatePartial(updates);
if (!service_result.successful) {
reply.code(418);
return { error: service_result.result };
}
return service_result.result;
},
);
fastify.post(
"/homepage/update/tidal",
{
preHandler: verifyAPIKey,
schema: {
description: "Update information for component on dpu status page",
tags: ["homepage"],
body: z.custom<TidalGetCurrent>(),
response: {
200: z.string(),
418: z.object({
error: z.string(),
}),
},
hide: true,
},
},
async (request, reply) => {
const update = request.body as TidalGetCurrent;
const service_result = await hpService.updatePartial([
{
component: "tidal",
data: update,
},
]);
if (!service_result.successful) {
reply.code(418);
return { error: service_result.result };
}
return service_result.result;
},
);
} }

View File

@@ -9,7 +9,7 @@ import {
type TidalGetCurrent, type TidalGetCurrent,
type WsService, type WsService,
} from "@dpu/shared"; } from "@dpu/shared";
import { logInfo, logWarning } from "@dpu/shared/dist/logger.js"; import { logInfo } from "@dpu/shared/dist/logger.js";
import type { GristService } from "../grist/service"; import type { GristService } from "../grist/service";
import type { HomeAssistantService } from "../homeassistant/service"; import type { HomeAssistantService } from "../homeassistant/service";
import type { TidalService } from "../tidal/service"; import type { TidalService } from "../tidal/service";
@@ -58,10 +58,8 @@ export class HomepageService extends BaseService<null> {
grist_personal_goals: personal_goals, grist_personal_goals: personal_goals,
}), }),
); );
} catch { } catch (error) {
const error_message = "error getting all information"; return this.getErrorResult("error getting all information.", error);
logWarning(error_message);
return this.getErrorResult(error_message);
} }
} }
@@ -75,7 +73,7 @@ export class HomepageService extends BaseService<null> {
} }
private async _getTemp(): Promise<string | null> { private async _getTemp(): Promise<string | null> {
const temp = await this.haService.getTemperatureText(); const temp = await this.haService.getTemperature();
return temp.successful ? temp.result : null; return temp.successful ? temp.result : null;
} }
@@ -110,22 +108,37 @@ export class HomepageService extends BaseService<null> {
return newPoll; return newPoll;
} }
async updatePartial(components: string[]): Promise<void> { async updatePartial(
components: ComponentUpdate[],
): Promise<ServiceResult<string>> {
const updates: ComponentUpdate[] = []; const updates: ComponentUpdate[] = [];
for (const component of components) { for (const component of components) {
switch (component) { switch (component.component) {
case "desk": { case "desk": {
this.updateHaDesk(await this._getDesk(), updates); this.updateHaDesk(
(component.data as API_HA_DeskPosition) ?? (await this._getDesk()),
updates,
);
break; break;
} }
case "temp": case "temp":
this.updateHaTemp(await this._getTemp(), updates); this.updateHaTemp(
(component.data as string) ?? (await this._getTemp()),
updates,
);
break; break;
case "tidal": case "tidal":
this.updateTidal(await this._getTidal(), updates); this.updateTidal(
(component.data as TidalGetCurrent) ?? (await this._getTidal()),
updates,
);
break; break;
case "grist": case "grist":
this.updateGristPG(await this._getGristPG(), updates); this.updateGristPG(
(component.data as GristRecord_PersonalGoals) ??
(await this._getGristPG()),
updates,
);
break; break;
default: default:
} }
@@ -135,9 +148,11 @@ export class HomepageService extends BaseService<null> {
type: "update", type: "update",
data: updates, data: updates,
}); });
return this.getSuccessfulResult("update broadcasted");
} }
private updateHaDesk( updateHaDesk(
new_ha_desk_position: API_HA_DeskPosition | null, new_ha_desk_position: API_HA_DeskPosition | null,
updates: ComponentUpdate[], updates: ComponentUpdate[],
): void { ): void {
@@ -153,10 +168,7 @@ export class HomepageService extends BaseService<null> {
} }
} }
private updateHaTemp( updateHaTemp(new_ha_temp: string | null, updates: ComponentUpdate[]): void {
new_ha_temp: string | null,
updates: ComponentUpdate[],
): void {
if (this.lastPoll.ha_temp !== new_ha_temp) { if (this.lastPoll.ha_temp !== new_ha_temp) {
this.lastPoll.ha_temp = new_ha_temp; this.lastPoll.ha_temp = new_ha_temp;
updates.push({ updates.push({
@@ -166,7 +178,7 @@ export class HomepageService extends BaseService<null> {
} }
} }
private updateTidal( updateTidal(
new_tidal_current: TidalGetCurrent | null, new_tidal_current: TidalGetCurrent | null,
updates: ComponentUpdate[], updates: ComponentUpdate[],
): void { ): void {
@@ -184,7 +196,7 @@ export class HomepageService extends BaseService<null> {
} }
} }
private updateGristPG( updateGristPG(
new_grist_personal_goals: GristRecord_PersonalGoals | null, new_grist_personal_goals: GristRecord_PersonalGoals | null,
updates: ComponentUpdate[], updates: ComponentUpdate[],
): void { ): void {
@@ -227,10 +239,16 @@ export class HomepageService extends BaseService<null> {
startPolling(): void { startPolling(): void {
logInfo("Polling started"); logInfo("Polling started");
const config: [string[], number][] = [ const config: [ComponentUpdate[], number][] = [
[["tidal"], 20_000], //[[{ component: "tidal", data: null }], 20_000],
[["desk"], 60_000], //[[{ component: "desk", data: null }], 60_000],
[["temp", "grist"], 600_000], [
[
//{ component: "temp", data: null },
{ component: "grist", data: null },
],
600_000,
],
]; ];
this.pollingIntervals = config.map(([components, interval]) => this.pollingIntervals = config.map(([components, interval]) =>

View File

@@ -8,10 +8,10 @@ import type { FastifyReply, FastifyRequest } from "fastify";
import Fastify from "fastify"; import Fastify from "fastify";
import fastifyAxios from "fastify-axios"; import fastifyAxios from "fastify-axios";
import { import {
jsonSchemaTransform, jsonSchemaTransform,
serializerCompiler, serializerCompiler,
validatorCompiler, validatorCompiler,
type ZodTypeProvider, type ZodTypeProvider,
} from "fastify-type-provider-zod"; } from "fastify-type-provider-zod";
import { SwaggerTheme, SwaggerThemeNameEnum } from "swagger-themes"; import { SwaggerTheme, SwaggerThemeNameEnum } from "swagger-themes";
import { z } from "zod"; import { z } from "zod";
@@ -129,7 +129,7 @@ await fastify.register(gristRoutes, { gristService });
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(wsRoutes, { wsService }); await fastify.register(wsRoutes, { wsService });
await fastify.register(homepageRoutes, { hpService }); await fastify.register(homepageRoutes, { hpService, haService, verifyAPIKey });
fastify.get( fastify.get(
"/ping", "/ping",

View File

@@ -3,7 +3,6 @@ import {
type ServiceResult, type ServiceResult,
type TidalGetCurrent, type TidalGetCurrent,
} from "@dpu/shared"; } from "@dpu/shared";
import { logWarning } from "@dpu/shared/dist/logger.js";
import type { TidalClient } from "./client.js"; import type { TidalClient } from "./client.js";
export class TidalService extends BaseService<TidalClient> { export class TidalService extends BaseService<TidalClient> {
@@ -27,10 +26,8 @@ export class TidalService extends BaseService<TidalClient> {
const response = await this.getClient().get<TidalGetCurrent>("current"); const response = await this.getClient().get<TidalGetCurrent>("current");
return this.getSuccessfulResult(response); return this.getSuccessfulResult(response);
} catch { } catch (error) {
const error_message = "error getting song from tidal"; return this.getErrorResult("error getting song from tidal.", error);
logWarning(error_message);
return this.getErrorResult(error_message);
} }
} }
@@ -41,10 +38,8 @@ export class TidalService extends BaseService<TidalClient> {
return this.getSuccessfulResult( return this.getSuccessfulResult(
Math.round(response.volume * 100), // * 100 because it's a decimal and we want a percentage Math.round(response.volume * 100), // * 100 because it's a decimal and we want a percentage
); );
} catch { } catch (error) {
const error_message = "error getting volume from tidal"; return this.getErrorResult("error getting volume from tidal.", error);
logWarning(error_message);
return this.getErrorResult(error_message);
} }
} }
@@ -73,9 +68,7 @@ export class TidalService extends BaseService<TidalClient> {
return await this.setVolumeToTidal(clampValue); return await this.setVolumeToTidal(clampValue);
} }
const error_message = "error parsing volume to set"; return this.getErrorResult("error parsing volume to set.");
logWarning(error_message);
return this.getErrorResult(error_message);
} }
async setVolumeToTidal( async setVolumeToTidal(
@@ -88,10 +81,8 @@ export class TidalService extends BaseService<TidalClient> {
); );
return this.getSuccessfulResult(Math.round(volume)); return this.getSuccessfulResult(Math.round(volume));
} catch { } catch (error) {
const error_message = "error setting volume from tidal"; return this.getErrorResult("error setting volume from tidal.", error);
logWarning(error_message);
return this.getErrorResult(error_message);
} }
} }
} }