initial commit

This commit is contained in:
2025-11-16 15:57:18 +01:00
parent f97b165008
commit edd6c028e5
21 changed files with 673 additions and 15 deletions

View File

@@ -0,0 +1,8 @@
import z from "zod";
export const MQTTClientOptions = z.object({
password: z.string().optional(),
user: z.string().optional(),
host: z.string(),
port: z.number(),
});

View File

@@ -0,0 +1,12 @@
import mqtt from "mqtt";
import type z from "zod";
import type { MQTTClientOptions } from "./mqtt-client.schema";
export async function getMqttClient(
options: z.output<typeof MQTTClientOptions>,
) {
return await mqtt.connectAsync(`mqtt://${options.host}:${options.port}`, {
password: options.password,
username: options.user,
});
}

11
src/config.schema.ts Normal file
View File

@@ -0,0 +1,11 @@
import z from "zod";
import { MQTTClientOptions } from "./clients/mqtt/mqtt-client.schema";
export const ConfigSchema = z.object({
mqtt: MQTTClientOptions,
publishInterval: z.number().default(2000),
device: z.object({
key: z.string(),
name: z.string(),
}),
});

1
src/constants.ts Normal file
View File

@@ -0,0 +1 @@
export const APPLICATION_NAME = "efibootmgr-mqtt";

8
src/efibootmgr.schema.ts Normal file
View File

@@ -0,0 +1,8 @@
import z from "zod";
export const BootEntry = z.object({
label: z.string(),
number: z.number(),
});
export const BootEntries = z.array(BootEntry);

21
src/efibootmgr.service.ts Normal file
View File

@@ -0,0 +1,21 @@
import { $ } from "bun";
import type z from "zod";
import type { BootEntries } from "./efibootmgr.schema";
export class EfiBootMgrService {
async reboot() {
await $`reboot`;
}
async setNextBoot(bootNum: string) {
await $`efibootmgr -n ${bootNum}`;
}
async listBootEntries(): Promise<z.output<typeof BootEntries>> {
const output = await $`efibootmgr`.text();
console.log(output);
return [];
}
}

34
src/index.ts Normal file
View File

@@ -0,0 +1,34 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { getMqttClient } from "./clients/mqtt/mqtt-client";
import { ConfigSchema } from "./config.schema";
import { EfiBootMgrService } from "./efibootmgr.service";
import { startListeners } from "./listener";
import { logger } from "./logger";
import { runPublishLoop } from "./publisher";
logger.info("Starting intialization...");
const configsFolder = process.env.CONFIGS_PATH || "./";
const fileContent = await readFile(
join(configsFolder, "efibootmgr.config.json"),
{
encoding: "utf8",
},
).catch((e) => {
throw new Error("Error while reading config!", { cause: e });
});
const json = JSON.parse(fileContent);
const config = ConfigSchema.parse(json);
const efiBootMgrService = new EfiBootMgrService();
const bootEntries = await efiBootMgrService.listBootEntries();
const mqttClient = await getMqttClient(config.mqtt);
runPublishLoop(config, mqttClient, bootEntries);
startListeners(config, mqttClient);

36
src/listener.ts Normal file
View File

@@ -0,0 +1,36 @@
import type { MqttClient } from "mqtt";
import type z from "zod";
import type { ConfigSchema } from "./config.schema";
import { logger } from "./logger";
export async function startListeners(
config: z.output<typeof ConfigSchema>,
mqttClient: MqttClient,
) {
await mqttClient.subscribeAsync(`efibootmgr-mqtt/${config.device.key}/+`);
mqttClient.on("message", async (topic, payload) => {
logger.info(topic, payload);
const pathParts = topic.split("/");
const action = pathParts[pathParts.length - 1];
switch (action) {
/* case "trigger-power": {
await nanoKvmService.triggerPower({});
return;
}
case "trigger-reset": {
await nanoKvmService.triggerReset();
return;
}
*/
default: {
throw new Error(`Unhandled action: ${action}`);
}
}
});
}

6
src/logger.ts Normal file
View File

@@ -0,0 +1,6 @@
import { Logger } from "tslog";
import { APPLICATION_NAME } from "./constants";
export const logger = new Logger({
name: APPLICATION_NAME,
});

65
src/publisher.ts Normal file
View File

@@ -0,0 +1,65 @@
import { sleep } from "bun";
import type { MqttClient } from "mqtt";
import type z from "zod";
import type { ConfigSchema } from "./config.schema";
import { APPLICATION_NAME } from "./constants";
import type { BootEntries } from "./efibootmgr.schema";
import { logger } from "./logger";
export async function runPublishLoop(
config: z.output<typeof ConfigSchema>,
mqttClient: MqttClient,
bootEntries: z.output<typeof BootEntries>,
) {
while (true) {
/* const origin = {
name: APPLICATION_NAME,
sw: "0.0.1",
url: "https://git.unom.io/enricobuehler/efibootmgr-mqtt",
}; */
const state_topic = `${APPLICATION_NAME}/${config.device.key}`;
const entityIds = {
actions: {
triggerReboot: "action_trigger_reboot",
},
};
const device = {
hw_version: 0,
identifiers: [config.device.key],
manufacturer: "unknown",
model: "unknown",
model_id: "unknown",
name: config.device.name,
sw_version: "1.0.0",
};
// Actions
for (const bootEntry of bootEntries) {
await mqttClient.publishAsync(
`homeassistant/button/${config.device.key}/button-trigger-reboot/config`,
JSON.stringify({
device,
command_topic: `${state_topic}/trigger-reboot`,
default_entity_id: `button.${config.device.key}_${entityIds.actions.triggerReboot}`,
name: `Reboot into ${bootEntry.label}`,
object_id: `efibootmgr_${config.device.key}_${entityIds.actions.triggerReboot}`,
payload_press: bootEntry.number,
unique_id: `${config.device.key}_efibootmgr_${entityIds.actions.triggerReboot}`,
}),
);
}
//
logger.debug("AutoDiscovery - Published actions");
//console.log(info.ips);
//console.log(gpio);
await sleep(config.publishInterval);
}
}

View File

@@ -0,0 +1,7 @@
import { writeFile } from "node:fs/promises";
import z from "zod";
import { ConfigSchema } from "@/config.schema";
const jsonSchema = z.toJSONSchema(ConfigSchema);
await writeFile("./config-schema.json", JSON.stringify(jsonSchema));