gadgetbridge integration

This commit is contained in:
Darius
2026-03-04 16:27:05 +01:00
parent 908350f696
commit 4a3a994359
6 changed files with 121 additions and 0 deletions

3
.gitignore vendored
View File

@@ -25,3 +25,6 @@ yarn-error.log*
# Environment
.env
.env.local
# Gadgetbridge DB
/src/gadgetbridge/db/*

View File

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

View File

@@ -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[];
}
}

View 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;
},
);
}

View 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,
);
}
}
}

View File

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