iobroker.hassemu 1.0.0 → 1.0.2

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 CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  <img src="https://raw.githubusercontent.com/krobipd/ioBroker.hassemu/main/admin/hassemu.svg" width="100" />
13
13
 
14
- Emulates a minimal [Home Assistant](https://www.home-assistant.io) server so that devices like the [Shelly Wall Display XL](https://www.shelly.com) can be redirected to any custom web URL — without running a real Home Assistant Core.
14
+ Emulates a minimal [Home Assistant](https://www.home-assistant.io) server so that devices expecting a Home Assistant dashboard can be redirected to any custom web URL — without running a real Home Assistant Core.
15
15
 
16
16
  > Previously known as `ioBroker.homeassistant-bridge`. Renamed to better reflect that this adapter emulates, not bridges.
17
17
 
@@ -19,7 +19,7 @@ Emulates a minimal [Home Assistant](https://www.home-assistant.io) server so tha
19
19
 
20
20
  ## Features
21
21
 
22
- - **Home Assistant Emulation** — minimal HA API compatible with Shelly Wall Display XL
22
+ - **Home Assistant Emulation** — minimal HA API for devices expecting a Home Assistant dashboard
23
23
  - **mDNS Discovery** — automatic detection via `_home-assistant._tcp` (cross-platform)
24
24
  - **OAuth2-like Auth Flow** — full login flow emulation, optional credential validation
25
25
  - **Flexible Redirect** — send the display to any ioBroker VIS, VIS-2, or custom web URL
@@ -39,7 +39,7 @@ Emulates a minimal [Home Assistant](https://www.home-assistant.io) server so tha
39
39
 
40
40
  | Port | Protocol | Purpose | Configurable |
41
41
  |------|----------|---------|--------------|
42
- | 8123 | TCP/HTTP | Home Assistant emulation (Shelly requires exactly this port) | No — fixed |
42
+ | 8123 | TCP/HTTP | Home Assistant emulation (HA standard port) | No — fixed |
43
43
 
44
44
  ---
45
45
 
@@ -57,7 +57,7 @@ Configuration is done via the Admin UI (jsonConfig):
57
57
  | **Username** | Login name (if auth enabled) | "admin" |
58
58
  | **Password** | Login password (stored encrypted) | - |
59
59
 
60
- > **Important: Port 8123 is mandatory.** The adapter always listens on port 8123 — this is hardcoded and cannot be changed. Shelly displays and other Home Assistant-compatible devices expect exactly this port. Make sure port 8123 is not already in use on your ioBroker server.
60
+ > **Important: Port 8123 is mandatory.** The adapter always listens on port 8123 — this is the standard Home Assistant port and cannot be changed. Make sure port 8123 is not already in use on your ioBroker server.
61
61
 
62
62
  **Important:** The redirect URL must be a network-accessible address, e.g.:
63
63
  ```
@@ -113,6 +113,9 @@ http://<IP>:8123/health
113
113
 
114
114
  ## Changelog
115
115
 
116
+ ### 1.0.2 (2026-04-08)
117
+ - Remove build/ from git tracking, fix .gitignore, clean up keywords and metadata
118
+
116
119
  ### 1.0.0 (2026-04-08)
117
120
  - Renamed from homeassistant-bridge to hassemu
118
121
 
package/admin/hassemu.svg CHANGED
@@ -6,7 +6,7 @@
6
6
  <path d="M32 12 L12 28 L12 52 L24 52 L24 38 L40 38 L40 52 L52 52 L52 28 Z"
7
7
  fill="white" stroke="white" stroke-width="2" stroke-linejoin="round"/>
8
8
 
9
- <!-- Bridge connection symbol -->
9
+ <!-- Connection symbol -->
10
10
  <path d="M20 44 Q32 36 44 44"
11
11
  fill="none" stroke="#41BDF5" stroke-width="3" stroke-linecap="round"/>
12
12
  <circle cx="20" cy="44" r="3" fill="#41BDF5"/>
@@ -12,7 +12,7 @@
12
12
  "header_server": "Server",
13
13
  "mdnsEnabled": "mDNS-Erkennung aktivieren",
14
14
  "mdnsEnabledTooltip": "Service per mDNS broadcasten damit Geräte ihn automatisch finden (funktioniert auf allen Plattformen)",
15
- "mdnsInfo": "mDNS sendet den Service im lokalen Netzwerk, damit Geräte (z.B. Shelly Wall Display) ihn automatisch finden können.\n\nWenn mDNS deaktiviert oder nicht funktioniert, geben Sie die URL manuell am Display ein:\n`http://IHRE_IP:8123`",
15
+ "mdnsInfo": "mDNS sendet den Service im lokalen Netzwerk, damit Geräte ihn automatisch finden können.\n\nWenn mDNS deaktiviert oder nicht funktioniert, geben Sie die URL manuell am Display ein:\n`http://IHRE_IP:8123`",
16
16
  "password": "Passwort",
17
17
  "serviceName": "Service-Name",
18
18
  "serviceNameTooltip": "Name der auf dem Display während der Erkennung angezeigt wird",
@@ -12,7 +12,7 @@
12
12
  "header_server": "Server",
13
13
  "mdnsEnabled": "Enable mDNS Discovery",
14
14
  "mdnsEnabledTooltip": "Broadcast the service via mDNS so devices can find it automatically (works on all platforms)",
15
- "mdnsInfo": "mDNS broadcasts the service on the local network so devices (e.g. Shelly Wall Display) can discover it automatically.\n\nIf mDNS is disabled or not working, enter the URL manually on the display:\n`http://YOUR_IP:8123`",
15
+ "mdnsInfo": "mDNS broadcasts the service on the local network so devices can discover it automatically.\n\nIf mDNS is disabled or not working, enter the URL manually on the display:\n`http://YOUR_IP:8123`",
16
16
  "password": "Password",
17
17
  "serviceName": "Service Name",
18
18
  "serviceNameTooltip": "Name shown on the display during discovery",
@@ -12,7 +12,7 @@
12
12
  "header_server": "Servidor",
13
13
  "mdnsEnabled": "Habilitar Descubrimiento mDNS",
14
14
  "mdnsEnabledTooltip": "Difundir el servicio via mDNS para que los dispositivos lo encuentren automáticamente (funciona en todas las plataformas)",
15
- "mdnsInfo": "mDNS difunde el servicio en la red local para que los dispositivos (ej. Shelly Wall Display) puedan descubrirlo automáticamente.\n\nSi mDNS está deshabilitado o no funciona, ingrese la URL manualmente en la pantalla:\n`http://YOUR_IP:8123`",
15
+ "mdnsInfo": "mDNS difunde el servicio en la red local para que los dispositivos puedan descubrirlo automáticamente.\n\nSi mDNS está deshabilitado o no funciona, ingrese la URL manualmente en la pantalla:\n`http://YOUR_IP:8123`",
16
16
  "password": "Contraseña",
17
17
  "serviceName": "Nombre del Servicio",
18
18
  "serviceNameTooltip": "Nombre mostrado en la pantalla durante el descubrimiento",
@@ -12,7 +12,7 @@
12
12
  "header_server": "Serveur",
13
13
  "mdnsEnabled": "Activer la Découverte mDNS",
14
14
  "mdnsEnabledTooltip": "Diffuser le service via mDNS pour que les appareils le trouvent automatiquement (fonctionne sur toutes les plateformes)",
15
- "mdnsInfo": "mDNS diffuse le service sur le réseau local pour que les appareils (ex. Shelly Wall Display) puissent le découvrir automatiquement.\n\nSi mDNS est désactivé ou ne fonctionne pas, entrez l'URL manuellement sur l'écran:\n`http://YOUR_IP:8123`",
15
+ "mdnsInfo": "mDNS diffuse le service sur le réseau local pour que les appareils puissent le découvrir automatiquement.\n\nSi mDNS est désactivé ou ne fonctionne pas, entrez l'URL manuellement sur l'écran:\n`http://YOUR_IP:8123`",
16
16
  "password": "Mot de passe",
17
17
  "serviceName": "Nom du Service",
18
18
  "serviceNameTooltip": "Nom affiché sur l'écran pendant la découverte",
@@ -12,7 +12,7 @@
12
12
  "header_server": "Server",
13
13
  "mdnsEnabled": "Abilita Scoperta mDNS",
14
14
  "mdnsEnabledTooltip": "Trasmetti il servizio via mDNS in modo che i dispositivi possano trovarlo automaticamente (funziona su tutte le piattaforme)",
15
- "mdnsInfo": "mDNS trasmette il servizio sulla rete locale in modo che i dispositivi (es. Shelly Wall Display) possano scoprirlo automaticamente.\n\nSe mDNS è disabilitato o non funziona, inserisci l'URL manualmente sul display:\n`http://YOUR_IP:8123`",
15
+ "mdnsInfo": "mDNS trasmette il servizio sulla rete locale in modo che i dispositivi possano scoprirlo automaticamente.\n\nSe mDNS è disabilitato o non funziona, inserisci l'URL manualmente sul display:\n`http://YOUR_IP:8123`",
16
16
  "password": "Password",
17
17
  "serviceName": "Nome del Servizio",
18
18
  "serviceNameTooltip": "Nome mostrato sul display durante il rilevamento",
@@ -12,7 +12,7 @@
12
12
  "header_server": "Server",
13
13
  "mdnsEnabled": "mDNS-detectie inschakelen",
14
14
  "mdnsEnabledTooltip": "De service via mDNS uitzenden zodat apparaten deze automatisch kunnen vinden (werkt op alle platformen)",
15
- "mdnsInfo": "mDNS zendt de service uit op het lokale netwerk zodat apparaten (bijv. Shelly Wall Display) deze automatisch kunnen ontdekken.\n\nAls mDNS uitgeschakeld is of niet werkt, voer de URL handmatig in op het scherm:\n`http://YOUR_IP:8123`",
15
+ "mdnsInfo": "mDNS zendt de service uit op het lokale netwerk zodat apparaten deze automatisch kunnen ontdekken.\n\nAls mDNS uitgeschakeld is of niet werkt, voer de URL handmatig in op het scherm:\n`http://YOUR_IP:8123`",
16
16
  "password": "Wachtwoord",
17
17
  "serviceName": "Servicenaam",
18
18
  "serviceNameTooltip": "Naam weergegeven op het scherm tijdens detectie",
@@ -12,7 +12,7 @@
12
12
  "header_server": "Serwer",
13
13
  "mdnsEnabled": "Włącz Wykrywanie mDNS",
14
14
  "mdnsEnabledTooltip": "Rozgłaszaj usługę przez mDNS, aby urządzenia mogły ją znaleźć automatycznie (działa na wszystkich platformach)",
15
- "mdnsInfo": "mDNS rozgłasza usługę w sieci lokalnej, aby urządzenia (np. Shelly Wall Display) mogły ją odkryć automatycznie.\n\nJeśli mDNS jest wyłączone lub nie działa, wprowadź URL ręcznie na ekranie:\n`http://YOUR_IP:8123`",
15
+ "mdnsInfo": "mDNS rozgłasza usługę w sieci lokalnej, aby urządzenia mogły ją odkryć automatycznie.\n\nJeśli mDNS jest wyłączone lub nie działa, wprowadź URL ręcznie na ekranie:\n`http://YOUR_IP:8123`",
16
16
  "password": "Hasło",
17
17
  "serviceName": "Nazwa Usługi",
18
18
  "serviceNameTooltip": "Nazwa wyświetlana na ekranie podczas wykrywania",
@@ -12,7 +12,7 @@
12
12
  "header_server": "Servidor",
13
13
  "mdnsEnabled": "Habilitar Descoberta mDNS",
14
14
  "mdnsEnabledTooltip": "Transmitir o serviço via mDNS para que os dispositivos o encontrem automaticamente (funciona em todas as plataformas)",
15
- "mdnsInfo": "O mDNS transmite o serviço na rede local para que dispositivos (ex. Shelly Wall Display) possam descobri-lo automaticamente.\n\nSe o mDNS estiver desativado ou não funcionar, insira a URL manualmente no display:\n`http://YOUR_IP:8123`",
15
+ "mdnsInfo": "O mDNS transmite o serviço na rede local para que dispositivos possam descobri-lo automaticamente.\n\nSe o mDNS estiver desativado ou não funcionar, insira a URL manualmente no display:\n`http://YOUR_IP:8123`",
16
16
  "password": "Senha",
17
17
  "serviceName": "Nome do Serviço",
18
18
  "serviceNameTooltip": "Nome exibido no display durante a descoberta",
@@ -12,7 +12,7 @@
12
12
  "header_server": "Сервер",
13
13
  "mdnsEnabled": "Включить mDNS обнаружение",
14
14
  "mdnsEnabledTooltip": "Транслировать службу через mDNS, чтобы устройства могли найти её автоматически (работает на всех платформах)",
15
- "mdnsInfo": "mDNS транслирует службу в локальной сети, чтобы устройства (напр. Shelly Wall Display) могли обнаружить её автоматически.\n\nЕсли mDNS отключён или не работает, введите URL вручную на дисплее:\n`http://YOUR_IP:8123`",
15
+ "mdnsInfo": "mDNS транслирует службу в локальной сети, чтобы устройства могли обнаружить её автоматически.\n\nЕсли mDNS отключён или не работает, введите URL вручную на дисплее:\n`http://YOUR_IP:8123`",
16
16
  "password": "Пароль",
17
17
  "serviceName": "Имя службы",
18
18
  "serviceNameTooltip": "Имя, отображаемое на дисплее во время обнаружения",
@@ -12,7 +12,7 @@
12
12
  "header_server": "Сервер",
13
13
  "mdnsEnabled": "Увімкнути mDNS Виявлення",
14
14
  "mdnsEnabledTooltip": "Транслювати службу через mDNS, щоб пристрої могли знайти її автоматично (працює на всіх платформах)",
15
- "mdnsInfo": "mDNS транслює службу в локальній мережі, щоб пристрої (напр. Shelly Wall Display) могли виявити її автоматично.\n\nЯкщо mDNS вимкнено або не працює, введіть URL вручну на дисплеї:\n`http://YOUR_IP:8123`",
15
+ "mdnsInfo": "mDNS транслює службу в локальній мережі, щоб пристрої могли виявити її автоматично.\n\nЯкщо mDNS вимкнено або не працює, введіть URL вручну на дисплеї:\n`http://YOUR_IP:8123`",
16
16
  "password": "Пароль",
17
17
  "serviceName": "Назва Служби",
18
18
  "serviceNameTooltip": "Назва, що відображається на дисплеї під час виявлення",
@@ -12,7 +12,7 @@
12
12
  "header_server": "服务器",
13
13
  "mdnsEnabled": "启用 mDNS 发现",
14
14
  "mdnsEnabledTooltip": "通过 mDNS 广播服务,使设备能够自动找到它(适用于所有平台)",
15
- "mdnsInfo": "mDNS 在本地网络上广播服务,使设备(如 Shelly Wall Display)能够自动发现它。\n\n如果 mDNS 已禁用或无法正常工作,请在显示屏上手动输入 URL:\n`http://YOUR_IP:8123`",
15
+ "mdnsInfo": "mDNS 在本地网络上广播服务,使设备能够自动发现它。\n\n如果 mDNS 已禁用或无法正常工作,请在显示屏上手动输入 URL:\n`http://YOUR_IP:8123`",
16
16
  "password": "密码",
17
17
  "serviceName": "服务名称",
18
18
  "serviceNameTooltip": "发现期间在显示屏上显示的名称",
package/admin/icon.svg CHANGED
@@ -6,7 +6,7 @@
6
6
  <path d="M32 12 L12 28 L12 52 L24 52 L24 38 L40 38 L40 52 L52 52 L52 28 Z"
7
7
  fill="white" stroke="white" stroke-width="2" stroke-linejoin="round"/>
8
8
 
9
- <!-- Bridge connection symbol -->
9
+ <!-- Connection symbol -->
10
10
  <path d="M20 44 Q32 36 44 44"
11
11
  fill="none" stroke="#41BDF5" stroke-width="3" stroke-linecap="round"/>
12
12
  <circle cx="20" cy="44" r="3" fill="#41BDF5"/>
@@ -334,7 +334,7 @@ class WebServer {
334
334
  return new Promise((resolve) => {
335
335
  if (this.server) {
336
336
  this.server.close(() => {
337
- this.adapter.log.info("Web server stopped");
337
+ this.adapter.log.debug("Web server stopped");
338
338
  this.server = null;
339
339
  resolve();
340
340
  });
@@ -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 Shelly checks 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.info('Web server stopped');\n this.server = null;\n resolve();\n });\n } else {\n resolve();\n }\n });\n }\n}\n"],
5
- "mappings": "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",
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": "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",
6
6
  "names": ["express", "crypto"]
7
7
  }
package/build/main.js CHANGED
@@ -25,7 +25,7 @@ var import_node_crypto = __toESM(require("node:crypto"));
25
25
  var utils = __toESM(require("@iobroker/adapter-core"));
26
26
  var import_mdns = require("./lib/mdns");
27
27
  var import_webserver = require("./lib/webserver");
28
- class HomeAssistantBridge extends utils.Adapter {
28
+ class HassEmu extends utils.Adapter {
29
29
  mdnsService = null;
30
30
  webServer = null;
31
31
  constructor(options = {}) {
@@ -73,7 +73,7 @@ class HomeAssistantBridge extends utils.Adapter {
73
73
  await this.setStateAsync("info.connection", true, true);
74
74
  const bindAddr = config.bindAddress || "0.0.0.0";
75
75
  this.log.info(
76
- `Home Assistant Bridge running on ${bindAddr}:${config.port}${config.mdnsEnabled ? ", mDNS active" : ""}`
76
+ `HA emulation running on ${bindAddr}:${config.port}${config.mdnsEnabled ? ", mDNS active" : ""}`
77
77
  );
78
78
  } catch (error) {
79
79
  const err = error;
@@ -103,8 +103,8 @@ class HomeAssistantBridge extends utils.Adapter {
103
103
  }
104
104
  }
105
105
  if (require.main !== module) {
106
- module.exports = (options) => new HomeAssistantBridge(options);
106
+ module.exports = (options) => new HassEmu(options);
107
107
  } else {
108
- (() => new HomeAssistantBridge())();
108
+ (() => new HassEmu())();
109
109
  }
110
110
  //# sourceMappingURL=main.js.map
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\n/** Native adapter configuration from io-package.json */\ninterface NativeConfig {\n port: number;\n bindAddress: string;\n visUrl: string;\n authRequired: boolean;\n username: string;\n password: string;\n mdnsEnabled: boolean;\n serviceName: string;\n}\n\nclass HomeAssistantBridge extends utils.Adapter {\n private mdnsService: MDNSService | null = null;\n private webServer: WebServer | null = null;\n\n declare config: NativeConfig;\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 // Validate configuration\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 config: AdapterConfig = {\n port: this.config.port || 8123,\n bindAddress: this.config.bindAddress || '0.0.0.0',\n visUrl: this.config.visUrl || '',\n authRequired: this.config.authRequired === true,\n username: this.config.username || 'admin',\n password: this.config.password || '',\n mdnsEnabled: this.config.mdnsEnabled !== false,\n serviceName: this.config.serviceName || 'ioBroker',\n };\n\n // Single UUID shared between WebServer and mDNS for consistency\n const instanceUuid = crypto.randomUUID();\n\n this.log.debug(`Config: port=${config.port}, auth=${config.authRequired}, mdns=${config.mdnsEnabled}`);\n\n if (config.visUrl) {\n this.log.debug(`Target URL: ${config.visUrl}`);\n\n if (/\\blocalhost\\b|127\\.0\\.0\\.1/.test(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, config, instanceUuid);\n await this.webServer.start();\n\n if (config.mdnsEnabled) {\n this.mdnsService = new MDNSService(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 = config.bindAddress || '0.0.0.0';\n this.log.info(\n `Home Assistant Bridge running on ${bindAddr}:${config.port}${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 HomeAssistantBridge(options);\n} else {\n // Otherwise start the instance directly\n (() => new HomeAssistantBridge())();\n}\n"],
5
- "mappings": "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",
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\n/** Native adapter configuration from io-package.json */\ninterface NativeConfig {\n port: number;\n bindAddress: string;\n visUrl: string;\n authRequired: boolean;\n username: string;\n password: string;\n mdnsEnabled: boolean;\n serviceName: string;\n}\n\nclass HassEmu extends utils.Adapter {\n private mdnsService: MDNSService | null = null;\n private webServer: WebServer | null = null;\n\n declare config: NativeConfig;\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 // Validate configuration\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 config: AdapterConfig = {\n port: this.config.port || 8123,\n bindAddress: this.config.bindAddress || '0.0.0.0',\n visUrl: this.config.visUrl || '',\n authRequired: this.config.authRequired === true,\n username: this.config.username || 'admin',\n password: this.config.password || '',\n mdnsEnabled: this.config.mdnsEnabled !== false,\n serviceName: this.config.serviceName || 'ioBroker',\n };\n\n // Single UUID shared between WebServer and mDNS for consistency\n const instanceUuid = crypto.randomUUID();\n\n this.log.debug(`Config: port=${config.port}, auth=${config.authRequired}, mdns=${config.mdnsEnabled}`);\n\n if (config.visUrl) {\n this.log.debug(`Target URL: ${config.visUrl}`);\n\n if (/\\blocalhost\\b|127\\.0\\.0\\.1/.test(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, config, instanceUuid);\n await this.webServer.start();\n\n if (config.mdnsEnabled) {\n this.mdnsService = new MDNSService(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 = config.bindAddress || '0.0.0.0';\n this.log.info(\n `HA emulation running on ${bindAddr}:${config.port}${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": "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",
6
6
  "names": ["crypto"]
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "hassemu",
4
- "version": "1.0.0",
4
+ "version": "1.0.2",
5
5
  "news": {
6
+ "1.0.2": {
7
+ "en": "Remove build/ from git tracking, fix .gitignore, clean up keywords and metadata",
8
+ "de": "build/ aus Git-Tracking entfernt, .gitignore korrigiert, Keywords und Metadaten bereinigt",
9
+ "ru": "Удален build/ из отслеживания git, исправлен .gitignore, очищены ключевые слова и метаданные",
10
+ "pt": "Removido build/ do rastreamento git, corrigido .gitignore, limpeza de palavras-chave e metadados",
11
+ "nl": "build/ verwijderd uit git-tracking, .gitignore gecorrigeerd, trefwoorden en metadata opgeschoond",
12
+ "fr": "Suppression de build/ du suivi git, correction de .gitignore, nettoyage des mots-clés et métadonnées",
13
+ "it": "Rimosso build/ dal tracciamento git, corretto .gitignore, pulizia parole chiave e metadati",
14
+ "es": "Eliminado build/ del seguimiento git, corregido .gitignore, limpieza de palabras clave y metadatos",
15
+ "pl": "Usunięto build/ ze śledzenia git, poprawiono .gitignore, wyczyszczono słowa kluczowe i metadane",
16
+ "uk": "Видалено build/ з відстеження git, виправлено .gitignore, очищено ключові слова та метадані",
17
+ "zh-cn": "从git跟踪中删除build/,修复.gitignore,清理关键词和元数据"
18
+ },
6
19
  "1.0.0": {
7
20
  "en": "Renamed from homeassistant-bridge to hassemu",
8
21
  "de": "Umbenannt von homeassistant-bridge zu hassemu",
@@ -49,10 +62,10 @@
49
62
  "keywords": [
50
63
  "homeassistant",
51
64
  "hass",
52
- "shelly",
53
- "wall display",
54
- "vis",
55
- "emulator"
65
+ "emulator",
66
+ "dashboard",
67
+ "redirect",
68
+ "vis"
56
69
  ],
57
70
  "licenseInformation": {
58
71
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.hassemu",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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",
@@ -12,10 +12,10 @@
12
12
  "ioBroker",
13
13
  "homeassistant",
14
14
  "hass",
15
- "shelly",
16
- "wall display",
17
- "vis",
18
- "emulation"
15
+ "emulator",
16
+ "dashboard",
17
+ "redirect",
18
+ "vis"
19
19
  ],
20
20
  "repository": {
21
21
  "type": "git",
@@ -65,8 +65,8 @@
65
65
  "test:ts": "npm run build:test && mocha --exit \"build/test/testConstants.js\" \"build/test/testMdns.js\" \"build/test/testWebServer.js\"",
66
66
  "test:package": "mocha test/package --exit",
67
67
  "test:integration": "mocha test/integration --exit",
68
- "test": "npm run test:ts && npm run test:package",
69
- "build:test": "rm -rf ./build && tsc -p tsconfig.test.json",
68
+ "test": "npm run build && npm run test:ts && npm run test:package",
69
+ "build:test": "tsc -p tsconfig.test.json",
70
70
  "lint": "eslint",
71
71
  "lint:fix": "eslint --fix",
72
72
  "format": "prettier --write .",