iobroker.hassemu 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/build/lib/webserver.js +1 -1
- package/build/lib/webserver.js.map +2 -2
- package/build/main.js +11 -19
- package/build/main.js.map +2 -2
- package/io-package.json +27 -1
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -113,13 +113,21 @@ http://<IP>:8123/health
|
|
|
113
113
|
|
|
114
114
|
## Changelog
|
|
115
115
|
|
|
116
|
+
### 1.0.4 (2026-04-12)
|
|
117
|
+
- DRY: remove duplicate NativeConfig interface and redundant config mapping with fallbacks
|
|
118
|
+
- Fix: prevent log spam when redirect URL is not configured (log once at startup, not per request)
|
|
119
|
+
- Tighten `createSession` visibility to private
|
|
120
|
+
|
|
121
|
+
### 1.0.3 (2026-04-12)
|
|
122
|
+
- Remove unused devDependencies, add `no-floating-promises` lint rule, remove redundant CI checkout
|
|
123
|
+
|
|
116
124
|
### 1.0.2 (2026-04-08)
|
|
117
125
|
- Remove build/ from git tracking, fix .gitignore, clean up keywords and metadata
|
|
118
126
|
|
|
119
127
|
### 1.0.0 (2026-04-08)
|
|
120
128
|
- Renamed from homeassistant-bridge to hassemu
|
|
121
129
|
|
|
122
|
-
Older
|
|
130
|
+
Older entries have been moved to [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
|
|
123
131
|
|
|
124
132
|
---
|
|
125
133
|
|
package/build/lib/webserver.js
CHANGED
|
@@ -285,7 +285,7 @@ class WebServer {
|
|
|
285
285
|
});
|
|
286
286
|
this.app.get("/", (_req, res) => {
|
|
287
287
|
if (!this.config.visUrl) {
|
|
288
|
-
this.adapter.log.
|
|
288
|
+
this.adapter.log.debug("No redirect URL configured \u2014 returning error to client");
|
|
289
289
|
this.json(
|
|
290
290
|
res,
|
|
291
291
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/webserver.ts"],
|
|
4
|
-
"sourcesContent": ["import crypto from 'node:crypto';\nimport type { Application, Request, Response, NextFunction } from 'express';\nimport express from 'express';\nimport type { Server } from 'node:http';\nimport { HA_VERSION, SESSION_TTL_MS, CLEANUP_INTERVAL_MS, LOGIN_SCHEMA } from './constants';\nimport type { AdapterConfig, SessionData, AdapterInterface } from './types';\n\n/** Express web server emulating Home Assistant API */\nexport class WebServer {\n private readonly adapter: AdapterInterface;\n private readonly config: AdapterConfig;\n private readonly app: Application;\n private server: Server | null = null;\n public readonly sessions: Map<string, SessionData> = new Map();\n private cleanupTimer: unknown = null;\n public readonly instanceUuid: string;\n\n /**\n * Creates a new WebServer instance\n *\n * @param adapter - Adapter interface for logging and state management\n * @param config - Adapter configuration\n * @param instanceUuid - Shared UUID for consistent identity across WebServer and mDNS\n */\n constructor(adapter: AdapterInterface, config: AdapterConfig, instanceUuid: string) {\n this.adapter = adapter;\n this.config = config;\n this.app = express();\n this.instanceUuid = instanceUuid;\n }\n\n /** Configured service name */\n get serviceName(): string {\n return this.config.serviceName || 'ioBroker';\n }\n\n /** Returns the actual address the server is bound to, or null if not running */\n get boundAddress(): { address: string; port: number } | null {\n if (!this.server) {\n return null;\n }\n const addr = this.server.address();\n if (typeof addr === 'string' || !addr) {\n return null;\n }\n return { address: addr.address, port: addr.port };\n }\n\n // --- Helpers ---\n\n private json(res: Response, data: unknown, status = 200): void {\n res.status(status).json(data);\n }\n\n /**\n * Create a session entry with automatic expiration\n *\n * @param key - Unique session identifier\n */\n createSession(key: string): void {\n this.sessions.set(key, { created: Date.now() });\n }\n\n /** Periodic cleanup of expired sessions */\n public cleanupSessions(): void {\n const now = Date.now();\n let cleaned = 0;\n for (const [key, session] of this.sessions) {\n if (now - session.created > SESSION_TTL_MS) {\n this.sessions.delete(key);\n cleaned++;\n }\n }\n if (cleaned > 0) {\n this.adapter.log.debug(`Session cleanup: removed ${cleaned} expired sessions`);\n }\n }\n\n // --- Middleware ---\n\n private setupMiddleware(): void {\n this.app.use(express.json());\n this.app.use(express.urlencoded({ extended: true }));\n\n // Handle JSON parse errors \u2014 return 400 instead of generic 500\n this.app.use((err: Error, _req: Request, res: Response, next: NextFunction) => {\n if (err instanceof SyntaxError && 'body' in err) {\n this.adapter.log.debug(`Malformed JSON in request: ${err.message}`);\n res.status(400).json({ error: 'Invalid JSON in request body' });\n return;\n }\n next(err);\n });\n\n // Request logging \u2014 use debug level to keep production logs clean\n this.app.use((req: Request, _res: Response, next: NextFunction) => {\n this.adapter.log.debug(`${req.method} ${req.path}`);\n next();\n });\n }\n\n // --- Routes ---\n\n private setupRoutes(): void {\n this.setupApiRoutes();\n this.setupAuthRoutes();\n this.setupMiscRoutes();\n this.setupCatchAll();\n }\n\n private setupApiRoutes(): void {\n // CRITICAL: trailing slash \u2014 HA clients check this endpoint for discovery\n this.app.get('/api/', (_req: Request, res: Response) => {\n this.json(res, { message: 'API running.' });\n });\n\n this.app.get('/api/config', (_req: Request, res: Response) => {\n this.json(res, {\n components: ['http', 'api', 'frontend', 'homeassistant'],\n config_dir: '/config',\n elevation: 0,\n latitude: 0,\n longitude: 0,\n location_name: this.serviceName,\n time_zone: 'UTC',\n unit_system: { length: 'km', mass: 'g', temperature: '\u00B0C', volume: 'L' },\n version: HA_VERSION,\n whitelist_external_dirs: [],\n });\n });\n\n this.app.get('/api/discovery_info', (req: Request, res: Response) => {\n const baseUrl = `http://${req.hostname}:${this.config.port}`;\n this.json(res, {\n base_url: baseUrl,\n external_url: null,\n internal_url: baseUrl,\n location_name: this.serviceName,\n requires_api_password: true,\n uuid: this.instanceUuid,\n version: HA_VERSION,\n });\n });\n\n // Stub-Endpoints for HA-API compatibility\n for (const path of ['/api/states', '/api/services', '/api/events']) {\n this.app.get(path, (_req: Request, res: Response) => this.json(res, []));\n }\n this.app.get('/api/error_log', (_req: Request, res: Response) => this.json(res, ''));\n }\n\n private setupAuthRoutes(): void {\n // Step 0: Auth providers\n this.app.get('/auth/providers', (_req: Request, res: Response) => {\n this.json(res, [\n {\n name: 'Home Assistant Local',\n type: 'homeassistant',\n id: null,\n },\n ]);\n });\n\n // Step 1: Initiate login flow\n this.app.post('/auth/login_flow', (_req: Request, res: Response) => {\n const flowId = crypto.randomUUID();\n this.createSession(flowId);\n this.adapter.log.debug(`Auth flow created: ${flowId}`);\n\n this.json(res, {\n type: 'form',\n flow_id: flowId,\n handler: ['homeassistant', null],\n step_id: 'init',\n data_schema: LOGIN_SCHEMA,\n description_placeholders: null,\n errors: null,\n });\n });\n\n // Step 2: Submit credentials\n this.app.post('/auth/login_flow/:flowId', (req: Request, res: Response) => {\n const flowId = req.params.flowId as string;\n\n if (!this.sessions.has(flowId)) {\n this.adapter.log.warn(`Unknown flow_id: ${flowId}`);\n this.json(\n res,\n {\n type: 'abort',\n flow_id: flowId,\n reason: 'unknown_flow',\n },\n 400,\n );\n return;\n }\n\n // Validate credentials if auth is enabled\n if (this.config.authRequired) {\n const { username, password } = req.body as { username?: string; password?: string };\n if (username !== this.config.username || password !== this.config.password) {\n this.adapter.log.warn('Invalid credentials');\n this.json(\n res,\n {\n type: 'form',\n flow_id: flowId,\n handler: ['homeassistant', null],\n step_id: 'init',\n data_schema: LOGIN_SCHEMA,\n errors: { base: 'invalid_auth' },\n description_placeholders: null,\n },\n 400,\n );\n return;\n }\n }\n\n // Auth OK \u2014 generate code\n this.sessions.delete(flowId);\n const code = crypto.randomUUID();\n this.createSession(code);\n this.adapter.log.debug('Auth flow completed \u2014 code issued');\n\n this.json(res, {\n version: 1,\n type: 'create_entry',\n flow_id: flowId,\n handler: ['homeassistant', null],\n result: code,\n description: null,\n description_placeholders: null,\n });\n });\n\n // Step 3: Exchange code for token\n this.app.post('/auth/token', (req: Request, res: Response) => {\n const { code, grant_type } = req.body as { code?: string; grant_type?: string };\n\n if (grant_type === 'authorization_code' && code && this.sessions.has(code)) {\n this.sessions.delete(code);\n this.adapter.log.debug('Display authenticated successfully');\n\n this.json(res, {\n access_token: crypto.randomUUID(),\n token_type: 'Bearer',\n refresh_token: crypto.randomUUID(),\n expires_in: 1800,\n });\n return;\n }\n\n // Refresh token \u2014 issue new token\n if (grant_type === 'refresh_token') {\n this.json(res, {\n access_token: crypto.randomUUID(),\n token_type: 'Bearer',\n expires_in: 1800,\n });\n return;\n }\n\n this.adapter.log.warn(`Token exchange failed: grant_type=${grant_type}`);\n this.json(\n res,\n {\n error: 'invalid_request',\n error_description: 'Invalid or expired code',\n },\n 400,\n );\n });\n }\n\n private setupMiscRoutes(): void {\n this.app.get('/health', (_req: Request, res: Response) => {\n this.json(res, {\n status: 'ok',\n adapter: 'hassemu',\n version: HA_VERSION,\n config: {\n mdns: this.config.mdnsEnabled,\n auth: this.config.authRequired,\n redirectTo: this.config.visUrl,\n },\n });\n });\n\n this.app.get('/manifest.json', (_req: Request, res: Response) => {\n this.json(res, {\n name: this.serviceName,\n short_name: this.serviceName,\n start_url: '/',\n display: 'standalone',\n background_color: '#ffffff',\n theme_color: '#03a9f4',\n });\n });\n\n // Redirect \u2014 Display WebView follows 302 natively\n this.app.get('/', (_req: Request, res: Response) => {\n if (!this.config.visUrl) {\n this.adapter.log.error('No redirect URL configured!');\n this.json(\n res,\n {\n error: 'No redirect URL configured',\n message: 'Please configure a redirect URL in the adapter settings.',\n },\n 500,\n );\n return;\n }\n this.adapter.log.debug(`Redirecting to: ${this.config.visUrl}`);\n res.redirect(this.config.visUrl);\n });\n }\n\n private setupCatchAll(): void {\n this.app.use((req: Request, res: Response) => {\n this.adapter.log.debug(`404: ${req.method} ${req.path}`);\n this.json(res, { error: 'Not Found', path: req.path }, 404);\n });\n }\n\n // --- Lifecycle ---\n\n /** Start the web server and session cleanup timer */\n async start(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.setupMiddleware();\n this.setupRoutes();\n\n const bindAddress = this.config.bindAddress || '0.0.0.0';\n this.server = this.app.listen(this.config.port, bindAddress, () => {\n this.adapter.log.debug(`Web server listening on ${bindAddress}:${this.config.port}`);\n resolve();\n });\n\n this.server.on('error', (error: NodeJS.ErrnoException) => {\n const msg =\n error.code === 'EADDRINUSE'\n ? `Port ${this.config.port} is already in use!`\n : `Server error: ${error.message}`;\n this.adapter.log.error(msg);\n reject(error);\n });\n\n // Session cleanup timer (adapter-managed for automatic cleanup on unload)\n this.cleanupTimer = this.adapter.setInterval(() => this.cleanupSessions(), CLEANUP_INTERVAL_MS);\n });\n }\n\n /** Stop the web server and cleanup timer */\n async stop(): Promise<void> {\n if (this.cleanupTimer) {\n this.adapter.clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n\n return new Promise(resolve => {\n if (this.server) {\n this.server.close(() => {\n this.adapter.log.debug('Web server stopped');\n this.server = null;\n resolve();\n });\n } else {\n resolve();\n }\n });\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAmB;AAEnB,qBAAoB;AAEpB,uBAA8E;AAIvE,MAAM,UAAU;AAAA,EACF;AAAA,EACA;AAAA,EACA;AAAA,EACT,SAAwB;AAAA,EAChB,WAAqC,oBAAI,IAAI;AAAA,EACrD,eAAwB;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAShB,YAAY,SAA2B,QAAuB,cAAsB;AAChF,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,UAAM,eAAAA,SAAQ;AACnB,SAAK,eAAe;AAAA,EACxB;AAAA;AAAA,EAGA,IAAI,cAAsB;AACtB,WAAO,KAAK,OAAO,eAAe;AAAA,EACtC;AAAA;AAAA,EAGA,IAAI,eAAyD;AACzD,QAAI,CAAC,KAAK,QAAQ;AACd,aAAO;AAAA,IACX;AACA,UAAM,OAAO,KAAK,OAAO,QAAQ;AACjC,QAAI,OAAO,SAAS,YAAY,CAAC,MAAM;AACnC,aAAO;AAAA,IACX;AACA,WAAO,EAAE,SAAS,KAAK,SAAS,MAAM,KAAK,KAAK;AAAA,EACpD;AAAA;AAAA,EAIQ,KAAK,KAAe,MAAe,SAAS,KAAW;AAC3D,QAAI,OAAO,MAAM,EAAE,KAAK,IAAI;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto';\nimport type { Application, Request, Response, NextFunction } from 'express';\nimport express from 'express';\nimport type { Server } from 'node:http';\nimport { HA_VERSION, SESSION_TTL_MS, CLEANUP_INTERVAL_MS, LOGIN_SCHEMA } from './constants';\nimport type { AdapterConfig, SessionData, AdapterInterface } from './types';\n\n/** Express web server emulating Home Assistant API */\nexport class WebServer {\n private readonly adapter: AdapterInterface;\n private readonly config: AdapterConfig;\n private readonly app: Application;\n private server: Server | null = null;\n public readonly sessions: Map<string, SessionData> = new Map();\n private cleanupTimer: unknown = null;\n public readonly instanceUuid: string;\n\n /**\n * Creates a new WebServer instance\n *\n * @param adapter - Adapter interface for logging and state management\n * @param config - Adapter configuration\n * @param instanceUuid - Shared UUID for consistent identity across WebServer and mDNS\n */\n constructor(adapter: AdapterInterface, config: AdapterConfig, instanceUuid: string) {\n this.adapter = adapter;\n this.config = config;\n this.app = express();\n this.instanceUuid = instanceUuid;\n }\n\n /** Configured service name */\n get serviceName(): string {\n return this.config.serviceName || 'ioBroker';\n }\n\n /** Returns the actual address the server is bound to, or null if not running */\n get boundAddress(): { address: string; port: number } | null {\n if (!this.server) {\n return null;\n }\n const addr = this.server.address();\n if (typeof addr === 'string' || !addr) {\n return null;\n }\n return { address: addr.address, port: addr.port };\n }\n\n // --- Helpers ---\n\n private json(res: Response, data: unknown, status = 200): void {\n res.status(status).json(data);\n }\n\n /**\n * Create a session entry with automatic expiration\n *\n * @param key - Unique session identifier\n */\n private createSession(key: string): void {\n this.sessions.set(key, { created: Date.now() });\n }\n\n /** Periodic cleanup of expired sessions */\n public cleanupSessions(): void {\n const now = Date.now();\n let cleaned = 0;\n for (const [key, session] of this.sessions) {\n if (now - session.created > SESSION_TTL_MS) {\n this.sessions.delete(key);\n cleaned++;\n }\n }\n if (cleaned > 0) {\n this.adapter.log.debug(`Session cleanup: removed ${cleaned} expired sessions`);\n }\n }\n\n // --- Middleware ---\n\n private setupMiddleware(): void {\n this.app.use(express.json());\n this.app.use(express.urlencoded({ extended: true }));\n\n // Handle JSON parse errors \u2014 return 400 instead of generic 500\n this.app.use((err: Error, _req: Request, res: Response, next: NextFunction) => {\n if (err instanceof SyntaxError && 'body' in err) {\n this.adapter.log.debug(`Malformed JSON in request: ${err.message}`);\n res.status(400).json({ error: 'Invalid JSON in request body' });\n return;\n }\n next(err);\n });\n\n // Request logging \u2014 use debug level to keep production logs clean\n this.app.use((req: Request, _res: Response, next: NextFunction) => {\n this.adapter.log.debug(`${req.method} ${req.path}`);\n next();\n });\n }\n\n // --- Routes ---\n\n private setupRoutes(): void {\n this.setupApiRoutes();\n this.setupAuthRoutes();\n this.setupMiscRoutes();\n this.setupCatchAll();\n }\n\n private setupApiRoutes(): void {\n // CRITICAL: trailing slash \u2014 HA clients check this endpoint for discovery\n this.app.get('/api/', (_req: Request, res: Response) => {\n this.json(res, { message: 'API running.' });\n });\n\n this.app.get('/api/config', (_req: Request, res: Response) => {\n this.json(res, {\n components: ['http', 'api', 'frontend', 'homeassistant'],\n config_dir: '/config',\n elevation: 0,\n latitude: 0,\n longitude: 0,\n location_name: this.serviceName,\n time_zone: 'UTC',\n unit_system: { length: 'km', mass: 'g', temperature: '\u00B0C', volume: 'L' },\n version: HA_VERSION,\n whitelist_external_dirs: [],\n });\n });\n\n this.app.get('/api/discovery_info', (req: Request, res: Response) => {\n const baseUrl = `http://${req.hostname}:${this.config.port}`;\n this.json(res, {\n base_url: baseUrl,\n external_url: null,\n internal_url: baseUrl,\n location_name: this.serviceName,\n requires_api_password: true,\n uuid: this.instanceUuid,\n version: HA_VERSION,\n });\n });\n\n // Stub-Endpoints for HA-API compatibility\n for (const path of ['/api/states', '/api/services', '/api/events']) {\n this.app.get(path, (_req: Request, res: Response) => this.json(res, []));\n }\n this.app.get('/api/error_log', (_req: Request, res: Response) => this.json(res, ''));\n }\n\n private setupAuthRoutes(): void {\n // Step 0: Auth providers\n this.app.get('/auth/providers', (_req: Request, res: Response) => {\n this.json(res, [\n {\n name: 'Home Assistant Local',\n type: 'homeassistant',\n id: null,\n },\n ]);\n });\n\n // Step 1: Initiate login flow\n this.app.post('/auth/login_flow', (_req: Request, res: Response) => {\n const flowId = crypto.randomUUID();\n this.createSession(flowId);\n this.adapter.log.debug(`Auth flow created: ${flowId}`);\n\n this.json(res, {\n type: 'form',\n flow_id: flowId,\n handler: ['homeassistant', null],\n step_id: 'init',\n data_schema: LOGIN_SCHEMA,\n description_placeholders: null,\n errors: null,\n });\n });\n\n // Step 2: Submit credentials\n this.app.post('/auth/login_flow/:flowId', (req: Request, res: Response) => {\n const flowId = req.params.flowId as string;\n\n if (!this.sessions.has(flowId)) {\n this.adapter.log.warn(`Unknown flow_id: ${flowId}`);\n this.json(\n res,\n {\n type: 'abort',\n flow_id: flowId,\n reason: 'unknown_flow',\n },\n 400,\n );\n return;\n }\n\n // Validate credentials if auth is enabled\n if (this.config.authRequired) {\n const { username, password } = req.body as { username?: string; password?: string };\n if (username !== this.config.username || password !== this.config.password) {\n this.adapter.log.warn('Invalid credentials');\n this.json(\n res,\n {\n type: 'form',\n flow_id: flowId,\n handler: ['homeassistant', null],\n step_id: 'init',\n data_schema: LOGIN_SCHEMA,\n errors: { base: 'invalid_auth' },\n description_placeholders: null,\n },\n 400,\n );\n return;\n }\n }\n\n // Auth OK \u2014 generate code\n this.sessions.delete(flowId);\n const code = crypto.randomUUID();\n this.createSession(code);\n this.adapter.log.debug('Auth flow completed \u2014 code issued');\n\n this.json(res, {\n version: 1,\n type: 'create_entry',\n flow_id: flowId,\n handler: ['homeassistant', null],\n result: code,\n description: null,\n description_placeholders: null,\n });\n });\n\n // Step 3: Exchange code for token\n this.app.post('/auth/token', (req: Request, res: Response) => {\n const { code, grant_type } = req.body as { code?: string; grant_type?: string };\n\n if (grant_type === 'authorization_code' && code && this.sessions.has(code)) {\n this.sessions.delete(code);\n this.adapter.log.debug('Display authenticated successfully');\n\n this.json(res, {\n access_token: crypto.randomUUID(),\n token_type: 'Bearer',\n refresh_token: crypto.randomUUID(),\n expires_in: 1800,\n });\n return;\n }\n\n // Refresh token \u2014 issue new token\n if (grant_type === 'refresh_token') {\n this.json(res, {\n access_token: crypto.randomUUID(),\n token_type: 'Bearer',\n expires_in: 1800,\n });\n return;\n }\n\n this.adapter.log.warn(`Token exchange failed: grant_type=${grant_type}`);\n this.json(\n res,\n {\n error: 'invalid_request',\n error_description: 'Invalid or expired code',\n },\n 400,\n );\n });\n }\n\n private setupMiscRoutes(): void {\n this.app.get('/health', (_req: Request, res: Response) => {\n this.json(res, {\n status: 'ok',\n adapter: 'hassemu',\n version: HA_VERSION,\n config: {\n mdns: this.config.mdnsEnabled,\n auth: this.config.authRequired,\n redirectTo: this.config.visUrl,\n },\n });\n });\n\n this.app.get('/manifest.json', (_req: Request, res: Response) => {\n this.json(res, {\n name: this.serviceName,\n short_name: this.serviceName,\n start_url: '/',\n display: 'standalone',\n background_color: '#ffffff',\n theme_color: '#03a9f4',\n });\n });\n\n // Redirect \u2014 Display WebView follows 302 natively\n this.app.get('/', (_req: Request, res: Response) => {\n if (!this.config.visUrl) {\n this.adapter.log.debug('No redirect URL configured \u2014 returning error to client');\n this.json(\n res,\n {\n error: 'No redirect URL configured',\n message: 'Please configure a redirect URL in the adapter settings.',\n },\n 500,\n );\n return;\n }\n this.adapter.log.debug(`Redirecting to: ${this.config.visUrl}`);\n res.redirect(this.config.visUrl);\n });\n }\n\n private setupCatchAll(): void {\n this.app.use((req: Request, res: Response) => {\n this.adapter.log.debug(`404: ${req.method} ${req.path}`);\n this.json(res, { error: 'Not Found', path: req.path }, 404);\n });\n }\n\n // --- Lifecycle ---\n\n /** Start the web server and session cleanup timer */\n async start(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.setupMiddleware();\n this.setupRoutes();\n\n const bindAddress = this.config.bindAddress || '0.0.0.0';\n this.server = this.app.listen(this.config.port, bindAddress, () => {\n this.adapter.log.debug(`Web server listening on ${bindAddress}:${this.config.port}`);\n resolve();\n });\n\n this.server.on('error', (error: NodeJS.ErrnoException) => {\n const msg =\n error.code === 'EADDRINUSE'\n ? `Port ${this.config.port} is already in use!`\n : `Server error: ${error.message}`;\n this.adapter.log.error(msg);\n reject(error);\n });\n\n // Session cleanup timer (adapter-managed for automatic cleanup on unload)\n this.cleanupTimer = this.adapter.setInterval(() => this.cleanupSessions(), CLEANUP_INTERVAL_MS);\n });\n }\n\n /** Stop the web server and cleanup timer */\n async stop(): Promise<void> {\n if (this.cleanupTimer) {\n this.adapter.clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n\n return new Promise(resolve => {\n if (this.server) {\n this.server.close(() => {\n this.adapter.log.debug('Web server stopped');\n this.server = null;\n resolve();\n });\n } else {\n resolve();\n }\n });\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAmB;AAEnB,qBAAoB;AAEpB,uBAA8E;AAIvE,MAAM,UAAU;AAAA,EACF;AAAA,EACA;AAAA,EACA;AAAA,EACT,SAAwB;AAAA,EAChB,WAAqC,oBAAI,IAAI;AAAA,EACrD,eAAwB;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAShB,YAAY,SAA2B,QAAuB,cAAsB;AAChF,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,UAAM,eAAAA,SAAQ;AACnB,SAAK,eAAe;AAAA,EACxB;AAAA;AAAA,EAGA,IAAI,cAAsB;AACtB,WAAO,KAAK,OAAO,eAAe;AAAA,EACtC;AAAA;AAAA,EAGA,IAAI,eAAyD;AACzD,QAAI,CAAC,KAAK,QAAQ;AACd,aAAO;AAAA,IACX;AACA,UAAM,OAAO,KAAK,OAAO,QAAQ;AACjC,QAAI,OAAO,SAAS,YAAY,CAAC,MAAM;AACnC,aAAO;AAAA,IACX;AACA,WAAO,EAAE,SAAS,KAAK,SAAS,MAAM,KAAK,KAAK;AAAA,EACpD;AAAA;AAAA,EAIQ,KAAK,KAAe,MAAe,SAAS,KAAW;AAC3D,QAAI,OAAO,MAAM,EAAE,KAAK,IAAI;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,cAAc,KAAmB;AACrC,SAAK,SAAS,IAAI,KAAK,EAAE,SAAS,KAAK,IAAI,EAAE,CAAC;AAAA,EAClD;AAAA;AAAA,EAGO,kBAAwB;AAC3B,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,UAAU;AACd,eAAW,CAAC,KAAK,OAAO,KAAK,KAAK,UAAU;AACxC,UAAI,MAAM,QAAQ,UAAU,iCAAgB;AACxC,aAAK,SAAS,OAAO,GAAG;AACxB;AAAA,MACJ;AAAA,IACJ;AACA,QAAI,UAAU,GAAG;AACb,WAAK,QAAQ,IAAI,MAAM,4BAA4B,OAAO,mBAAmB;AAAA,IACjF;AAAA,EACJ;AAAA;AAAA,EAIQ,kBAAwB;AAC5B,SAAK,IAAI,IAAI,eAAAA,QAAQ,KAAK,CAAC;AAC3B,SAAK,IAAI,IAAI,eAAAA,QAAQ,WAAW,EAAE,UAAU,KAAK,CAAC,CAAC;AAGnD,SAAK,IAAI,IAAI,CAAC,KAAY,MAAe,KAAe,SAAuB;AAC3E,UAAI,eAAe,eAAe,UAAU,KAAK;AAC7C,aAAK,QAAQ,IAAI,MAAM,8BAA8B,IAAI,OAAO,EAAE;AAClE,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,+BAA+B,CAAC;AAC9D;AAAA,MACJ;AACA,WAAK,GAAG;AAAA,IACZ,CAAC;AAGD,SAAK,IAAI,IAAI,CAAC,KAAc,MAAgB,SAAuB;AAC/D,WAAK,QAAQ,IAAI,MAAM,GAAG,IAAI,MAAM,IAAI,IAAI,IAAI,EAAE;AAClD,WAAK;AAAA,IACT,CAAC;AAAA,EACL;AAAA;AAAA,EAIQ,cAAoB;AACxB,SAAK,eAAe;AACpB,SAAK,gBAAgB;AACrB,SAAK,gBAAgB;AACrB,SAAK,cAAc;AAAA,EACvB;AAAA,EAEQ,iBAAuB;AAE3B,SAAK,IAAI,IAAI,SAAS,CAAC,MAAe,QAAkB;AACpD,WAAK,KAAK,KAAK,EAAE,SAAS,eAAe,CAAC;AAAA,IAC9C,CAAC;AAED,SAAK,IAAI,IAAI,eAAe,CAAC,MAAe,QAAkB;AAC1D,WAAK,KAAK,KAAK;AAAA,QACX,YAAY,CAAC,QAAQ,OAAO,YAAY,eAAe;AAAA,QACvD,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,UAAU;AAAA,QACV,WAAW;AAAA,QACX,eAAe,KAAK;AAAA,QACpB,WAAW;AAAA,QACX,aAAa,EAAE,QAAQ,MAAM,MAAM,KAAK,aAAa,SAAM,QAAQ,IAAI;AAAA,QACvE,SAAS;AAAA,QACT,yBAAyB,CAAC;AAAA,MAC9B,CAAC;AAAA,IACL,CAAC;AAED,SAAK,IAAI,IAAI,uBAAuB,CAAC,KAAc,QAAkB;AACjE,YAAM,UAAU,UAAU,IAAI,QAAQ,IAAI,KAAK,OAAO,IAAI;AAC1D,WAAK,KAAK,KAAK;AAAA,QACX,UAAU;AAAA,QACV,cAAc;AAAA,QACd,cAAc;AAAA,QACd,eAAe,KAAK;AAAA,QACpB,uBAAuB;AAAA,QACvB,MAAM,KAAK;AAAA,QACX,SAAS;AAAA,MACb,CAAC;AAAA,IACL,CAAC;AAGD,eAAW,QAAQ,CAAC,eAAe,iBAAiB,aAAa,GAAG;AAChE,WAAK,IAAI,IAAI,MAAM,CAAC,MAAe,QAAkB,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC;AAAA,IAC3E;AACA,SAAK,IAAI,IAAI,kBAAkB,CAAC,MAAe,QAAkB,KAAK,KAAK,KAAK,EAAE,CAAC;AAAA,EACvF;AAAA,EAEQ,kBAAwB;AAE5B,SAAK,IAAI,IAAI,mBAAmB,CAAC,MAAe,QAAkB;AAC9D,WAAK,KAAK,KAAK;AAAA,QACX;AAAA,UACI,MAAM;AAAA,UACN,MAAM;AAAA,UACN,IAAI;AAAA,QACR;AAAA,MACJ,CAAC;AAAA,IACL,CAAC;AAGD,SAAK,IAAI,KAAK,oBAAoB,CAAC,MAAe,QAAkB;AAChE,YAAM,SAAS,mBAAAC,QAAO,WAAW;AACjC,WAAK,cAAc,MAAM;AACzB,WAAK,QAAQ,IAAI,MAAM,sBAAsB,MAAM,EAAE;AAErD,WAAK,KAAK,KAAK;AAAA,QACX,MAAM;AAAA,QACN,SAAS;AAAA,QACT,SAAS,CAAC,iBAAiB,IAAI;AAAA,QAC/B,SAAS;AAAA,QACT,aAAa;AAAA,QACb,0BAA0B;AAAA,QAC1B,QAAQ;AAAA,MACZ,CAAC;AAAA,IACL,CAAC;AAGD,SAAK,IAAI,KAAK,4BAA4B,CAAC,KAAc,QAAkB;AACvE,YAAM,SAAS,IAAI,OAAO;AAE1B,UAAI,CAAC,KAAK,SAAS,IAAI,MAAM,GAAG;AAC5B,aAAK,QAAQ,IAAI,KAAK,oBAAoB,MAAM,EAAE;AAClD,aAAK;AAAA,UACD;AAAA,UACA;AAAA,YACI,MAAM;AAAA,YACN,SAAS;AAAA,YACT,QAAQ;AAAA,UACZ;AAAA,UACA;AAAA,QACJ;AACA;AAAA,MACJ;AAGA,UAAI,KAAK,OAAO,cAAc;AAC1B,cAAM,EAAE,UAAU,SAAS,IAAI,IAAI;AACnC,YAAI,aAAa,KAAK,OAAO,YAAY,aAAa,KAAK,OAAO,UAAU;AACxE,eAAK,QAAQ,IAAI,KAAK,qBAAqB;AAC3C,eAAK;AAAA,YACD;AAAA,YACA;AAAA,cACI,MAAM;AAAA,cACN,SAAS;AAAA,cACT,SAAS,CAAC,iBAAiB,IAAI;AAAA,cAC/B,SAAS;AAAA,cACT,aAAa;AAAA,cACb,QAAQ,EAAE,MAAM,eAAe;AAAA,cAC/B,0BAA0B;AAAA,YAC9B;AAAA,YACA;AAAA,UACJ;AACA;AAAA,QACJ;AAAA,MACJ;AAGA,WAAK,SAAS,OAAO,MAAM;AAC3B,YAAM,OAAO,mBAAAA,QAAO,WAAW;AAC/B,WAAK,cAAc,IAAI;AACvB,WAAK,QAAQ,IAAI,MAAM,wCAAmC;AAE1D,WAAK,KAAK,KAAK;AAAA,QACX,SAAS;AAAA,QACT,MAAM;AAAA,QACN,SAAS;AAAA,QACT,SAAS,CAAC,iBAAiB,IAAI;AAAA,QAC/B,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,0BAA0B;AAAA,MAC9B,CAAC;AAAA,IACL,CAAC;AAGD,SAAK,IAAI,KAAK,eAAe,CAAC,KAAc,QAAkB;AAC1D,YAAM,EAAE,MAAM,WAAW,IAAI,IAAI;AAEjC,UAAI,eAAe,wBAAwB,QAAQ,KAAK,SAAS,IAAI,IAAI,GAAG;AACxE,aAAK,SAAS,OAAO,IAAI;AACzB,aAAK,QAAQ,IAAI,MAAM,oCAAoC;AAE3D,aAAK,KAAK,KAAK;AAAA,UACX,cAAc,mBAAAA,QAAO,WAAW;AAAA,UAChC,YAAY;AAAA,UACZ,eAAe,mBAAAA,QAAO,WAAW;AAAA,UACjC,YAAY;AAAA,QAChB,CAAC;AACD;AAAA,MACJ;AAGA,UAAI,eAAe,iBAAiB;AAChC,aAAK,KAAK,KAAK;AAAA,UACX,cAAc,mBAAAA,QAAO,WAAW;AAAA,UAChC,YAAY;AAAA,UACZ,YAAY;AAAA,QAChB,CAAC;AACD;AAAA,MACJ;AAEA,WAAK,QAAQ,IAAI,KAAK,qCAAqC,UAAU,EAAE;AACvE,WAAK;AAAA,QACD;AAAA,QACA;AAAA,UACI,OAAO;AAAA,UACP,mBAAmB;AAAA,QACvB;AAAA,QACA;AAAA,MACJ;AAAA,IACJ,CAAC;AAAA,EACL;AAAA,EAEQ,kBAAwB;AAC5B,SAAK,IAAI,IAAI,WAAW,CAAC,MAAe,QAAkB;AACtD,WAAK,KAAK,KAAK;AAAA,QACX,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,UACJ,MAAM,KAAK,OAAO;AAAA,UAClB,MAAM,KAAK,OAAO;AAAA,UAClB,YAAY,KAAK,OAAO;AAAA,QAC5B;AAAA,MACJ,CAAC;AAAA,IACL,CAAC;AAED,SAAK,IAAI,IAAI,kBAAkB,CAAC,MAAe,QAAkB;AAC7D,WAAK,KAAK,KAAK;AAAA,QACX,MAAM,KAAK;AAAA,QACX,YAAY,KAAK;AAAA,QACjB,WAAW;AAAA,QACX,SAAS;AAAA,QACT,kBAAkB;AAAA,QAClB,aAAa;AAAA,MACjB,CAAC;AAAA,IACL,CAAC;AAGD,SAAK,IAAI,IAAI,KAAK,CAAC,MAAe,QAAkB;AAChD,UAAI,CAAC,KAAK,OAAO,QAAQ;AACrB,aAAK,QAAQ,IAAI,MAAM,6DAAwD;AAC/E,aAAK;AAAA,UACD;AAAA,UACA;AAAA,YACI,OAAO;AAAA,YACP,SAAS;AAAA,UACb;AAAA,UACA;AAAA,QACJ;AACA;AAAA,MACJ;AACA,WAAK,QAAQ,IAAI,MAAM,mBAAmB,KAAK,OAAO,MAAM,EAAE;AAC9D,UAAI,SAAS,KAAK,OAAO,MAAM;AAAA,IACnC,CAAC;AAAA,EACL;AAAA,EAEQ,gBAAsB;AAC1B,SAAK,IAAI,IAAI,CAAC,KAAc,QAAkB;AAC1C,WAAK,QAAQ,IAAI,MAAM,QAAQ,IAAI,MAAM,IAAI,IAAI,IAAI,EAAE;AACvD,WAAK,KAAK,KAAK,EAAE,OAAO,aAAa,MAAM,IAAI,KAAK,GAAG,GAAG;AAAA,IAC9D,CAAC;AAAA,EACL;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AACzB,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACpC,WAAK,gBAAgB;AACrB,WAAK,YAAY;AAEjB,YAAM,cAAc,KAAK,OAAO,eAAe;AAC/C,WAAK,SAAS,KAAK,IAAI,OAAO,KAAK,OAAO,MAAM,aAAa,MAAM;AAC/D,aAAK,QAAQ,IAAI,MAAM,2BAA2B,WAAW,IAAI,KAAK,OAAO,IAAI,EAAE;AACnF,gBAAQ;AAAA,MACZ,CAAC;AAED,WAAK,OAAO,GAAG,SAAS,CAAC,UAAiC;AACtD,cAAM,MACF,MAAM,SAAS,eACT,QAAQ,KAAK,OAAO,IAAI,wBACxB,iBAAiB,MAAM,OAAO;AACxC,aAAK,QAAQ,IAAI,MAAM,GAAG;AAC1B,eAAO,KAAK;AAAA,MAChB,CAAC;AAGD,WAAK,eAAe,KAAK,QAAQ,YAAY,MAAM,KAAK,gBAAgB,GAAG,oCAAmB;AAAA,IAClG,CAAC;AAAA,EACL;AAAA;AAAA,EAGA,MAAM,OAAsB;AACxB,QAAI,KAAK,cAAc;AACnB,WAAK,QAAQ,cAAc,KAAK,YAAY;AAC5C,WAAK,eAAe;AAAA,IACxB;AAEA,WAAO,IAAI,QAAQ,aAAW;AAC1B,UAAI,KAAK,QAAQ;AACb,aAAK,OAAO,MAAM,MAAM;AACpB,eAAK,QAAQ,IAAI,MAAM,oBAAoB;AAC3C,eAAK,SAAS;AACd,kBAAQ;AAAA,QACZ,CAAC;AAAA,MACL,OAAO;AACH,gBAAQ;AAAA,MACZ;AAAA,IACJ,CAAC;AAAA,EACL;AACJ;",
|
|
6
6
|
"names": ["express", "crypto"]
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -42,38 +42,30 @@ class HassEmu extends utils.Adapter {
|
|
|
42
42
|
if (!this.config.visUrl) {
|
|
43
43
|
this.log.error("No redirect URL configured! Please configure a URL in the adapter settings.");
|
|
44
44
|
}
|
|
45
|
-
const config = {
|
|
46
|
-
port: this.config.port || 8123,
|
|
47
|
-
bindAddress: this.config.bindAddress || "0.0.0.0",
|
|
48
|
-
visUrl: this.config.visUrl || "",
|
|
49
|
-
authRequired: this.config.authRequired === true,
|
|
50
|
-
username: this.config.username || "admin",
|
|
51
|
-
password: this.config.password || "",
|
|
52
|
-
mdnsEnabled: this.config.mdnsEnabled !== false,
|
|
53
|
-
serviceName: this.config.serviceName || "ioBroker"
|
|
54
|
-
};
|
|
55
45
|
const instanceUuid = import_node_crypto.default.randomUUID();
|
|
56
|
-
this.log.debug(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
this.log.debug(
|
|
47
|
+
`Config: port=${this.config.port}, auth=${this.config.authRequired}, mdns=${this.config.mdnsEnabled}`
|
|
48
|
+
);
|
|
49
|
+
if (this.config.visUrl) {
|
|
50
|
+
this.log.debug(`Target URL: ${this.config.visUrl}`);
|
|
51
|
+
if (/\blocalhost\b|127\.0\.0\.1/.test(this.config.visUrl)) {
|
|
60
52
|
this.log.warn(
|
|
61
53
|
"visUrl contains localhost \u2014 the display cannot reach this! Use the real IP address."
|
|
62
54
|
);
|
|
63
55
|
}
|
|
64
56
|
}
|
|
65
|
-
this.webServer = new import_webserver.WebServer(this, config, instanceUuid);
|
|
57
|
+
this.webServer = new import_webserver.WebServer(this, this.config, instanceUuid);
|
|
66
58
|
await this.webServer.start();
|
|
67
|
-
if (config.mdnsEnabled) {
|
|
68
|
-
this.mdnsService = new import_mdns.MDNSService(this, config, instanceUuid);
|
|
59
|
+
if (this.config.mdnsEnabled) {
|
|
60
|
+
this.mdnsService = new import_mdns.MDNSService(this, this.config, instanceUuid);
|
|
69
61
|
this.mdnsService.start();
|
|
70
62
|
} else {
|
|
71
63
|
this.log.debug("mDNS disabled \u2014 enter URL manually on the display");
|
|
72
64
|
}
|
|
73
65
|
await this.setStateAsync("info.connection", true, true);
|
|
74
|
-
const bindAddr = config.bindAddress || "0.0.0.0";
|
|
66
|
+
const bindAddr = this.config.bindAddress || "0.0.0.0";
|
|
75
67
|
this.log.info(
|
|
76
|
-
`HA emulation running on ${bindAddr}:${config.port}${config.mdnsEnabled ? ", mDNS active" : ""}`
|
|
68
|
+
`HA emulation running on ${bindAddr}:${this.config.port}${this.config.mdnsEnabled ? ", mDNS active" : ""}`
|
|
77
69
|
);
|
|
78
70
|
} catch (error) {
|
|
79
71
|
const err = error;
|
package/build/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["import crypto from 'node:crypto';\nimport * as utils from '@iobroker/adapter-core';\nimport { MDNSService } from './lib/mdns';\nimport { WebServer } from './lib/webserver';\nimport type { AdapterConfig } from './lib/types';\n\
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAAA,yBAAmB;AACnB,YAAuB;AACvB,kBAA4B;AAC5B,uBAA0B;
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto';\nimport * as utils from '@iobroker/adapter-core';\nimport { MDNSService } from './lib/mdns';\nimport { WebServer } from './lib/webserver';\nimport type { AdapterConfig } from './lib/types';\n\nclass HassEmu extends utils.Adapter {\n private mdnsService: MDNSService | null = null;\n private webServer: WebServer | null = null;\n\n declare config: AdapterConfig;\n\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: 'hassemu',\n });\n\n this.on('ready', this.onReady.bind(this));\n this.on('unload', this.onUnload.bind(this));\n }\n\n private async onReady(): Promise<void> {\n try {\n await this.setStateAsync('info.connection', false, true);\n\n if (!this.config.visUrl) {\n this.log.error('No redirect URL configured! Please configure a URL in the adapter settings.');\n }\n\n const instanceUuid = crypto.randomUUID();\n\n this.log.debug(\n `Config: port=${this.config.port}, auth=${this.config.authRequired}, mdns=${this.config.mdnsEnabled}`,\n );\n\n if (this.config.visUrl) {\n this.log.debug(`Target URL: ${this.config.visUrl}`);\n\n if (/\\blocalhost\\b|127\\.0\\.0\\.1/.test(this.config.visUrl)) {\n this.log.warn(\n 'visUrl contains localhost \u2014 the display cannot reach this! Use the real IP address.',\n );\n }\n }\n\n this.webServer = new WebServer(this, this.config, instanceUuid);\n await this.webServer.start();\n\n if (this.config.mdnsEnabled) {\n this.mdnsService = new MDNSService(this, this.config, instanceUuid);\n this.mdnsService.start();\n } else {\n this.log.debug('mDNS disabled \u2014 enter URL manually on the display');\n }\n\n await this.setStateAsync('info.connection', true, true);\n const bindAddr = this.config.bindAddress || '0.0.0.0';\n this.log.info(\n `HA emulation running on ${bindAddr}:${this.config.port}${this.config.mdnsEnabled ? ', mDNS active' : ''}`,\n );\n } catch (error) {\n const err = error as Error;\n this.log.error(`Failed to start: ${err.message}`);\n if (err.stack) {\n this.log.debug(err.stack);\n }\n }\n }\n\n private onUnload(callback: () => void): void {\n try {\n if (this.mdnsService) {\n this.mdnsService.stop();\n this.mdnsService = null;\n }\n\n if (this.webServer) {\n this.webServer.stop().catch((err: Error) => this.log.error(`Server stop error: ${err.message}`));\n this.webServer = null;\n }\n\n void this.setState('info.connection', { val: false, ack: true });\n } catch (error) {\n const err = error as Error;\n this.log.error(`Shutdown error: ${err.message}`);\n } finally {\n callback();\n }\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new HassEmu(options);\n} else {\n // Otherwise start the instance directly\n (() => new HassEmu())();\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAAA,yBAAmB;AACnB,YAAuB;AACvB,kBAA4B;AAC5B,uBAA0B;AAG1B,MAAM,gBAAgB,MAAM,QAAQ;AAAA,EACxB,cAAkC;AAAA,EAClC,YAA8B;AAAA,EAI/B,YAAY,UAAyC,CAAC,GAAG;AAC5D,UAAM;AAAA,MACF,GAAG;AAAA,MACH,MAAM;AAAA,IACV,CAAC;AAED,SAAK,GAAG,SAAS,KAAK,QAAQ,KAAK,IAAI,CAAC;AACxC,SAAK,GAAG,UAAU,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA,EAC9C;AAAA,EAEA,MAAc,UAAyB;AACnC,QAAI;AACA,YAAM,KAAK,cAAc,mBAAmB,OAAO,IAAI;AAEvD,UAAI,CAAC,KAAK,OAAO,QAAQ;AACrB,aAAK,IAAI,MAAM,6EAA6E;AAAA,MAChG;AAEA,YAAM,eAAe,mBAAAA,QAAO,WAAW;AAEvC,WAAK,IAAI;AAAA,QACL,gBAAgB,KAAK,OAAO,IAAI,UAAU,KAAK,OAAO,YAAY,UAAU,KAAK,OAAO,WAAW;AAAA,MACvG;AAEA,UAAI,KAAK,OAAO,QAAQ;AACpB,aAAK,IAAI,MAAM,eAAe,KAAK,OAAO,MAAM,EAAE;AAElD,YAAI,6BAA6B,KAAK,KAAK,OAAO,MAAM,GAAG;AACvD,eAAK,IAAI;AAAA,YACL;AAAA,UACJ;AAAA,QACJ;AAAA,MACJ;AAEA,WAAK,YAAY,IAAI,2BAAU,MAAM,KAAK,QAAQ,YAAY;AAC9D,YAAM,KAAK,UAAU,MAAM;AAE3B,UAAI,KAAK,OAAO,aAAa;AACzB,aAAK,cAAc,IAAI,wBAAY,MAAM,KAAK,QAAQ,YAAY;AAClE,aAAK,YAAY,MAAM;AAAA,MAC3B,OAAO;AACH,aAAK,IAAI,MAAM,wDAAmD;AAAA,MACtE;AAEA,YAAM,KAAK,cAAc,mBAAmB,MAAM,IAAI;AACtD,YAAM,WAAW,KAAK,OAAO,eAAe;AAC5C,WAAK,IAAI;AAAA,QACL,2BAA2B,QAAQ,IAAI,KAAK,OAAO,IAAI,GAAG,KAAK,OAAO,cAAc,kBAAkB,EAAE;AAAA,MAC5G;AAAA,IACJ,SAAS,OAAO;AACZ,YAAM,MAAM;AACZ,WAAK,IAAI,MAAM,oBAAoB,IAAI,OAAO,EAAE;AAChD,UAAI,IAAI,OAAO;AACX,aAAK,IAAI,MAAM,IAAI,KAAK;AAAA,MAC5B;AAAA,IACJ;AAAA,EACJ;AAAA,EAEQ,SAAS,UAA4B;AACzC,QAAI;AACA,UAAI,KAAK,aAAa;AAClB,aAAK,YAAY,KAAK;AACtB,aAAK,cAAc;AAAA,MACvB;AAEA,UAAI,KAAK,WAAW;AAChB,aAAK,UAAU,KAAK,EAAE,MAAM,CAAC,QAAe,KAAK,IAAI,MAAM,sBAAsB,IAAI,OAAO,EAAE,CAAC;AAC/F,aAAK,YAAY;AAAA,MACrB;AAEA,WAAK,KAAK,SAAS,mBAAmB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC;AAAA,IACnE,SAAS,OAAO;AACZ,YAAM,MAAM;AACZ,WAAK,IAAI,MAAM,mBAAmB,IAAI,OAAO,EAAE;AAAA,IACnD,UAAE;AACE,eAAS;AAAA,IACb;AAAA,EACJ;AACJ;AAEA,IAAI,QAAQ,SAAS,QAAQ;AAEzB,SAAO,UAAU,CAAC,YAAuD,IAAI,QAAQ,OAAO;AAChG,OAAO;AAEH,GAAC,MAAM,IAAI,QAAQ,GAAG;AAC1B;",
|
|
6
6
|
"names": ["crypto"]
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "hassemu",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.4",
|
|
5
5
|
"news": {
|
|
6
|
+
"1.0.4": {
|
|
7
|
+
"en": "DRY: remove duplicate config interface and redundant mapping, fix log spam on missing redirect URL, tighten visibility",
|
|
8
|
+
"de": "DRY: doppeltes Config-Interface und redundantes Mapping entfernt, Log-Spam bei fehlender Redirect-URL behoben, Sichtbarkeit verschärft",
|
|
9
|
+
"ru": "DRY: удалён дублирующий интерфейс конфигурации и избыточное отображение, исправлен спам в логах при отсутствии URL перенаправления, ужесточена видимость",
|
|
10
|
+
"pt": "DRY: removida interface de configuração duplicada e mapeamento redundante, corrigido spam de log com URL de redirecionamento ausente, visibilidade restringida",
|
|
11
|
+
"nl": "DRY: dubbele config-interface en redundante mapping verwijderd, log-spam bij ontbrekende redirect-URL opgelost, zichtbaarheid verscherpt",
|
|
12
|
+
"fr": "DRY: suppression de l'interface de configuration dupliquée et du mapping redondant, correction du spam de log pour URL de redirection manquante, visibilité renforcée",
|
|
13
|
+
"it": "DRY: rimossa interfaccia di configurazione duplicata e mapping ridondante, corretto spam log con URL di reindirizzamento mancante, visibilità rafforzata",
|
|
14
|
+
"es": "DRY: eliminada interfaz de configuración duplicada y mapeo redundante, corregido spam de log con URL de redirección ausente, visibilidad reforzada",
|
|
15
|
+
"pl": "DRY: usunięto zduplikowany interfejs konfiguracji i nadmiarowe mapowanie, naprawiono spam logów przy brakującym URL przekierowania, zaostrzona widoczność",
|
|
16
|
+
"uk": "DRY: видалено дублюючий інтерфейс конфігурації та надлишкове відображення, виправлено спам логів при відсутньому URL перенаправлення, посилено видимість",
|
|
17
|
+
"zh-cn": "DRY:移除重复配置接口和冗余映射,修复缺少重定向URL时的日志刷屏,收紧可见性"
|
|
18
|
+
},
|
|
19
|
+
"1.0.3": {
|
|
20
|
+
"en": "Remove unused devDependencies, add no-floating-promises lint rule, remove redundant CI checkout",
|
|
21
|
+
"de": "Ungenutzte devDependencies entfernt, no-floating-promises Lint-Regel hinzugefügt, redundanten CI-Checkout entfernt",
|
|
22
|
+
"ru": "Удалены неиспользуемые devDependencies, добавлено правило lint no-floating-promises, удален избыточный checkout в CI",
|
|
23
|
+
"pt": "Removidas devDependencies não utilizadas, adicionada regra lint no-floating-promises, removido checkout redundante no CI",
|
|
24
|
+
"nl": "Ongebruikte devDependencies verwijderd, no-floating-promises lintregel toegevoegd, redundante CI-checkout verwijderd",
|
|
25
|
+
"fr": "Suppression des devDependencies inutilisées, ajout de la règle lint no-floating-promises, suppression du checkout CI redondant",
|
|
26
|
+
"it": "Rimosse devDependencies inutilizzate, aggiunta regola lint no-floating-promises, rimosso checkout CI ridondante",
|
|
27
|
+
"es": "Eliminadas devDependencies no utilizadas, añadida regla lint no-floating-promises, eliminado checkout CI redundante",
|
|
28
|
+
"pl": "Usunięto nieużywane devDependencies, dodano regułę lint no-floating-promises, usunięto nadmiarowy checkout w CI",
|
|
29
|
+
"uk": "Видалено невикористані devDependencies, додано правило лінтингу no-floating-promises, видалено зайвий checkout у CI",
|
|
30
|
+
"zh-cn": "移除未使用的开发依赖,添加 no-floating-promises 检查规则,移除冗余的 CI checkout"
|
|
31
|
+
},
|
|
6
32
|
"1.0.2": {
|
|
7
33
|
"en": "Remove build/ from git tracking, fix .gitignore, clean up keywords and metadata",
|
|
8
34
|
"de": "build/ aus Git-Tracking entfernt, .gitignore korrigiert, Keywords und Metadaten bereinigt",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.hassemu",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Emulates a minimal Home Assistant server so devices expecting a Home Assistant dashboard can display any custom web URL.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "krobi",
|
|
@@ -46,8 +46,6 @@
|
|
|
46
46
|
"@types/iobroker": "npm:@iobroker/types@^7.1.0",
|
|
47
47
|
"@types/node": "^25.5.0",
|
|
48
48
|
"rimraf": "^6.1.3",
|
|
49
|
-
"source-map-support": "^0.5.21",
|
|
50
|
-
"ts-node": "^10.9.2",
|
|
51
49
|
"typescript": "~5.9.3"
|
|
52
50
|
},
|
|
53
51
|
"main": "build/main.js",
|
|
@@ -62,7 +60,7 @@
|
|
|
62
60
|
"build": "build-adapter ts",
|
|
63
61
|
"watch": "build-adapter ts --watch",
|
|
64
62
|
"check": "tsc --noEmit",
|
|
65
|
-
"test:ts": "npm run build:test && mocha --exit \"build/test/
|
|
63
|
+
"test:ts": "npm run build:test && mocha --exit \"build/test/test*.js\"",
|
|
66
64
|
"test:package": "mocha test/package --exit",
|
|
67
65
|
"test:integration": "mocha test/integration --exit",
|
|
68
66
|
"test": "npm run build && npm run test:ts && npm run test:package",
|