feat: Enhance speech and AI configuration with advanced environment settings

- Update `.env.example` with comprehensive speech and AI configuration options
- Modify Docker Compose speech configuration for more flexible audio and ASR settings
- Enhance Dockerfile to support Python virtual environment and speech dependencies
- Refactor environment loading to use Bun's file system utilities
- Improve device listing tool with more detailed device statistics
- Add support for multiple AI models and dynamic configuration
This commit is contained in:
jango-blockchained
2025-02-10 03:28:58 +01:00
parent 986b1949cd
commit b6bd53b01a
10 changed files with 764 additions and 283 deletions

View File

@@ -1,5 +1,5 @@
import { config as dotenvConfig } from "dotenv";
import fs from "fs";
import { file } from "bun";
import path from "path";
/**
@@ -15,7 +15,7 @@ const ENV_FILE_MAPPING: Record<string, string> = {
* Loads environment variables from the appropriate files based on NODE_ENV.
* First loads environment-specific file, then overrides with generic .env if it exists.
*/
export function loadEnvironmentVariables() {
export async function loadEnvironmentVariables() {
// Determine the current environment (default to 'development')
const nodeEnv = (process.env.NODE_ENV || "development").toLowerCase();
@@ -29,19 +29,29 @@ export function loadEnvironmentVariables() {
const envPath = path.resolve(process.cwd(), envFile);
// Load the environment-specific file if it exists
if (fs.existsSync(envPath)) {
dotenvConfig({ path: envPath });
console.log(`Loaded environment variables from ${envFile}`);
} else {
console.warn(`Environment-specific file ${envFile} not found.`);
try {
const envFileExists = await file(envPath).exists();
if (envFileExists) {
dotenvConfig({ path: envPath });
console.log(`Loaded environment variables from ${envFile}`);
} else {
console.warn(`Environment-specific file ${envFile} not found.`);
}
} catch (error) {
console.warn(`Error checking environment file ${envFile}:`, error);
}
// Finally, check if there is a generic .env file present
// If so, load it with the override option, so its values take precedence
const genericEnvPath = path.resolve(process.cwd(), ".env");
if (fs.existsSync(genericEnvPath)) {
dotenvConfig({ path: genericEnvPath, override: true });
console.log("Loaded and overrode with generic .env file");
try {
const genericEnvExists = await file(genericEnvPath).exists();
if (genericEnvExists) {
dotenvConfig({ path: genericEnvPath, override: true });
console.log("Loaded and overrode with generic .env file");
}
} catch (error) {
console.warn(`Error checking generic .env file:`, error);
}
}

View File

@@ -1,6 +1,4 @@
import "./polyfills.js";
import { config } from "dotenv";
import { resolve } from "path";
import { file } from "bun";
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { swagger } from "@elysiajs/swagger";
@@ -27,17 +25,11 @@ import {
} from "./commands.js";
import { speechService } from "./speech/index.js";
import { APP_CONFIG } from "./config/app.config.js";
import { loadEnvironmentVariables } from "./config/loadEnv.js";
import { MCP_SCHEMA } from "./mcp/schema.js";
// Load environment variables based on NODE_ENV
const envFile =
process.env.NODE_ENV === "production"
? ".env"
: process.env.NODE_ENV === "test"
? ".env.test"
: ".env.development";
console.log(`Loading environment from ${envFile}`);
config({ path: resolve(process.cwd(), envFile) });
await loadEnvironmentVariables();
// Configuration
const HASS_TOKEN = process.env.HASS_TOKEN;
@@ -126,6 +118,20 @@ const app = new Elysia()
.use(sanitizeInput)
.use(errorHandler);
// Mount API routes
app.get("/api/mcp", () => MCP_SCHEMA);
app.post("/api/mcp/execute", async ({ body }: { body: { tool: string; parameters: Record<string, unknown> } }) => {
const { tool: toolName, parameters } = body;
const tool = tools.find((t) => t.name === toolName);
if (!tool) {
return {
success: false,
message: `Tool '${toolName}' not found`,
};
}
return await tool.execute(parameters);
});
// Health check endpoint
app.get("/health", () => ({
status: "ok",

View File

@@ -21,20 +21,72 @@ export const listDevicesTool: Tool = {
}
const states = (await response.json()) as HassState[];
const devices: Record<string, HassState[]> = {
light: states.filter(state => state.entity_id.startsWith('light.')),
climate: states.filter(state => state.entity_id.startsWith('climate.'))
const devices: Record<string, HassState[]> = {};
// Group devices by domain
states.forEach(state => {
const [domain] = state.entity_id.split('.');
if (!devices[domain]) {
devices[domain] = [];
}
devices[domain].push(state);
});
// Calculate device statistics
const deviceStats = Object.entries(devices).map(([domain, entities]) => {
const activeStates = ['on', 'home', 'unlocked', 'open'];
const active = entities.filter(e => activeStates.includes(e.state)).length;
const uniqueStates = [...new Set(entities.map(e => e.state))];
return {
domain,
count: entities.length,
active,
inactive: entities.length - active,
states: uniqueStates,
sample: entities.slice(0, 2).map(e => ({
id: e.entity_id,
state: e.state,
name: e.attributes?.friendly_name || e.entity_id
}))
};
});
const totalDevices = states.length;
const deviceTypes = Object.keys(devices);
const deviceSummary = {
total_devices: totalDevices,
device_types: deviceTypes,
by_domain: Object.fromEntries(
deviceStats.map(stat => [
stat.domain,
{
count: stat.count,
active: stat.active,
states: stat.states,
sample: stat.sample
}
])
)
};
return {
success: true,
devices,
device_summary: deviceSummary
};
} catch (error) {
console.error('Error in list devices tool:', error);
return {
success: false,
message:
error instanceof Error ? error.message : "Unknown error occurred",
message: error instanceof Error ? error.message : "Unknown error occurred",
devices: {},
device_summary: {
total_devices: 0,
device_types: [],
by_domain: {}
}
};
}
},