Compare commits
39 Commits
07719bbc1f
...
feature/ga
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2749ebef2b | ||
|
|
2dd3805a54 | ||
|
|
0e7abb9113 | ||
|
|
ba74eb0deb | ||
|
|
4a3a994359 | ||
|
|
908350f696 | ||
|
|
37d3e2ede4 | ||
|
|
b938068b61 | ||
|
|
772a54aca8 | ||
|
|
c98304fe8e | ||
|
|
ff66061c32 | ||
|
|
92ff251021 | ||
|
|
ff87c3b5ce | ||
|
|
12e7611267 | ||
|
|
b7daaab9cf | ||
|
|
c7faa4fc0a | ||
|
|
9a9b99c84c | ||
|
|
d5b178fabe | ||
|
|
bef511ea72 | ||
|
|
26256276d9 | ||
|
|
f027818046 | ||
|
|
008eda6f71 | ||
|
|
8948470baa | ||
|
|
d1d0ff55a5 | ||
|
|
e1ef661a7a | ||
|
|
a8280ce27f | ||
|
|
350d7dee3e | ||
|
|
ef7752c680 | ||
|
|
b9a494a87c | ||
|
|
5866216e36 | ||
|
|
18708add17 | ||
|
|
8670a9ab71 | ||
|
|
0cdeec5eef | ||
|
|
57bbae2919 | ||
|
|
34fee498eb | ||
|
|
7ef755f120 | ||
|
|
40c8f144b5 | ||
|
|
419ec65cc6 | ||
|
|
105fb3aecb |
13
.env.example
13
.env.example
@@ -1,12 +1 @@
|
|||||||
API_KEY=apiKey
|
look into config.ts
|
||||||
PORT=
|
|
||||||
ENV_DEV=true
|
|
||||||
|
|
||||||
TIDAL_HOST=http://localhost
|
|
||||||
TIDAL_PORT=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
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,3 +25,6 @@ yarn-error.log*
|
|||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# Gadgetbridge DB
|
||||||
|
/src/gadgetbridge/db/*
|
||||||
|
|||||||
834
package-lock.json
generated
834
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -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 src/index.ts"
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"devdbg": "tsx watch --inspect-brk src/index.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Darius",
|
"author": "Darius",
|
||||||
@@ -14,18 +15,25 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dpu/shared": "git+https://git.dariusbag.dev/DarDarBinks/dpu-shared.git",
|
"@dpu/shared": "git+https://git.dariusbag.dev/DarDarBinks/dpu-shared.git",
|
||||||
"@fastify/sse": "^0.4.0",
|
"@fastify/cors": "^11.2.0",
|
||||||
"@fastify/swagger": "^9.6.1",
|
"@fastify/swagger": "^9.6.1",
|
||||||
"@fastify/swagger-ui": "^5.2.3",
|
"@fastify/swagger-ui": "^5.2.3",
|
||||||
|
"@fastify/websocket": "^11.2.0",
|
||||||
|
"better-sqlite3": "^12.6.2",
|
||||||
"dotenv": "^17.2.2",
|
"dotenv": "^17.2.2",
|
||||||
"fastify": "^5.6.2",
|
"fastify": "^5.6.2",
|
||||||
"fastify-axios": "^1.3.0",
|
"fastify-axios": "^1.3.0",
|
||||||
"fastify-type-provider-zod": "^6.1.0",
|
"fastify-type-provider-zod": "^6.1.0",
|
||||||
|
"luxon": "^3.7.2",
|
||||||
"swagger-themes": "^1.4.3",
|
"swagger-themes": "^1.4.3",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"@types/luxon": "^3.7.1",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"openapi-types": "^12.1.3",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,32 @@ dotenv.config();
|
|||||||
export const Config = {
|
export const Config = {
|
||||||
api_key: process.env.API_KEY,
|
api_key: process.env.API_KEY,
|
||||||
port: process.env.PORT || "8080",
|
port: process.env.PORT || "8080",
|
||||||
env_dev: process.env.ENV_DEV || false,
|
|
||||||
|
|
||||||
tidal: {
|
gadgetbridge: {
|
||||||
host: process.env.TIDAL_HOST || "",
|
db_path:
|
||||||
port: process.env.TIDAL_PORT || "",
|
process.env.GADGETBRIDGE_DB_PATH || "src/gadgetbridge/db/Gadgetbridge.db",
|
||||||
|
old_db_path:
|
||||||
|
process.env.GADGETBRIDGE_OLD_DB_PATH || "src/gadgetbridge/db/OldSteps.db",
|
||||||
|
},
|
||||||
|
|
||||||
|
grist: {
|
||||||
|
api_url: process.env.GRIST_API_URL || "",
|
||||||
|
api_token: process.env.GRIST_API_TOKEN || "",
|
||||||
|
|
||||||
|
table_personal_goals_path: process.env.GRIST_TPG_PATH || "",
|
||||||
},
|
},
|
||||||
|
|
||||||
homeassistant: {
|
homeassistant: {
|
||||||
api_url: process.env.HA_API_URL || "",
|
api_url: process.env.HA_API_URL || "",
|
||||||
api_token: process.env.HA_API_TOKEN || "",
|
api_token: process.env.HA_API_TOKEN || "",
|
||||||
|
|
||||||
id_desk_sensor_binary: process.env.HA_DESK_SENSOR_BINARY || "",
|
id_sensor_desk_binary: process.env.HA_SENSOR_DESK_BINARY || "",
|
||||||
id_room_sensors: process.env.HA_ROOMTEMP_SENSOR_IDS?.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 || "",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
tidal: {
|
||||||
|
address: process.env.TIDAL_ADDRESS || "",
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
57
src/gadgetbridge/client.ts
Normal file
57
src/gadgetbridge/client.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { watchFile } from "node:fs";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
export type StepRow = {
|
||||||
|
date: string;
|
||||||
|
steps: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GadgetbridgeClient {
|
||||||
|
private db: Database.Database;
|
||||||
|
private readonly dbPath: string;
|
||||||
|
private readonly oldDbPath: string;
|
||||||
|
|
||||||
|
constructor(dbPath: string, oldDbPath: string) {
|
||||||
|
this.dbPath = dbPath;
|
||||||
|
this.oldDbPath = oldDbPath;
|
||||||
|
this.db = this.openDb();
|
||||||
|
watchFile(dbPath, { interval: 20 * 60 * 1000 }, () => {
|
||||||
|
try {
|
||||||
|
this.db.close();
|
||||||
|
} catch {}
|
||||||
|
this.db = this.openDb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private openDb(): Database.Database {
|
||||||
|
const db = new Database(this.dbPath, { readonly: true });
|
||||||
|
db.exec(`ATTACH DATABASE '${this.oldDbPath}' AS old`);
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStepsPerDay(fromTimestamp: number, toTimestamp: number): StepRow[] {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
SELECT date, SUM(steps) AS steps
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
DATE(TIMESTAMP, 'unixepoch', 'localtime') AS date,
|
||||||
|
STEPS AS steps
|
||||||
|
FROM HUAMI_EXTENDED_ACTIVITY_SAMPLE
|
||||||
|
WHERE TIMESTAMP >= ? AND TIMESTAMP < ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT date, steps
|
||||||
|
FROM old.steps
|
||||||
|
WHERE date >= DATE(?, 'unixepoch', 'localtime')
|
||||||
|
AND date < DATE(?, 'unixepoch', 'localtime')
|
||||||
|
)
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date
|
||||||
|
`);
|
||||||
|
return stmt.all(
|
||||||
|
fromTimestamp,
|
||||||
|
toTimestamp,
|
||||||
|
fromTimestamp,
|
||||||
|
toTimestamp,
|
||||||
|
) as StepRow[];
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/gadgetbridge/private-routes.ts
Normal file
53
src/gadgetbridge/private-routes.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import { z } from "zod";
|
||||||
|
import type { GadgetbridgeService } from "./service.js";
|
||||||
|
|
||||||
|
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
|
||||||
|
export async function privateGadgetbridgeRoutes(
|
||||||
|
fastify: FastifyInstance,
|
||||||
|
{ gadgetbridgeService }: { gadgetbridgeService: GadgetbridgeService },
|
||||||
|
) {
|
||||||
|
fastify.get(
|
||||||
|
"/gadgetbridge/steps",
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
description: "Get steps per day for a timespan",
|
||||||
|
tags: ["gadgetbridge"],
|
||||||
|
querystring: z.object({
|
||||||
|
from: z.string().regex(dateRegex, "Must be YYYY-MM-DD"),
|
||||||
|
to: z.string().regex(dateRegex, "Must be YYYY-MM-DD"),
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.array(
|
||||||
|
z.object({
|
||||||
|
date: z.string(),
|
||||||
|
steps: z.number(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
418: z.object({
|
||||||
|
error: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
|
const { from, to } = request.query as any;
|
||||||
|
|
||||||
|
const fromDate = new Date(`${from}T00:00:00`);
|
||||||
|
const toDate = new Date(`${to}T00:00:00`);
|
||||||
|
|
||||||
|
const service_result = gadgetbridgeService.getStepsForTimespan(
|
||||||
|
fromDate,
|
||||||
|
toDate,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!service_result.successful) {
|
||||||
|
reply.code(418);
|
||||||
|
return { error: service_result.result };
|
||||||
|
}
|
||||||
|
|
||||||
|
return service_result.result;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/gadgetbridge/service.ts
Normal file
23
src/gadgetbridge/service.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { BaseService, type ServiceResult } from "@dpu/shared";
|
||||||
|
import type { GadgetbridgeClient, StepRow } from "./client.js";
|
||||||
|
|
||||||
|
export class GadgetbridgeService extends BaseService<GadgetbridgeClient> {
|
||||||
|
getStepsForTimespan(
|
||||||
|
from: Date,
|
||||||
|
to: Date,
|
||||||
|
): ServiceResult<StepRow[] | string> {
|
||||||
|
try {
|
||||||
|
const fromTs = Math.floor(from.getTime() / 1000);
|
||||||
|
// Add one day to make `to` inclusive
|
||||||
|
const toTs = Math.floor(to.getTime() / 1000) + 86400;
|
||||||
|
|
||||||
|
const rows = this.getClient().getStepsPerDay(fromTs, toTs);
|
||||||
|
return this.getSuccessfulResult(rows);
|
||||||
|
} catch (error) {
|
||||||
|
return this.getErrorResult(
|
||||||
|
"error getting steps from gadgetbridge.",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/grist/client.ts
Normal file
15
src/grist/client.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { BaseClient } from "@dpu/shared";
|
||||||
|
import { printNetworkError } from "@dpu/shared/dist/logger.js";
|
||||||
|
|
||||||
|
export class GristClient extends BaseClient {
|
||||||
|
async get<T>(endpoint: string): Promise<T> {
|
||||||
|
try {
|
||||||
|
const response = await this.getAxios().get<T>(`${endpoint}`);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
printNetworkError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/grist/private-routes.ts
Normal file
39
src/grist/private-routes.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { GristRecord_PersonalGoals } from "@dpu/shared";
|
||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import { z } from "zod";
|
||||||
|
import type { GristService } from "./service";
|
||||||
|
|
||||||
|
export async function privateGristRoutes(
|
||||||
|
fastify: FastifyInstance,
|
||||||
|
{
|
||||||
|
gristService,
|
||||||
|
}: {
|
||||||
|
gristService: GristService;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
fastify.get(
|
||||||
|
"/grist/today",
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
description: "Get goals for today",
|
||||||
|
tags: ["grist"],
|
||||||
|
response: {
|
||||||
|
200: z.custom<GristRecord_PersonalGoals>(),
|
||||||
|
418: z.object({
|
||||||
|
error: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (_request, reply) => {
|
||||||
|
const service_result = await gristService.getToday();
|
||||||
|
|
||||||
|
if (!service_result.successful) {
|
||||||
|
reply.code(418);
|
||||||
|
return { error: service_result.result };
|
||||||
|
}
|
||||||
|
|
||||||
|
return service_result.result;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
64
src/grist/service.ts
Normal file
64
src/grist/service.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
BaseService,
|
||||||
|
type GristRecord_PersonalGoals,
|
||||||
|
type ServiceResult,
|
||||||
|
} from "@dpu/shared";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
import { Config } from "../config.js";
|
||||||
|
import type { GristClient } from "./client.js";
|
||||||
|
|
||||||
|
export class GristService extends BaseService<GristClient> {
|
||||||
|
async getToday(): Promise<ServiceResult<GristRecord_PersonalGoals | string>> {
|
||||||
|
try {
|
||||||
|
const filter_string = encodeURIComponent(
|
||||||
|
`{ "id": [${this.getTodayAsId()}]}`,
|
||||||
|
);
|
||||||
|
const query = `${Config.grist.table_personal_goals_path}?filter=${filter_string}`;
|
||||||
|
|
||||||
|
const response = await this.getClient().get<{
|
||||||
|
records: Record<string, Record<string, unknown>>[];
|
||||||
|
}>(query);
|
||||||
|
|
||||||
|
if (response.records?.length > 0) {
|
||||||
|
return this.getSuccessfulResult(
|
||||||
|
this.transformToPersonalGoals(response.records[0].fields),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.getErrorResult(
|
||||||
|
"error getting record from grist. record not found.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return this.getErrorResult("error getting record from grist.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTodayAsId() {
|
||||||
|
const now = DateTime.now().setZone("Europe/Berlin");
|
||||||
|
const start = DateTime.fromObject(
|
||||||
|
{ year: now.year, month: 1, day: 1, hour: 0, minute: 0 },
|
||||||
|
{ zone: "Europe/Berlin" },
|
||||||
|
);
|
||||||
|
const diff = now.diff(start).toMillis();
|
||||||
|
return Math.ceil(diff / 86400000);
|
||||||
|
}
|
||||||
|
|
||||||
|
transformToPersonalGoals(
|
||||||
|
obj: Record<string, unknown>,
|
||||||
|
): GristRecord_PersonalGoals {
|
||||||
|
return {
|
||||||
|
went_outside: (obj.went_outside as boolean) ?? false,
|
||||||
|
standing: (obj.standing as boolean) ?? false,
|
||||||
|
standing_goal: (obj.standing_goal as number) ?? 0,
|
||||||
|
steps: (obj.steps as boolean) ?? false,
|
||||||
|
steps_goal: (obj.steps_goal as number) ?? 0,
|
||||||
|
pushups: (obj.pushups as boolean) ?? false,
|
||||||
|
squats: (obj.squats as boolean) ?? false,
|
||||||
|
leg_raises: (obj.leg_raises as boolean) ?? false,
|
||||||
|
reps_goal: (obj.reps_goal as number) ?? 0,
|
||||||
|
stairs: (obj.stairs as boolean) ?? false,
|
||||||
|
stairs_goal: (obj.stairs_goal as number) ?? 0,
|
||||||
|
is_workday: (obj.WeekdayHelper as number) === 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { HomeAssistantDeskPositionResult } from "@dpu/shared";
|
import type { HomeAssistantDeskPositionResult } from "@dpu/shared";
|
||||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { HomeAssistantService } from "../homeassistant/service.js";
|
import type { HomeAssistantService } from "./service.js";
|
||||||
|
|
||||||
export async function homeAssistantRoutes(
|
export async function privateHomeAssistantRoutes(
|
||||||
fastify: FastifyInstance,
|
fastify: FastifyInstance,
|
||||||
{
|
{
|
||||||
haService,
|
haService,
|
||||||
@@ -24,7 +24,6 @@ export async function homeAssistantRoutes(
|
|||||||
tags: ["homeassistant"],
|
tags: ["homeassistant"],
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
position: z.string(),
|
|
||||||
is_standing: z.boolean(),
|
is_standing: z.boolean(),
|
||||||
last_changed: z.string(),
|
last_changed: z.string(),
|
||||||
last_changed_seconds: z.number(),
|
last_changed_seconds: z.number(),
|
||||||
@@ -94,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);
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,71 +45,49 @@ export class HomeAssistantService extends BaseService<HomeAssistantClient> {
|
|||||||
> {
|
> {
|
||||||
try {
|
try {
|
||||||
const raw = await this.getClient().getEntityState(
|
const raw = await this.getClient().getEntityState(
|
||||||
Config.homeassistant.id_desk_sensor_binary,
|
Config.homeassistant.id_sensor_desk_binary,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return this.getSuccessfulResult(this.convertHaEntityToPosResult(raw));
|
||||||
|
} catch (error) {
|
||||||
|
return this.getErrorResult("error getting desk position.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
convertHaEntityToPosResult(
|
||||||
|
raw: HomeAssistantEntity,
|
||||||
|
): HomeAssistantDeskPositionResult {
|
||||||
const position = Number(raw.state);
|
const position = Number(raw.state);
|
||||||
|
|
||||||
const result = {
|
return {
|
||||||
raw,
|
raw,
|
||||||
as_boolean: position === 1,
|
as_boolean: position === 1,
|
||||||
as_text: () => {
|
|
||||||
if (position === 1) return "standing";
|
|
||||||
if (position === 0) return "sitting";
|
|
||||||
return "unknown";
|
|
||||||
},
|
|
||||||
last_changed: calculateSecondsBetween(
|
last_changed: calculateSecondsBetween(
|
||||||
new Date(raw.last_changed).getTime(),
|
new Date(raw.last_changed).getTime(),
|
||||||
Date.now(),
|
Date.now(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.getSuccessfulResult(result);
|
|
||||||
} catch (error) {
|
|
||||||
const error_message = "error getting desk position";
|
|
||||||
logWarning(error_message, error);
|
|
||||||
return this.getErrorResult(error_message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
convertPosResultToApiAnswer(
|
convertPosResultToApiAnswer(
|
||||||
position: HomeAssistantDeskPositionResult,
|
position: HomeAssistantDeskPositionResult,
|
||||||
): API_HA_DeskPosition {
|
): API_HA_DeskPosition {
|
||||||
return {
|
return {
|
||||||
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),
|
||||||
last_changed_seconds: position.last_changed.seconds,
|
last_changed_seconds: position.last_changed.seconds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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_room_sensors,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return this.getSuccessfulResult(entity.state);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logWarning("error getting temperatures:", error);
|
return this.getErrorResult("error getting temperature.", error);
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
164
src/homepage/private-routes.ts
Normal file
164
src/homepage/private-routes.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import {
|
||||||
|
type ComponentUpdate,
|
||||||
|
type GristRecord_PersonalGoals,
|
||||||
|
type HA_Update,
|
||||||
|
type HomeAssistantEntity,
|
||||||
|
StatusComponent,
|
||||||
|
type TidalGetCurrent,
|
||||||
|
} from "@dpu/shared";
|
||||||
|
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 { GristService } from "../grist/service.js";
|
||||||
|
import type { HomeAssistantService } from "../homeassistant/service.js";
|
||||||
|
import type { HomepageService } from "./service.js";
|
||||||
|
|
||||||
|
export async function privateHomepageRoutes(
|
||||||
|
fastify: FastifyInstance,
|
||||||
|
{
|
||||||
|
hpService,
|
||||||
|
gristService,
|
||||||
|
haService,
|
||||||
|
verifyAPIKey,
|
||||||
|
}: {
|
||||||
|
hpService: HomepageService;
|
||||||
|
gristService: GristService;
|
||||||
|
haService: HomeAssistantService;
|
||||||
|
verifyAPIKey: (
|
||||||
|
request: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) => Promise<void>;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
fastify.post(
|
||||||
|
"/homepage/update/grist",
|
||||||
|
{
|
||||||
|
preHandler: verifyAPIKey,
|
||||||
|
schema: {
|
||||||
|
description: "Update information for component on dpu status page",
|
||||||
|
tags: ["homepage"],
|
||||||
|
body: z.custom<GristRecord_PersonalGoals>(),
|
||||||
|
response: {
|
||||||
|
200: z.string(),
|
||||||
|
418: z.object({
|
||||||
|
error: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
hide: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
|
const updatedRecords = request.body as Record<string, unknown>[];
|
||||||
|
const record = updatedRecords[0];
|
||||||
|
if (record.id !== gristService.getTodayAsId()) {
|
||||||
|
return "no processing done. id not from today.";
|
||||||
|
}
|
||||||
|
const service_result = await hpService.updatePartial([
|
||||||
|
{
|
||||||
|
component: StatusComponent.GRIST_PERSONAL_GOALS,
|
||||||
|
data: gristService.transformToPersonalGoals(record),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!service_result.successful) {
|
||||||
|
reply.code(418);
|
||||||
|
return { error: 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: StatusComponent.HA_DESK_POSITION,
|
||||||
|
data: haService.convertPosResultToApiAnswer(
|
||||||
|
haService.convertHaEntityToPosResult(ha_entity),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Config.homeassistant.id_sensor_roomtemp:
|
||||||
|
updates.push({
|
||||||
|
component: StatusComponent.HA_TEMP,
|
||||||
|
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: StatusComponent.TIDAL_CURRENT,
|
||||||
|
data: update,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!service_result.successful) {
|
||||||
|
reply.code(418);
|
||||||
|
return { error: service_result.result };
|
||||||
|
}
|
||||||
|
|
||||||
|
return service_result.result;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import type { FastifyInstance } from "fastify";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { type HomepageService } from "./service.js";
|
|
||||||
import { type API_HA_DeskPosition, type TidalGetCurrent } from "@dpu/shared";
|
|
||||||
|
|
||||||
export async function homepageRoutes(
|
|
||||||
fastify: FastifyInstance,
|
|
||||||
{
|
|
||||||
hpService,
|
|
||||||
}: {
|
|
||||||
hpService: HomepageService;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
fastify.get(
|
|
||||||
"/homepage/status",
|
|
||||||
{
|
|
||||||
schema: {
|
|
||||||
description: "Get all current information for dpu status page",
|
|
||||||
tags: ["homepage"],
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
ha_desk_position: z.custom<API_HA_DeskPosition>().nullable(),
|
|
||||||
ha_temp: z.string().nullable(),
|
|
||||||
tidal_current: z.custom<TidalGetCurrent>().nullable(),
|
|
||||||
}),
|
|
||||||
418: z.object({
|
|
||||||
error: z.string(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async (_request, reply) => {
|
|
||||||
const service_result = await hpService.getFullInformation();
|
|
||||||
|
|
||||||
if (!service_result.successful) {
|
|
||||||
reply.code(418);
|
|
||||||
return { error: service_result.result };
|
|
||||||
}
|
|
||||||
|
|
||||||
return service_result.result;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,151 +1,283 @@
|
|||||||
import {
|
import {
|
||||||
|
type API_HA_DeskPosition,
|
||||||
BaseService,
|
BaseService,
|
||||||
FullInformation,
|
type ComponentUpdate,
|
||||||
HomeAssistantDeskPositionResult,
|
createComponentUpdate,
|
||||||
SseClientChangeEvent,
|
type FullInformation,
|
||||||
SseService,
|
type GristRecord_PersonalGoals,
|
||||||
TidalGetCurrent,
|
type HomeAssistantDeskPositionResult,
|
||||||
type ServiceResult,
|
type ServiceResult,
|
||||||
|
StatusComponent,
|
||||||
|
type TidalGetCurrent,
|
||||||
|
type WsService,
|
||||||
} from "@dpu/shared";
|
} from "@dpu/shared";
|
||||||
import { logWarning } from "@dpu/shared/dist/logger.js";
|
import { logInfo, logWarning } from "@dpu/shared/dist/logger.js";
|
||||||
import { HomeAssistantService } from "../homeassistant/service";
|
import { DateTime } from "luxon";
|
||||||
import { TidalService } from "../tidal/service";
|
import type { WebSocket } from "ws";
|
||||||
|
import type { GristService } from "../grist/service";
|
||||||
|
import type { HomeAssistantService } from "../homeassistant/service";
|
||||||
|
import type { TidalService } from "../tidal/service";
|
||||||
|
|
||||||
export class HomepageService extends BaseService<null> {
|
export class HomepageService extends BaseService<null> {
|
||||||
|
private gristService: GristService;
|
||||||
private haService: HomeAssistantService;
|
private haService: HomeAssistantService;
|
||||||
private tidalService: TidalService;
|
private tidalService: TidalService;
|
||||||
private sseService: SseService;
|
private wsService: WsService;
|
||||||
private pollingInterval: ReturnType<typeof setInterval> | null = null;
|
private lastPoll: FullInformation = {
|
||||||
private songEndTimeout: ReturnType<typeof setTimeout> | null = null;
|
ha_desk_position: null,
|
||||||
private lastPoll: FullInformation | null = null;
|
ha_temp: null,
|
||||||
|
tidal_current: null,
|
||||||
|
grist_personal_goals: null,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
gristService: GristService,
|
||||||
haService: HomeAssistantService,
|
haService: HomeAssistantService,
|
||||||
tidalService: TidalService,
|
tidalService: TidalService,
|
||||||
sseService: SseService,
|
wsService: WsService,
|
||||||
) {
|
) {
|
||||||
super(null);
|
super(null);
|
||||||
|
this.gristService = gristService;
|
||||||
this.haService = haService;
|
this.haService = haService;
|
||||||
this.tidalService = tidalService;
|
this.tidalService = tidalService;
|
||||||
this.sseService = sseService;
|
this.wsService = wsService;
|
||||||
this.listenForClientChange();
|
}
|
||||||
|
|
||||||
|
scheduleMidnightGristUpdate(): void {
|
||||||
|
const now = DateTime.now().setZone("Europe/Berlin");
|
||||||
|
const nextMidnight = now.plus({ days: 1 }).startOf("day");
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await this.fetchAndBroadcastGrist();
|
||||||
|
this.scheduleMidnightGristUpdate();
|
||||||
|
}, nextMidnight.diff(now).toMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchAndBroadcastGrist(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (this.wsService.getClientCount() > 0) {
|
||||||
|
const updates: ComponentUpdate[] = [];
|
||||||
|
this.updateGristPG(await this._getGristPG(), updates);
|
||||||
|
|
||||||
|
if (updates.length > 0) {
|
||||||
|
this.wsService.broadcast({
|
||||||
|
type: "update",
|
||||||
|
data: updates,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logWarning("Error fetching midnight Grist update", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendFullInformationToSocket(socket: WebSocket): Promise<void> {
|
||||||
|
try {
|
||||||
|
const [desk, temp, tidal, personal_goals] = await this._getAll();
|
||||||
|
const updates: ComponentUpdate[] = [
|
||||||
|
createComponentUpdate(StatusComponent.HA_DESK_POSITION, desk),
|
||||||
|
createComponentUpdate(StatusComponent.HA_TEMP, temp),
|
||||||
|
createComponentUpdate(StatusComponent.TIDAL_CURRENT, tidal),
|
||||||
|
createComponentUpdate(
|
||||||
|
StatusComponent.GRIST_PERSONAL_GOALS,
|
||||||
|
personal_goals,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "update",
|
||||||
|
data: updates,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logWarning("error getting all information for socket update.", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFullInformation(): Promise<ServiceResult<FullInformation | string>> {
|
async getFullInformation(): Promise<ServiceResult<FullInformation | string>> {
|
||||||
try {
|
try {
|
||||||
const [desk, temp, song] = await Promise.all([
|
const [desk, temp, tidal, personal_goals] = await this._getAll();
|
||||||
this.haService.getDeskPosition(),
|
|
||||||
this.haService.getTemperatureText(),
|
|
||||||
this.tidalService.getSong(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = {
|
return this.getSuccessfulResult(
|
||||||
ha_desk_position: desk.successful
|
this.updateFull({
|
||||||
|
ha_desk_position: desk as API_HA_DeskPosition,
|
||||||
|
ha_temp: temp as string,
|
||||||
|
tidal_current: tidal as TidalGetCurrent,
|
||||||
|
grist_personal_goals: personal_goals as GristRecord_PersonalGoals,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return this.getErrorResult("error getting all information.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getAll(): Promise<Iterable<unknown>> {
|
||||||
|
return await Promise.all([
|
||||||
|
this._getDesk(),
|
||||||
|
this._getTemp(),
|
||||||
|
this._getTidal(),
|
||||||
|
this._getGristPG(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getDesk(): Promise<API_HA_DeskPosition | null> {
|
||||||
|
const desk = await this.haService.getDeskPosition();
|
||||||
|
return desk.successful
|
||||||
? this.haService.convertPosResultToApiAnswer(
|
? this.haService.convertPosResultToApiAnswer(
|
||||||
desk.result as HomeAssistantDeskPositionResult,
|
desk.result as HomeAssistantDeskPositionResult,
|
||||||
)
|
)
|
||||||
: null,
|
: null;
|
||||||
ha_temp: temp.successful ? temp.result : null,
|
|
||||||
tidal_current: song ? (song.result as TidalGetCurrent) : null,
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.getSuccessfulResult(this.updateLastPoll(result));
|
|
||||||
} catch {
|
|
||||||
const error_message = "error getting all information";
|
|
||||||
logWarning(error_message);
|
|
||||||
return this.getErrorResult(error_message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLastPoll(newPoll: FullInformation) {
|
private async _getTemp(): Promise<string | null> {
|
||||||
const updates = [];
|
const temp = await this.haService.getTemperature();
|
||||||
if (
|
return temp.successful ? temp.result : null;
|
||||||
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) {
|
private async _getTidal(): Promise<TidalGetCurrent | null> {
|
||||||
updates.push({
|
const tidal = await this.tidalService.getSong();
|
||||||
component: "ha_temp",
|
return tidal ? (tidal.result as TidalGetCurrent) : null;
|
||||||
data: newPoll.ha_temp,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
private async _getGristPG(): Promise<GristRecord_PersonalGoals | null> {
|
||||||
this.lastPoll?.tidal_current?.title !== newPoll.tidal_current?.title ||
|
const personal_goals = await this.gristService.getToday();
|
||||||
this.lastPoll?.tidal_current?.status !== newPoll.tidal_current?.status
|
return personal_goals.successful
|
||||||
) {
|
? (personal_goals.result as GristRecord_PersonalGoals)
|
||||||
updates.push({
|
: null;
|
||||||
component: "tidal_current",
|
|
||||||
data: newPoll.tidal_current,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFull(newPoll: FullInformation): FullInformation {
|
||||||
|
const updates: ComponentUpdate[] = [];
|
||||||
|
|
||||||
|
this.updateHaDesk(newPoll.ha_desk_position, updates);
|
||||||
|
this.updateHaTemp(newPoll.ha_temp, updates);
|
||||||
|
this.updateTidal(newPoll.tidal_current, updates);
|
||||||
|
this.updateGristPG(newPoll.grist_personal_goals, updates);
|
||||||
|
|
||||||
if (updates.length > 0) {
|
if (updates.length > 0) {
|
||||||
this.sseService.notifyClients({
|
logInfo("Updating clients");
|
||||||
|
this.wsService.broadcast({
|
||||||
type: "update",
|
type: "update",
|
||||||
data: updates,
|
data: updates,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastPoll = newPoll;
|
|
||||||
this.scheduleSongEndPoll(newPoll.tidal_current);
|
|
||||||
return newPoll;
|
return newPoll;
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleSongEndPoll(tidal: TidalGetCurrent | null): void {
|
async updatePartial(
|
||||||
this.clearSongEndPoll();
|
components: ComponentUpdate[],
|
||||||
|
): Promise<ServiceResult<string>> {
|
||||||
if (!tidal || tidal.status === "paused") {
|
const updates: ComponentUpdate[] = [];
|
||||||
return;
|
for (const component of components) {
|
||||||
|
switch (component.component) {
|
||||||
|
case StatusComponent.HA_DESK_POSITION: {
|
||||||
|
this.updateHaDesk(
|
||||||
|
(component.data as API_HA_DeskPosition) ?? (await this._getDesk()),
|
||||||
|
updates,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case StatusComponent.HA_TEMP:
|
||||||
|
this.updateHaTemp(
|
||||||
|
(component.data as string) ?? (await this._getTemp()),
|
||||||
|
updates,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case StatusComponent.TIDAL_CURRENT:
|
||||||
|
this.updateTidal(
|
||||||
|
(component.data as TidalGetCurrent) ?? (await this._getTidal()),
|
||||||
|
updates,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case StatusComponent.GRIST_PERSONAL_GOALS:
|
||||||
|
this.updateGristPG(
|
||||||
|
(component.data as GristRecord_PersonalGoals) ??
|
||||||
|
(await this._getGristPG()),
|
||||||
|
updates,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const remainingSeconds = tidal.durationInSeconds - tidal.currentInSeconds;
|
this.wsService.broadcast({
|
||||||
if (remainingSeconds > 0) {
|
type: "update",
|
||||||
this.songEndTimeout = setTimeout(
|
data: updates,
|
||||||
() => {
|
});
|
||||||
this.songEndTimeout = null;
|
|
||||||
this.getFullInformation();
|
return this.getSuccessfulResult("update broadcasted");
|
||||||
},
|
}
|
||||||
(remainingSeconds + 1) * 1000,
|
|
||||||
|
updateHaDesk(
|
||||||
|
new_ha_desk_position: API_HA_DeskPosition | null,
|
||||||
|
updates: ComponentUpdate[],
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
this.lastPoll.ha_desk_position?.is_standing !==
|
||||||
|
new_ha_desk_position?.is_standing
|
||||||
|
) {
|
||||||
|
this.lastPoll.ha_desk_position = new_ha_desk_position;
|
||||||
|
updates.push(
|
||||||
|
createComponentUpdate(
|
||||||
|
StatusComponent.HA_DESK_POSITION,
|
||||||
|
new_ha_desk_position,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearSongEndPoll(): void {
|
updateHaTemp(new_ha_temp: string | null, updates: ComponentUpdate[]): void {
|
||||||
if (this.songEndTimeout) {
|
if (this.lastPoll.ha_temp !== new_ha_temp) {
|
||||||
clearTimeout(this.songEndTimeout);
|
this.lastPoll.ha_temp = new_ha_temp;
|
||||||
this.songEndTimeout = null;
|
updates.push(createComponentUpdate(StatusComponent.HA_TEMP, new_ha_temp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listenForClientChange(): void {
|
updateTidal(
|
||||||
this.sseService.onClientChange((clientChange: SseClientChangeEvent) => {
|
new_tidal_current: TidalGetCurrent | null,
|
||||||
if (clientChange.clientCount === 0) {
|
updates: ComponentUpdate[],
|
||||||
this.stopPolling();
|
): void {
|
||||||
} else {
|
if (
|
||||||
if (!this.pollingInterval) {
|
this.lastPoll.tidal_current?.title !== new_tidal_current?.title ||
|
||||||
this.startPolling();
|
this.lastPoll.tidal_current?.artists !== new_tidal_current?.artists ||
|
||||||
|
this.lastPoll.tidal_current?.status !== new_tidal_current?.status ||
|
||||||
|
this.lastPoll.tidal_current?.volume !== new_tidal_current?.volume
|
||||||
|
) {
|
||||||
|
this.lastPoll.tidal_current = new_tidal_current;
|
||||||
|
updates.push(
|
||||||
|
createComponentUpdate(StatusComponent.TIDAL_CURRENT, new_tidal_current),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
startPolling(): void {
|
updateGristPG(
|
||||||
this.pollingInterval = setInterval(() => {
|
new_grist_personal_goals: GristRecord_PersonalGoals | null,
|
||||||
this.getFullInformation();
|
updates: ComponentUpdate[],
|
||||||
}, 30000);
|
): void {
|
||||||
}
|
if (
|
||||||
|
this.lastPoll.grist_personal_goals?.went_outside !==
|
||||||
stopPolling(): void {
|
new_grist_personal_goals?.went_outside ||
|
||||||
if (this.pollingInterval) {
|
this.lastPoll.grist_personal_goals?.standing !==
|
||||||
clearInterval(this.pollingInterval);
|
new_grist_personal_goals?.standing ||
|
||||||
}
|
this.lastPoll.grist_personal_goals?.steps !==
|
||||||
this.clearSongEndPoll();
|
new_grist_personal_goals?.steps ||
|
||||||
|
this.lastPoll.grist_personal_goals?.pushups !==
|
||||||
|
new_grist_personal_goals?.pushups ||
|
||||||
|
this.lastPoll.grist_personal_goals?.squats !==
|
||||||
|
new_grist_personal_goals?.squats ||
|
||||||
|
this.lastPoll.grist_personal_goals?.leg_raises !==
|
||||||
|
new_grist_personal_goals?.leg_raises ||
|
||||||
|
this.lastPoll.grist_personal_goals?.stairs !==
|
||||||
|
new_grist_personal_goals?.stairs
|
||||||
|
) {
|
||||||
|
this.lastPoll.grist_personal_goals = new_grist_personal_goals;
|
||||||
|
updates.push(
|
||||||
|
createComponentUpdate(
|
||||||
|
StatusComponent.GRIST_PERSONAL_GOALS,
|
||||||
|
new_grist_personal_goals,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
205
src/index.ts
205
src/index.ts
@@ -1,56 +1,103 @@
|
|||||||
import { logInfo } from "@dpu/shared/dist/logger.js";
|
import { WsService } from "@dpu/shared";
|
||||||
import fastifySwagger from "@fastify/swagger";
|
import { logError } from "@dpu/shared/dist/logger.js";
|
||||||
import fastifySwaggerUi from "@fastify/swagger-ui";
|
import fastifyCors, { type FastifyCorsOptions } from "@fastify/cors";
|
||||||
|
import fastifySwagger, { type SwaggerOptions } from "@fastify/swagger";
|
||||||
|
import fastifySwaggerUi, {
|
||||||
|
type FastifySwaggerUiOptions,
|
||||||
|
} from "@fastify/swagger-ui";
|
||||||
|
import fastifyWebsocket from "@fastify/websocket";
|
||||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import Fastify from "fastify";
|
import Fastify from "fastify";
|
||||||
import fastifyAxios from "fastify-axios";
|
import fastifyAxios, { type FastifyAxiosOptions } 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 type { OpenAPIV3 } from "openapi-types";
|
||||||
import { SwaggerTheme, SwaggerThemeNameEnum } from "swagger-themes";
|
import { SwaggerTheme, SwaggerThemeNameEnum } from "swagger-themes";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Config } from "./config.js";
|
import { Config } from "./config.js";
|
||||||
|
import { GadgetbridgeClient } from "./gadgetbridge/client.js";
|
||||||
|
import { privateGadgetbridgeRoutes } from "./gadgetbridge/private-routes.js";
|
||||||
|
import { GadgetbridgeService } from "./gadgetbridge/service.js";
|
||||||
|
import { GristClient } from "./grist/client.js";
|
||||||
|
import { privateGristRoutes } from "./grist/private-routes.js";
|
||||||
|
import { GristService } from "./grist/service.js";
|
||||||
import { HomeAssistantClient } from "./homeassistant/client.js";
|
import { HomeAssistantClient } from "./homeassistant/client.js";
|
||||||
import { homeAssistantRoutes } from "./homeassistant/routes.js";
|
import { privateHomeAssistantRoutes } from "./homeassistant/private-routes.js";
|
||||||
import { HomeAssistantService } from "./homeassistant/service.js";
|
import { HomeAssistantService } from "./homeassistant/service.js";
|
||||||
import { TidalClient } from "./tidal/client.js";
|
import { privateHomepageRoutes } from "./homepage/private-routes.js";
|
||||||
import { TidalService } from "./tidal/service.js";
|
|
||||||
import { tidalRoutes } from "./tidal/routes.js";
|
|
||||||
import { sseRoutes } from "./sse/routes.js";
|
|
||||||
import { SseService } from "@dpu/shared";
|
|
||||||
import { HomepageService } from "./homepage/service.js";
|
import { HomepageService } from "./homepage/service.js";
|
||||||
import { homepageRoutes } from "./homepage/routes.js";
|
import { TidalClient } from "./tidal/client.js";
|
||||||
import fastifySSE from "@fastify/sse";
|
import { privateTidalRoutes } from "./tidal/private-routes.js";
|
||||||
|
import { TidalService } from "./tidal/service.js";
|
||||||
|
import { publicWsRoutes } from "./websocket/public-routes.js";
|
||||||
|
|
||||||
const fastify = Fastify().withTypeProvider<ZodTypeProvider>();
|
// Initialize with Zod
|
||||||
|
const publicServer = Fastify().withTypeProvider<ZodTypeProvider>();
|
||||||
|
const privateServer = Fastify().withTypeProvider<ZodTypeProvider>();
|
||||||
|
publicServer.setValidatorCompiler(validatorCompiler);
|
||||||
|
publicServer.setSerializerCompiler(serializerCompiler);
|
||||||
|
privateServer.setValidatorCompiler(validatorCompiler);
|
||||||
|
privateServer.setSerializerCompiler(serializerCompiler);
|
||||||
|
|
||||||
fastify.setValidatorCompiler(validatorCompiler);
|
// Cors
|
||||||
fastify.setSerializerCompiler(serializerCompiler);
|
function getCorsObject(urls: string[]): FastifyCorsOptions {
|
||||||
|
return {
|
||||||
|
origin: urls,
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
allowedHeaders: ["Content-Type", "Authorization"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await fastify.register(fastifySwagger, {
|
await publicServer.register(
|
||||||
|
fastifyCors,
|
||||||
|
getCorsObject(["https://dariusbag.dev"]),
|
||||||
|
);
|
||||||
|
await privateServer.register(
|
||||||
|
fastifyCors,
|
||||||
|
getCorsObject(["http://localhost:4321"]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Swagger
|
||||||
|
function getSwaggerObject(servers: OpenAPIV3.ServerObject[]): SwaggerOptions {
|
||||||
|
return {
|
||||||
openapi: {
|
openapi: {
|
||||||
info: {
|
info: {
|
||||||
title: "DPU API",
|
title: "DPU API",
|
||||||
description: "API Documentation",
|
description: "API Documentation",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
},
|
},
|
||||||
servers: [
|
servers: servers,
|
||||||
{ url: "http://localhost:8080", description: "dev" },
|
|
||||||
{ url: "https://dpu.dariusbag.dev/api", description: "prod" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
transform: jsonSchemaTransform,
|
transform: jsonSchemaTransform,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await publicServer.register(
|
||||||
|
fastifySwagger,
|
||||||
|
getSwaggerObject([
|
||||||
|
{ url: "https://dpu.dariusbag.dev/api", description: "prod" },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
await privateServer.register(
|
||||||
|
fastifySwagger,
|
||||||
|
getSwaggerObject([
|
||||||
|
{ url: "http://localhost:8080", description: "dev" },
|
||||||
|
{ url: "http://192.168.178.161:20001", description: "prod" },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Swagger UI
|
||||||
const theme = new SwaggerTheme();
|
const theme = new SwaggerTheme();
|
||||||
const content = theme.getBuffer(SwaggerThemeNameEnum.ONE_DARK);
|
const content = theme.getBuffer(SwaggerThemeNameEnum.ONE_DARK);
|
||||||
|
|
||||||
await fastify.register(fastifySwaggerUi, {
|
function getSwaggerUiObject(indexPrefix: string): FastifySwaggerUiOptions {
|
||||||
|
return {
|
||||||
routePrefix: "/docs",
|
routePrefix: "/docs",
|
||||||
indexPrefix: `${Config.env_dev ? "" : "/api"}`,
|
indexPrefix: indexPrefix,
|
||||||
uiConfig: {
|
uiConfig: {
|
||||||
docExpansion: "list",
|
docExpansion: "list",
|
||||||
deepLinking: false,
|
deepLinking: false,
|
||||||
@@ -58,10 +105,22 @@ await fastify.register(fastifySwaggerUi, {
|
|||||||
theme: {
|
theme: {
|
||||||
css: [{ filename: "theme.css", content: content }],
|
css: [{ filename: "theme.css", content: content }],
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await fastify.register(fastifyAxios, {
|
await publicServer.register(fastifySwaggerUi, getSwaggerUiObject("/api"));
|
||||||
|
await privateServer.register(fastifySwaggerUi, getSwaggerUiObject(""));
|
||||||
|
|
||||||
|
// axios
|
||||||
|
function getAxiosConfig(): FastifyAxiosOptions {
|
||||||
|
return {
|
||||||
clients: {
|
clients: {
|
||||||
|
grist: {
|
||||||
|
baseURL: Config.grist.api_url,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Config.grist.api_token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
homeassistant: {
|
homeassistant: {
|
||||||
baseURL: Config.homeassistant.api_url,
|
baseURL: Config.homeassistant.api_url,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -69,44 +128,92 @@ await fastify.register(fastifyAxios, {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
tidal: {
|
tidal: {
|
||||||
baseURL: `${Config.tidal.host}:${Config.tidal.port}`,
|
baseURL: `${Config.tidal.address}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await fastify.register(fastifySSE);
|
await privateServer.register(fastifyAxios, getAxiosConfig());
|
||||||
|
|
||||||
const haClient = new HomeAssistantClient(fastify.axios.homeassistant);
|
// Websockets
|
||||||
|
await publicServer.register(fastifyWebsocket);
|
||||||
|
|
||||||
|
// Clients and Services
|
||||||
|
const gadgetbridgeClient = new GadgetbridgeClient(Config.gadgetbridge.db_path, Config.gadgetbridge.old_db_path);
|
||||||
|
const gadgetbridgeService = new GadgetbridgeService(gadgetbridgeClient);
|
||||||
|
|
||||||
|
const gristClient = new GristClient(privateServer.axios.grist);
|
||||||
|
const gristService = new GristService(gristClient);
|
||||||
|
|
||||||
|
const haClient = new HomeAssistantClient(privateServer.axios.homeassistant);
|
||||||
const haService = new HomeAssistantService(haClient);
|
const haService = new HomeAssistantService(haClient);
|
||||||
|
|
||||||
const tidalClient = new TidalClient(fastify.axios.tidal);
|
const tidalClient = new TidalClient(privateServer.axios.tidal);
|
||||||
const tidalService = new TidalService(tidalClient);
|
const tidalService = new TidalService(tidalClient);
|
||||||
|
|
||||||
const sseService = new SseService();
|
const wsService = new WsService();
|
||||||
|
|
||||||
const hpService = new HomepageService(haService, tidalService, sseService);
|
const hpService = new HomepageService(
|
||||||
|
gristService,
|
||||||
|
haService,
|
||||||
|
tidalService,
|
||||||
|
wsService,
|
||||||
|
);
|
||||||
|
|
||||||
|
hpService.scheduleMidnightGristUpdate();
|
||||||
|
|
||||||
async function verifyAPIKey(
|
async function verifyAPIKey(
|
||||||
request: FastifyRequest,
|
request: FastifyRequest,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const apiKey = request.headers["x-api-key"];
|
const apiKey = request.headers["x-api-key"] ?? request.headers.authorization;
|
||||||
|
|
||||||
if (!apiKey || apiKey !== Config.api_key) {
|
if (!apiKey || apiKey !== Config.api_key) {
|
||||||
logInfo("POST Request with wrong API key received");
|
logError("POST Request with wrong API key received");
|
||||||
return reply.code(401).send({ error: "Invalid API key" });
|
return reply.code(401).send({ error: "Invalid API key" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = parseInt(Config.port, 10);
|
|
||||||
|
|
||||||
// Register routes
|
// Register routes
|
||||||
await fastify.register(homeAssistantRoutes, { haService, verifyAPIKey });
|
await publicServer.register(publicWsRoutes, { wsService, hpService });
|
||||||
await fastify.register(tidalRoutes, { tidalService, verifyAPIKey });
|
|
||||||
await fastify.register(sseRoutes, { sseService });
|
|
||||||
await fastify.register(homepageRoutes, { hpService });
|
|
||||||
|
|
||||||
fastify.get(
|
await privateServer.register(privateGadgetbridgeRoutes, {
|
||||||
|
gadgetbridgeService,
|
||||||
|
});
|
||||||
|
await privateServer.register(privateGristRoutes, { gristService });
|
||||||
|
await privateServer.register(privateHomeAssistantRoutes, {
|
||||||
|
haService,
|
||||||
|
verifyAPIKey,
|
||||||
|
});
|
||||||
|
await privateServer.register(privateTidalRoutes, {
|
||||||
|
tidalService,
|
||||||
|
verifyAPIKey,
|
||||||
|
});
|
||||||
|
await privateServer.register(privateHomepageRoutes, {
|
||||||
|
hpService,
|
||||||
|
gristService,
|
||||||
|
haService,
|
||||||
|
verifyAPIKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ping Routes
|
||||||
|
publicServer.get(
|
||||||
|
"/ping",
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
description: "Health check endpoint",
|
||||||
|
tags: ["default"],
|
||||||
|
response: {
|
||||||
|
200: z.literal("pong"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (_request, _reply) => {
|
||||||
|
return "pong" as const;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
privateServer.get(
|
||||||
"/ping",
|
"/ping",
|
||||||
{
|
{
|
||||||
schema: {
|
schema: {
|
||||||
@@ -122,8 +229,20 @@ fastify.get(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await fastify.ready();
|
const port = parseInt(Config.port, 10);
|
||||||
fastify.listen({ port: port, host: "0.0.0.0" }, (err, address) => {
|
const publicPort = port + 1;
|
||||||
|
|
||||||
|
await publicServer.ready();
|
||||||
|
await privateServer.ready();
|
||||||
|
|
||||||
|
publicServer.listen({ port: publicPort, host: "0.0.0.0" }, (err, address) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`Server listening at ${address}`);
|
||||||
|
});
|
||||||
|
privateServer.listen({ port: port, host: "0.0.0.0" }, (err, address) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import type { FastifyInstance } from "fastify";
|
|
||||||
import { logInfo, type SseEvent, type SseService } from "@dpu/shared";
|
|
||||||
import { randomUUID } from "node:crypto";
|
|
||||||
|
|
||||||
export async function sseRoutes(
|
|
||||||
fastify: FastifyInstance,
|
|
||||||
{
|
|
||||||
sseService,
|
|
||||||
}: {
|
|
||||||
sseService: SseService;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
fastify.get(
|
|
||||||
"/dpu/events",
|
|
||||||
{
|
|
||||||
schema: {
|
|
||||||
description: "Register for SSE",
|
|
||||||
tags: ["sse"],
|
|
||||||
hide: true,
|
|
||||||
},
|
|
||||||
sse: true,
|
|
||||||
},
|
|
||||||
async (_request, reply) => {
|
|
||||||
reply.sse.keepAlive();
|
|
||||||
|
|
||||||
const clientId = randomUUID();
|
|
||||||
const sendEvent = (data: SseEvent) => {
|
|
||||||
reply.sse.send({
|
|
||||||
data: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
sseService.addClient({ id: clientId, send: sendEvent });
|
|
||||||
|
|
||||||
await reply.sse.send({ data: "Connected" });
|
|
||||||
logInfo(`Connection for client ${clientId} established`);
|
|
||||||
|
|
||||||
reply.sse.onClose(() => {
|
|
||||||
sseService.removeClient(clientId);
|
|
||||||
logInfo(`Connection for client ${clientId} closed`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { TidalService } from "../tidal/service.js";
|
import type { TidalService } from "./service.js";
|
||||||
|
|
||||||
export async function tidalRoutes(
|
export async function privateTidalRoutes(
|
||||||
fastify: FastifyInstance,
|
fastify: FastifyInstance,
|
||||||
{
|
{
|
||||||
tidalService,
|
tidalService,
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/websocket/public-routes.ts
Normal file
37
src/websocket/public-routes.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { logInfo, type WsService } from "@dpu/shared";
|
||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import type { HomepageService } from "../homepage/service";
|
||||||
|
|
||||||
|
export async function publicWsRoutes(
|
||||||
|
fastify: FastifyInstance,
|
||||||
|
{
|
||||||
|
wsService,
|
||||||
|
hpService,
|
||||||
|
}: {
|
||||||
|
wsService: WsService;
|
||||||
|
hpService: HomepageService;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
fastify.get(
|
||||||
|
"/dpu/status/events",
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
description: "Register for WebSocket events",
|
||||||
|
tags: ["ws"],
|
||||||
|
hide: true,
|
||||||
|
},
|
||||||
|
websocket: true,
|
||||||
|
},
|
||||||
|
(socket, _request) => {
|
||||||
|
wsService.addClient(socket);
|
||||||
|
socket.send(JSON.stringify({ type: "message", data: "Connected" }));
|
||||||
|
hpService.sendFullInformationToSocket(socket)
|
||||||
|
logInfo(`Connection for client established`);
|
||||||
|
|
||||||
|
socket.on("close", () => {
|
||||||
|
wsService.removeClient(socket);
|
||||||
|
logInfo(`Connection for client closed`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,48 +1,48 @@
|
|||||||
### Simple GET Request
|
### Simple GET Request
|
||||||
GET http://localhost:8080/api/ping
|
GET http://localhost:8080/ping
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
#################### HA #######################
|
#################### HA #######################
|
||||||
|
|
||||||
### Simple GET Desk Position
|
### Simple GET Desk Position
|
||||||
GET http://localhost:8080/api/homeassistant/desk/position
|
GET http://localhost:8080/homeassistant/desk/position
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
### Simple GET Stand Automation
|
### Simple GET Stand Automation
|
||||||
GET http://localhost:8080/api/homeassistant/desk/stand
|
GET http://localhost:8080/homeassistant/desk/stand
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
### Simple POST Stand Automation
|
### Simple POST Stand Automation
|
||||||
POST http://localhost:8080/api/homeassistant/desk/stand
|
POST http://localhost:8080/homeassistant/desk/stand
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
### Simple GET Temps
|
### Simple GET Temps
|
||||||
GET http://localhost:8080/api/homeassistant/temperature
|
GET http://localhost:8080/homeassistant/temperature
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
#################### TIDAL #######################
|
#################### TIDAL #######################
|
||||||
|
|
||||||
### Simple GET Song
|
### Simple GET Song
|
||||||
GET http://localhost:8080/api/tidal/song
|
GET http://localhost:8080/tidal/song
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
### Simple GET Song Formatted
|
### Simple GET Song Formatted
|
||||||
GET http://localhost:8080/api/tidal/songFormatted
|
GET http://localhost:8080/tidal/songFormatted
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
### Simple GET Volume
|
### Simple GET Volume
|
||||||
GET http://localhost:8080/api/tidal/volume
|
GET http://localhost:8080/tidal/volume
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
### Simple SET Volume
|
### Simple SET Volume
|
||||||
POST http://localhost:8080/api/tidal/volume
|
POST http://localhost:8080/tidal/volume
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|||||||
Reference in New Issue
Block a user