gadgetbridge integration
This commit is contained in:
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/*
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ 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",
|
||||||
|
|
||||||
|
gadgetbridge: {
|
||||||
|
db_path:
|
||||||
|
process.env.GADGETBRIDGE_DB_PATH || "src/gadgetbridge/db/Gadgetbridge.db",
|
||||||
|
},
|
||||||
|
|
||||||
grist: {
|
grist: {
|
||||||
api_url: process.env.GRIST_API_URL || "",
|
api_url: process.env.GRIST_API_URL || "",
|
||||||
api_token: process.env.GRIST_API_TOKEN || "",
|
api_token: process.env.GRIST_API_TOKEN || "",
|
||||||
|
|||||||
27
src/gadgetbridge/client.ts
Normal file
27
src/gadgetbridge/client.ts
Normal 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[];
|
||||||
|
}
|
||||||
|
}
|
||||||
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/index.ts
10
src/index.ts
@@ -19,6 +19,9 @@ 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 { GristClient } from "./grist/client.js";
|
||||||
import { privateGristRoutes } from "./grist/private-routes.js";
|
import { privateGristRoutes } from "./grist/private-routes.js";
|
||||||
import { GristService } from "./grist/service.js";
|
import { GristService } from "./grist/service.js";
|
||||||
@@ -82,6 +85,7 @@ await publicServer.register(
|
|||||||
await privateServer.register(
|
await privateServer.register(
|
||||||
fastifySwagger,
|
fastifySwagger,
|
||||||
getSwaggerObject([
|
getSwaggerObject([
|
||||||
|
{ url: "http://localhost:8080", description: "dev" },
|
||||||
{ url: "http://192.168.178.161:20001", description: "prod" },
|
{ url: "http://192.168.178.161:20001", description: "prod" },
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
@@ -136,6 +140,9 @@ await privateServer.register(fastifyAxios, getAxiosConfig());
|
|||||||
await publicServer.register(fastifyWebsocket);
|
await publicServer.register(fastifyWebsocket);
|
||||||
|
|
||||||
// Clients and Services
|
// Clients and Services
|
||||||
|
const gadgetbridgeClient = new GadgetbridgeClient(Config.gadgetbridge.db_path);
|
||||||
|
const gadgetbridgeService = new GadgetbridgeService(gadgetbridgeClient);
|
||||||
|
|
||||||
const gristClient = new GristClient(privateServer.axios.grist);
|
const gristClient = new GristClient(privateServer.axios.grist);
|
||||||
const gristService = new GristService(gristClient);
|
const gristService = new GristService(gristClient);
|
||||||
|
|
||||||
@@ -171,6 +178,9 @@ async function verifyAPIKey(
|
|||||||
// Register routes
|
// Register routes
|
||||||
await publicServer.register(publicWsRoutes, { wsService, hpService });
|
await publicServer.register(publicWsRoutes, { wsService, hpService });
|
||||||
|
|
||||||
|
await privateServer.register(privateGadgetbridgeRoutes, {
|
||||||
|
gadgetbridgeService,
|
||||||
|
});
|
||||||
await privateServer.register(privateGristRoutes, { gristService });
|
await privateServer.register(privateGristRoutes, { gristService });
|
||||||
await privateServer.register(privateHomeAssistantRoutes, {
|
await privateServer.register(privateHomeAssistantRoutes, {
|
||||||
haService,
|
haService,
|
||||||
|
|||||||
Reference in New Issue
Block a user