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

@@ -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 || "",
},

View File

@@ -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<GristClient> {
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);
}
}

View File

@@ -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<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> {
try {
const response = await this.getAxios().get<HomeAssistantEntity>(

View File

@@ -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);

View File

@@ -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<HomeAssistantClient> {
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<HomeAssistantClient> {
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<HomeAssistantClient> {
};
}
async getTemperatureText(): Promise<ServiceResult<string>> {
async getTemperature(): Promise<ServiceResult<string>> {
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<HomeAssistantEntity[]> {
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);
}
}
}

View File

@@ -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<void>;
},
) {
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<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 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<null> {
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<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;
}
@@ -110,22 +108,37 @@ export class HomepageService extends BaseService<null> {
return newPoll;
}
async updatePartial(components: string[]): Promise<void> {
async updatePartial(
components: ComponentUpdate[],
): Promise<ServiceResult<string>> {
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<null> {
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<null> {
}
}
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<null> {
}
}
private updateTidal(
updateTidal(
new_tidal_current: TidalGetCurrent | null,
updates: ComponentUpdate[],
): void {
@@ -184,7 +196,7 @@ export class HomepageService extends BaseService<null> {
}
}
private updateGristPG(
updateGristPG(
new_grist_personal_goals: GristRecord_PersonalGoals | null,
updates: ComponentUpdate[],
): void {
@@ -227,10 +239,16 @@ export class HomepageService extends BaseService<null> {
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]) =>

View File

@@ -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",

View File

@@ -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<TidalClient> {
@@ -27,10 +26,8 @@ export class TidalService extends BaseService<TidalClient> {
const response = await this.getClient().get<TidalGetCurrent>("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<TidalClient> {
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<TidalClient> {
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<TidalClient> {
);
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);
}
}
}