From 4a3a994359e0788bdff957ef18db325be93c853c Mon Sep 17 00:00:00 2001 From: Darius Date: Wed, 4 Mar 2026 16:27:05 +0100 Subject: [PATCH] gadgetbridge integration --- .gitignore | 3 ++ src/config.ts | 5 +++ src/gadgetbridge/client.ts | 27 +++++++++++++++ src/gadgetbridge/private-routes.ts | 53 ++++++++++++++++++++++++++++++ src/gadgetbridge/service.ts | 23 +++++++++++++ src/index.ts | 10 ++++++ 6 files changed, 121 insertions(+) create mode 100644 src/gadgetbridge/client.ts create mode 100644 src/gadgetbridge/private-routes.ts create mode 100644 src/gadgetbridge/service.ts diff --git a/.gitignore b/.gitignore index 3df6506..b7f934d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ yarn-error.log* # Environment .env .env.local + +# Gadgetbridge DB +/src/gadgetbridge/db/* diff --git a/src/config.ts b/src/config.ts index c8bd332..06c0b8f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,6 +6,11 @@ export const Config = { api_key: process.env.API_KEY, port: process.env.PORT || "8080", + gadgetbridge: { + db_path: + process.env.GADGETBRIDGE_DB_PATH || "src/gadgetbridge/db/Gadgetbridge.db", + }, + grist: { api_url: process.env.GRIST_API_URL || "", api_token: process.env.GRIST_API_TOKEN || "", diff --git a/src/gadgetbridge/client.ts b/src/gadgetbridge/client.ts new file mode 100644 index 0000000..0a7a3f2 --- /dev/null +++ b/src/gadgetbridge/client.ts @@ -0,0 +1,27 @@ +import { DatabaseSync } from "node:sqlite"; + +export type StepRow = { + date: string; + steps: number; +}; + +export class GadgetbridgeClient { + private db: DatabaseSync; + + constructor(dbPath: string) { + this.db = new DatabaseSync(dbPath, { open: true }); + } + + getStepsPerDay(fromTimestamp: number, toTimestamp: number): StepRow[] { + const stmt = this.db.prepare(` + SELECT + DATE(TIMESTAMP, 'unixepoch', 'localtime') AS date, + SUM(STEPS) AS steps + FROM HUAMI_EXTENDED_ACTIVITY_SAMPLE + WHERE TIMESTAMP >= ? AND TIMESTAMP < ? + GROUP BY date + ORDER BY date + `); + return stmt.all(fromTimestamp, toTimestamp) as StepRow[]; + } +} diff --git a/src/gadgetbridge/private-routes.ts b/src/gadgetbridge/private-routes.ts new file mode 100644 index 0000000..59e4f5f --- /dev/null +++ b/src/gadgetbridge/private-routes.ts @@ -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; + }, + ); +} diff --git a/src/gadgetbridge/service.ts b/src/gadgetbridge/service.ts new file mode 100644 index 0000000..277d5ae --- /dev/null +++ b/src/gadgetbridge/service.ts @@ -0,0 +1,23 @@ +import { BaseService, type ServiceResult } from "@dpu/shared"; +import type { GadgetbridgeClient, StepRow } from "./client.js"; + +export class GadgetbridgeService extends BaseService { + getStepsForTimespan( + from: Date, + to: Date, + ): ServiceResult { + 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, + ); + } + } +} diff --git a/src/index.ts b/src/index.ts index f9ce505..c880a72 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,9 @@ import type { OpenAPIV3 } from "openapi-types"; import { SwaggerTheme, SwaggerThemeNameEnum } from "swagger-themes"; import { z } from "zod"; 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"; @@ -82,6 +85,7 @@ await publicServer.register( await privateServer.register( fastifySwagger, getSwaggerObject([ + { url: "http://localhost:8080", description: "dev" }, { url: "http://192.168.178.161:20001", description: "prod" }, ]), ); @@ -136,6 +140,9 @@ await privateServer.register(fastifyAxios, getAxiosConfig()); await publicServer.register(fastifyWebsocket); // Clients and Services +const gadgetbridgeClient = new GadgetbridgeClient(Config.gadgetbridge.db_path); +const gadgetbridgeService = new GadgetbridgeService(gadgetbridgeClient); + const gristClient = new GristClient(privateServer.axios.grist); const gristService = new GristService(gristClient); @@ -171,6 +178,9 @@ async function verifyAPIKey( // Register routes await publicServer.register(publicWsRoutes, { wsService, hpService }); +await privateServer.register(privateGadgetbridgeRoutes, { + gadgetbridgeService, +}); await privateServer.register(privateGristRoutes, { gristService }); await privateServer.register(privateHomeAssistantRoutes, { haService,