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 +7 -4
- package/admin/hassemu.svg +1 -1
- package/admin/i18n/de/translations.json +1 -1
- package/admin/i18n/en/translations.json +1 -1
- package/admin/i18n/es/translations.json +1 -1
- package/admin/i18n/fr/translations.json +1 -1
- package/admin/i18n/it/translations.json +1 -1
- package/admin/i18n/nl/translations.json +1 -1
- package/admin/i18n/pl/translations.json +1 -1
- package/admin/i18n/pt/translations.json +1 -1
- package/admin/i18n/ru/translations.json +1 -1
- package/admin/i18n/uk/translations.json +1 -1
- package/admin/i18n/zh-cn/translations.json +1 -1
- package/admin/icon.svg +1 -1
- package/build/lib/webserver.js +1 -1
- package/build/lib/webserver.js.map +2 -2
- package/build/main.js +4 -4
- package/build/main.js.map +2 -2
- package/io-package.json +18 -5
- package/package.json +7 -7
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
<!--
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 транслирует службу в локальной сети, чтобы устройства
|
|
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 транслює службу в локальній мережі, щоб пристрої
|
|
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
|
|
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
|
-
<!--
|
|
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"/>
|
package/build/lib/webserver.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
`
|
|
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
|
|
106
|
+
module.exports = (options) => new HassEmu(options);
|
|
107
107
|
} else {
|
|
108
|
-
(() => new
|
|
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
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAAA,yBAAmB;AACnB,YAAuB;AACvB,kBAA4B;AAC5B,uBAA0B;AAe1B,MAAM,
|
|
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.
|
|
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
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
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": "
|
|
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 .",
|