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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
src/index.ts
30
src/index.ts
@@ -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",
|
||||
|
||||
@@ -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: {}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user