diff --git a/.env.example b/.env.example index 1747329..cc3ebd0 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1 @@ -API_KEY=apiKey -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 +look into config.ts diff --git a/package-lock.json b/package-lock.json index d1a0bdb..210e323 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,8 +30,8 @@ } }, "node_modules/@dpu/shared": { - "version": "1.9.1", - "resolved": "git+https://git.dariusbag.dev/DarDarBinks/dpu-shared.git#2b85020d52e9f79956e2fde3833eef071f6b73af", + "version": "1.9.6", + "resolved": "git+https://git.dariusbag.dev/DarDarBinks/dpu-shared.git#872d3755d06c8e8c6452d8ab7212b6426d84943b", "dependencies": { "@types/ws": "^8.18.1", "axios": "^1.7.9", @@ -679,9 +679,9 @@ } }, "node_modules/@fastify/swagger": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.6.1.tgz", - "integrity": "sha512-fKlpJqFMWoi4H3EdUkDaMteEYRCfQMEkK0HJJ0eaf4aRlKd8cbq0pVkOfXDXmtvMTXYcnx3E+l023eFDBsA1HA==", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.7.0.tgz", + "integrity": "sha512-Vp1SC1GC2Hrkd3faFILv86BzUNyFz5N4/xdExqtCgkGASOzn/x+eMe4qXIGq7cdT6wif/P/oa6r1Ruqx19paZA==", "funding": [ { "type": "github", @@ -790,9 +790,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.11.tgz", - "integrity": "sha512-/Af7O8r1frCVgOz0I62jWUtMohJ0/ZQU/ZoketltOJPZpnb17yoNc9BSoVuV9qlaIXJiPNOpsfq4ByFajSArNQ==", + "version": "24.10.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.12.tgz", + "integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==", "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -872,13 +872,13 @@ } }, "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, diff --git a/package.json b/package.json index 235a070..ad9854e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "clean": "rimraf dist", "build": "npm run clean && tsc", "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": [], "author": "Darius", diff --git a/src/config.ts b/src/config.ts index e254d71..166763e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -19,7 +19,7 @@ export const Config = { api_token: process.env.HA_API_TOKEN || "", 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 || "", }, diff --git a/src/grist/service.ts b/src/grist/service.ts index 00b8945..4db2bc9 100644 --- a/src/grist/service.ts +++ b/src/grist/service.ts @@ -1,7 +1,6 @@ import { BaseService, type GristRecord_PersonalGoals, - logWarning, type ServiceResult, } from "@dpu/shared"; import { DateTime } from "luxon"; @@ -25,14 +24,12 @@ export class GristService extends BaseService { this.transformToPersonalGoals(response.records[0].fields), ); } else { - const error_message = "error finding record from grist"; - logWarning(error_message); - return this.getErrorResult(error_message); + return this.getErrorResult( + "error getting record from grist. record not found.", + ); } - } catch { - const error_message = "error getting record from grist"; - logWarning(error_message); - return this.getErrorResult(error_message); + } catch (error) { + return this.getErrorResult("error getting record from grist.", error); } } diff --git a/src/homeassistant/client.ts b/src/homeassistant/client.ts index 26cc345..588bef4 100644 --- a/src/homeassistant/client.ts +++ b/src/homeassistant/client.ts @@ -2,16 +2,6 @@ import { BaseClient, type HomeAssistantEntity } from "@dpu/shared"; import { printNetworkError } from "@dpu/shared/dist/logger.js"; export class HomeAssistantClient extends BaseClient { - async getEntityStates(entityIds: string[]): Promise { - 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 { try { const response = await this.getAxios().get( diff --git a/src/homeassistant/routes.ts b/src/homeassistant/routes.ts index 4f6c93f..6e14e5b 100644 --- a/src/homeassistant/routes.ts +++ b/src/homeassistant/routes.ts @@ -93,7 +93,7 @@ export async function homeAssistantRoutes( }, }, async (_request, reply) => { - const service_result = await haService.getTemperatureText(); + const service_result = await haService.getTemperature(); if (!service_result.successful) { reply.code(418); diff --git a/src/homeassistant/service.ts b/src/homeassistant/service.ts index 6c9242d..55a850b 100644 --- a/src/homeassistant/service.ts +++ b/src/homeassistant/service.ts @@ -5,7 +5,6 @@ import { type HomeAssistantEntity, type ServiceResult, } from "@dpu/shared"; -import { logWarning } from "@dpu/shared/dist/logger.js"; import { calculateSecondsBetween } from "@dpu/shared/dist/timehelper.js"; import { Config } from "../config.js"; import type { HomeAssistantClient } from "./client.js"; @@ -37,9 +36,7 @@ export class HomeAssistantService extends BaseService { return this.getSuccessfulResult(result); } catch (error) { - const error_message = `error starting stand automation. ${error instanceof Error ? error.message : error}`; - logWarning(error_message); - return this.getErrorResult(error_message); + return this.getErrorResult("error starting stand automation.", error); } } @@ -51,25 +48,27 @@ export class HomeAssistantService extends BaseService { Config.homeassistant.id_sensor_desk_binary, ); - const position = Number(raw.state); - - const result = { - raw, - as_boolean: position === 1, - last_changed: calculateSecondsBetween( - new Date(raw.last_changed).getTime(), - Date.now(), - ), - }; - - return this.getSuccessfulResult(result); + return this.getSuccessfulResult(this.convertHaEntityToPosResult(raw)); } catch (error) { - const error_message = "error getting desk position"; - logWarning(error_message, error); - return this.getErrorResult(error_message); + return this.getErrorResult("error getting desk position.", error); } } + 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( position: HomeAssistantDeskPositionResult, ): API_HA_DeskPosition { @@ -80,33 +79,15 @@ export class HomeAssistantService extends BaseService { }; } - async getTemperatureText(): Promise> { + async getTemperature(): Promise> { try { - const entities = await this.getTemperatures(); - const values = entities - .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 { - try { - return await this.getClient().getEntityStates( - Config.homeassistant.id_sensors_roomtemp, + const entity = await this.getClient().getEntityState( + Config.homeassistant.id_sensor_roomtemp, ); + + return this.getSuccessfulResult(entity.state); } catch (error) { - logWarning("error getting temperatures:", error); - return []; + return this.getErrorResult("error getting temperature.", error); } } } diff --git a/src/homepage/routes.ts b/src/homepage/routes.ts index 97e7185..86866cc 100644 --- a/src/homepage/routes.ts +++ b/src/homepage/routes.ts @@ -1,18 +1,31 @@ import type { API_HA_DeskPosition, + ComponentUpdate, GristRecord_PersonalGoals, + HA_Update, + HomeAssistantEntity, TidalGetCurrent, } 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 { Config } from "../config.js"; +import type { HomeAssistantService } from "../homeassistant/service.js"; import type { HomepageService } from "./service.js"; export async function homepageRoutes( fastify: FastifyInstance, { hpService, + haService, + verifyAPIKey, }: { hpService: HomepageService; + haService: HomeAssistantService; + verifyAPIKey: ( + request: FastifyRequest, + reply: FastifyReply, + ) => Promise; }, ) { fastify.get( @@ -47,4 +60,96 @@ export async function homepageRoutes( 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(), + 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(), + 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; + }, + ); } diff --git a/src/homepage/service.ts b/src/homepage/service.ts index 3976f00..a2025fa 100644 --- a/src/homepage/service.ts +++ b/src/homepage/service.ts @@ -9,7 +9,7 @@ import { type TidalGetCurrent, type WsService, } 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 { HomeAssistantService } from "../homeassistant/service"; import type { TidalService } from "../tidal/service"; @@ -58,10 +58,8 @@ export class HomepageService extends BaseService { grist_personal_goals: personal_goals, }), ); - } catch { - const error_message = "error getting all information"; - logWarning(error_message); - return this.getErrorResult(error_message); + } catch (error) { + return this.getErrorResult("error getting all information.", error); } } @@ -75,7 +73,7 @@ export class HomepageService extends BaseService { } private async _getTemp(): Promise { - const temp = await this.haService.getTemperatureText(); + const temp = await this.haService.getTemperature(); return temp.successful ? temp.result : null; } @@ -110,22 +108,37 @@ export class HomepageService extends BaseService { return newPoll; } - async updatePartial(components: string[]): Promise { + async updatePartial( + components: ComponentUpdate[], + ): Promise> { const updates: ComponentUpdate[] = []; for (const component of components) { - switch (component) { + switch (component.component) { case "desk": { - this.updateHaDesk(await this._getDesk(), updates); + this.updateHaDesk( + (component.data as API_HA_DeskPosition) ?? (await this._getDesk()), + updates, + ); break; } case "temp": - this.updateHaTemp(await this._getTemp(), updates); + this.updateHaTemp( + (component.data as string) ?? (await this._getTemp()), + updates, + ); break; case "tidal": - this.updateTidal(await this._getTidal(), updates); + this.updateTidal( + (component.data as TidalGetCurrent) ?? (await this._getTidal()), + updates, + ); break; case "grist": - this.updateGristPG(await this._getGristPG(), updates); + this.updateGristPG( + (component.data as GristRecord_PersonalGoals) ?? + (await this._getGristPG()), + updates, + ); break; default: } @@ -135,9 +148,11 @@ export class HomepageService extends BaseService { type: "update", data: updates, }); + + return this.getSuccessfulResult("update broadcasted"); } - private updateHaDesk( + updateHaDesk( new_ha_desk_position: API_HA_DeskPosition | null, updates: ComponentUpdate[], ): void { @@ -153,10 +168,7 @@ export class HomepageService extends BaseService { } } - private updateHaTemp( - new_ha_temp: string | null, - updates: ComponentUpdate[], - ): void { + updateHaTemp(new_ha_temp: string | null, updates: ComponentUpdate[]): void { if (this.lastPoll.ha_temp !== new_ha_temp) { this.lastPoll.ha_temp = new_ha_temp; updates.push({ @@ -166,7 +178,7 @@ export class HomepageService extends BaseService { } } - private updateTidal( + updateTidal( new_tidal_current: TidalGetCurrent | null, updates: ComponentUpdate[], ): void { @@ -184,7 +196,7 @@ export class HomepageService extends BaseService { } } - private updateGristPG( + updateGristPG( new_grist_personal_goals: GristRecord_PersonalGoals | null, updates: ComponentUpdate[], ): void { @@ -227,10 +239,16 @@ export class HomepageService extends BaseService { startPolling(): void { logInfo("Polling started"); - const config: [string[], number][] = [ - [["tidal"], 20_000], - [["desk"], 60_000], - [["temp", "grist"], 600_000], + const config: [ComponentUpdate[], number][] = [ + //[[{ component: "tidal", data: null }], 20_000], + //[[{ component: "desk", data: null }], 60_000], + [ + [ + //{ component: "temp", data: null }, + { component: "grist", data: null }, + ], + 600_000, + ], ]; this.pollingIntervals = config.map(([components, interval]) => diff --git a/src/index.ts b/src/index.ts index 8d03a30..7f4fe35 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,10 +8,10 @@ import type { FastifyReply, FastifyRequest } from "fastify"; import Fastify from "fastify"; import fastifyAxios from "fastify-axios"; import { - jsonSchemaTransform, - serializerCompiler, - validatorCompiler, - type ZodTypeProvider, + jsonSchemaTransform, + serializerCompiler, + validatorCompiler, + type ZodTypeProvider, } from "fastify-type-provider-zod"; import { SwaggerTheme, SwaggerThemeNameEnum } from "swagger-themes"; import { z } from "zod"; @@ -129,7 +129,7 @@ await fastify.register(gristRoutes, { gristService }); await fastify.register(homeAssistantRoutes, { haService, verifyAPIKey }); await fastify.register(tidalRoutes, { tidalService, verifyAPIKey }); await fastify.register(wsRoutes, { wsService }); -await fastify.register(homepageRoutes, { hpService }); +await fastify.register(homepageRoutes, { hpService, haService, verifyAPIKey }); fastify.get( "/ping", diff --git a/src/tidal/service.ts b/src/tidal/service.ts index a1127d3..b68769b 100644 --- a/src/tidal/service.ts +++ b/src/tidal/service.ts @@ -3,7 +3,6 @@ import { type ServiceResult, type TidalGetCurrent, } from "@dpu/shared"; -import { logWarning } from "@dpu/shared/dist/logger.js"; import type { TidalClient } from "./client.js"; export class TidalService extends BaseService { @@ -27,10 +26,8 @@ export class TidalService extends BaseService { const response = await this.getClient().get("current"); return this.getSuccessfulResult(response); - } catch { - const error_message = "error getting song from tidal"; - logWarning(error_message); - return this.getErrorResult(error_message); + } catch (error) { + return this.getErrorResult("error getting song from tidal.", error); } } @@ -41,10 +38,8 @@ export class TidalService extends BaseService { return this.getSuccessfulResult( Math.round(response.volume * 100), // * 100 because it's a decimal and we want a percentage ); - } catch { - const error_message = "error getting volume from tidal"; - logWarning(error_message); - return this.getErrorResult(error_message); + } catch (error) { + return this.getErrorResult("error getting volume from tidal.", error); } } @@ -73,9 +68,7 @@ export class TidalService extends BaseService { return await this.setVolumeToTidal(clampValue); } - const error_message = "error parsing volume to set"; - logWarning(error_message); - return this.getErrorResult(error_message); + return this.getErrorResult("error parsing volume to set."); } async setVolumeToTidal( @@ -88,10 +81,8 @@ export class TidalService extends BaseService { ); return this.getSuccessfulResult(Math.round(volume)); - } catch { - const error_message = "error setting volume from tidal"; - logWarning(error_message); - return this.getErrorResult(error_message); + } catch (error) { + return this.getErrorResult("error setting volume from tidal.", error); } } }