@tomfy/shelly 2026.3.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/CHANGELOG.md +5 -0
- package/README.md +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +533 -0
- package/dist/index.js.map +1 -0
- package/package.json +30 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAoQ7C,MAAM,CAAC,OAAO,WAAW,MAAM,EAAE,SAAS,QAyVzC","sourcesContent":["// Shelly plugin for Tomfy\n// Supports Gen2 devices via auto-detected components.\n//\n// Configs are stored in ~/.tomfy/config/shelly/<object-id>.json\n// New devices are auto-discovered via mDNS and saved automatically.\n// Uses WebSocket for real-time push updates.\n//\n// Components detected from Shelly.GetStatus:\n// switch — on, power*, voltage*, current*, temperature, energy* (* PM models only)\n// input — state property + singlePush/doublePush/longPush events\n// cover — state, position, power*, voltage*, current*, temperature\n// light — on, brightness\n//\n// Object-level properties (all objects):\n// firmware.installedVersion, firmware.latestVersion, firmware.updateAvailable\n// (only when config.trackFirmware is true)\n//\n// Naming convention (same for all component types):\n// Single component: <property> e.g. on, power\n// Multiple: <name>.<property> e.g. output0.on, kitchen.power\n// Component names use camelCased Shelly config name when available,\n// otherwise fall back to output0/input0/cover0/light0.\n//\n// Plugin config is exposed as properties on an object named \"shelly\":\n// tomfy get shelly.config — view all config\n// tomfy set shelly.config.discoveryInterval 120000 — update config\n// tomfy set shelly.config.trackFirmware true — enable firmware tracking\n\nimport type { PluginAPI } from \"@tomfy/core\";\nimport { toCamelCase } from \"@tomfy/core\";\nimport { Bonjour } from \"bonjour-service\";\n\n// ── Types ───────────────────────────────────────────────────\n\ntype ShellyComponentType = \"switch\" | \"input\" | \"cover\" | \"light\";\n\ninterface ShellyComponentConfig {\n type: ShellyComponentType;\n id: number;\n name: string | null;\n metering?: boolean; // switch and cover: whether power metering is available\n}\n\ninterface ShellyDeviceConfig {\n host: string;\n model?: string;\n mac?: string;\n components: ShellyComponentConfig[];\n}\n\ninterface ShellyInputEvent {\n component: string; // \"input:0\"\n id: number;\n event: string; // \"single_push\", \"double_push\", \"long_push\"\n ts: number;\n}\n\n// ── Legacy config migration ─────────────────────────────────\n\ninterface LegacyDeviceConfig {\n host: string;\n model?: string;\n mac?: string;\n outputs?: number;\n outputNames?: (string | null)[];\n inputs?: number;\n inputNames?: (string | null)[];\n}\n\nconst migrateConfig = (raw: Record<string, any>): ShellyDeviceConfig => {\n if (raw.components) return raw as ShellyDeviceConfig;\n\n // Convert old outputs/inputs format to components array\n const legacy = raw as LegacyDeviceConfig;\n const components: ShellyComponentConfig[] = [];\n\n const outputs = legacy.outputs ?? 0;\n for (let i = 0; i < outputs; i++) {\n components.push({\n type: \"switch\",\n id: i,\n name: legacy.outputNames?.[i] ?? null,\n metering: true, // assume PM for old configs; re-probe will correct\n });\n }\n\n const inputs = legacy.inputs ?? 0;\n for (let i = 0; i < inputs; i++) {\n components.push({\n type: \"input\",\n id: i,\n name: legacy.inputNames?.[i] ?? null,\n });\n }\n\n return { host: legacy.host, model: legacy.model, mac: legacy.mac, components };\n};\n\n// ── HTTP API ────────────────────────────────────────────────\n\nconst rpc = async (host: string, method: string, params: Record<string, unknown> = {}): Promise<void> => {\n const res = await fetch(`http://${host}/rpc/${method}`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(params),\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n};\n\n// ── WebSocket connection ────────────────────────────────────\n\ninterface WsConnection {\n close(): void;\n}\n\nconst COMPONENT_RE = /^(switch|input|cover|light):(\\d+)$/;\n\nconst connectWs = (\n host: string,\n onComponentStatus: (type: string, id: number, data: Record<string, any>) => void,\n onSysStatus: (data: Record<string, any>) => void,\n onDeviceInfo: (data: Record<string, any>) => void,\n onInputEvent: (event: ShellyInputEvent) => void,\n onError: (err: Error) => void,\n onClose: () => void,\n): WsConnection => {\n const ws = new WebSocket(`ws://${host}/rpc`);\n let rpcId = 1;\n let alive = true;\n\n const extractComponents = (obj: Record<string, any>) => {\n for (const [key, data] of Object.entries(obj)) {\n if (key === \"sys\" && data && typeof data === \"object\") {\n onSysStatus(data as Record<string, any>);\n continue;\n }\n const m = COMPONENT_RE.exec(key);\n if (m && data && typeof data === \"object\") {\n onComponentStatus(m[1], parseInt(m[2], 10), data as Record<string, any>);\n }\n }\n };\n\n ws.addEventListener(\"open\", () => {\n ws.send(JSON.stringify({ jsonrpc: \"2.0\", id: rpcId++, src: \"tomfy\", method: \"Shelly.GetStatus\" }));\n ws.send(JSON.stringify({ jsonrpc: \"2.0\", id: rpcId++, src: \"tomfy\", method: \"Shelly.GetDeviceInfo\" }));\n });\n\n ws.addEventListener(\"message\", (event: MessageEvent) => {\n try {\n const msg = JSON.parse(String(event.data)) as Record<string, any>;\n\n if (msg.result) {\n // Distinguish GetDeviceInfo (has \"ver\") from GetStatus (has component keys)\n if (\"ver\" in msg.result) {\n onDeviceInfo(msg.result);\n } else {\n extractComponents(msg.result);\n }\n }\n if (msg.method === \"NotifyStatus\" && msg.params) extractComponents(msg.params);\n if (msg.method === \"NotifyFullStatus\" && msg.params) extractComponents(msg.params);\n if (msg.method === \"NotifyEvent\" && msg.params?.events) {\n for (const evt of msg.params.events as ShellyInputEvent[]) {\n if (evt.component?.startsWith(\"input:\")) onInputEvent(evt);\n }\n }\n } catch (e) {\n onError(e as Error);\n }\n });\n\n ws.addEventListener(\"error\", () => onError(new Error(`WebSocket error for ${host}`)));\n ws.addEventListener(\"close\", () => {\n if (alive) onClose();\n });\n\n return {\n close() {\n alive = false;\n ws.close();\n },\n };\n};\n\n// ── Probe device via HTTP ───────────────────────────────────\n\ninterface ProbeResult {\n id: string;\n name: string;\n model: string;\n mac: string;\n gen: number;\n components: ShellyComponentConfig[];\n}\n\nconst probeShelly = async (host: string): Promise<ProbeResult | null> => {\n try {\n const res = await fetch(`http://${host}/shelly`, { signal: AbortSignal.timeout(3000) });\n if (!res.ok) return null;\n const data = (await res.json()) as Record<string, any>;\n const gen = data.gen ?? 1;\n if (gen < 2) return null;\n\n // Detect components from Shelly.GetStatus\n let status: Record<string, any> = {};\n try {\n const statusRes = await fetch(`http://${host}/rpc/Shelly.GetStatus`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ id: 1 }),\n signal: AbortSignal.timeout(3000),\n });\n if (statusRes.ok) status = (await statusRes.json()) as Record<string, any>;\n } catch {\n /* empty status */\n }\n\n // Fetch config for component names\n let cfg: Record<string, any> = {};\n try {\n const configRes = await fetch(`http://${host}/rpc/Shelly.GetConfig`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ id: 1 }),\n signal: AbortSignal.timeout(3000),\n });\n if (configRes.ok) cfg = (await configRes.json()) as Record<string, any>;\n } catch {\n /* no config */\n }\n\n const components: ShellyComponentConfig[] = [];\n for (const key of Object.keys(status)) {\n const m = COMPONENT_RE.exec(key);\n if (!m) continue;\n\n const type = m[1] as ShellyComponentType;\n const id = parseInt(m[2], 10);\n const compStatus = status[key] as Record<string, any> | undefined;\n const compCfg = cfg[key] as Record<string, any> | undefined;\n\n const comp: ShellyComponentConfig = {\n type,\n id,\n name: compCfg?.name || null,\n };\n\n // Detect power metering for switch and cover\n if ((type === \"switch\" || type === \"cover\") && compStatus && \"apower\" in compStatus) {\n comp.metering = true;\n }\n\n components.push(comp);\n }\n\n return {\n id: data.id ?? data.hostname ?? `shelly-${host.replace(/\\./g, \"-\")}`,\n name: data.name || data.app || data.id || host,\n model: data.model ?? \"unknown\",\n mac: data.mac ?? \"\",\n gen,\n components,\n };\n } catch {\n return null;\n }\n};\n\n// ── Plugin ──────────────────────────────────────────────────\n\nconst INPUT_EVENT_NAMES = [\"buttonDown\", \"buttonUp\", \"push\", \"doublePush\", \"longPush\"] as const;\nconst SHELLY_EVENT_MAP: Record<string, string> = {\n btn_down: \"buttonDown\",\n btn_up: \"buttonUp\",\n single_push: \"push\",\n double_push: \"doublePush\",\n long_push: \"longPush\",\n};\n\nconst DEFAULT_PREFIXES: Record<ShellyComponentType, string> = {\n switch: \"output\",\n input: \"input\",\n cover: \"cover\",\n light: \"light\",\n};\n\nexport default function (plugin: PluginAPI) {\n // ── Config object ───────────────────────────────────────\n // Expose plugin config as properties on an object named after the plugin.\n // Persisted in ~/.tomfy/config/shelly/shelly.json alongside hardware objects.\n\n const savedConfig = plugin.config.load<Record<string, unknown>>(plugin.pluginId);\n let discoveryInterval = typeof savedConfig?.discoveryInterval === \"number\" ? savedConfig.discoveryInterval : 60000;\n let trackFirmware = typeof savedConfig?.trackFirmware === \"boolean\" ? savedConfig.trackFirmware : false;\n\n const configObject = plugin.registerObject(plugin.pluginId, { name: plugin.pluginId });\n configObject.defineProperty(\"config.discoveryInterval\", {\n type: \"integer\",\n writable: true,\n value: discoveryInterval,\n });\n configObject.defineProperty(\"config.trackFirmware\", {\n type: \"boolean\",\n writable: true,\n value: trackFirmware,\n });\n\n const saveConfig = () => {\n const existing = plugin.config.load<Record<string, unknown>>(plugin.pluginId) ?? {};\n plugin.config.save(plugin.pluginId, { ...existing, discoveryInterval, trackFirmware });\n };\n\n const wsConnections: WsConnection[] = [];\n const registeredObjects = new Set<string>();\n\n // ── Register an object ──────────────────────────────────\n\n const setupObject = (id: string, deviceConfig: ShellyDeviceConfig, displayName: string) => {\n if (registeredObjects.has(id)) return;\n registeredObjects.add(id);\n\n const { host, components } = deviceConfig;\n const obj = plugin.registerObject(id, { name: displayName });\n\n // Group components by type for single/multi naming\n const byType = new Map<string, ShellyComponentConfig[]>();\n for (const comp of components) {\n const list = byType.get(comp.type) ?? [];\n list.push(comp);\n byType.set(comp.type, list);\n }\n\n // Naming: single component of a type → flat, multiple → prefixed\n const prefix = (comp: ShellyComponentConfig): string => {\n return comp.name ? toCamelCase(comp.name) : `${DEFAULT_PREFIXES[comp.type]}${comp.id}`;\n };\n const p = (comp: ShellyComponentConfig, key: string): string => {\n const siblings = byType.get(comp.type)!;\n return siblings.length === 1 ? key : `${prefix(comp)}.${key}`;\n };\n\n // Track all property names for reset on disconnect\n const allPropertyNames: string[] = [];\n const define = (comp: ShellyComponentConfig, key: string, def: Parameters<typeof obj.defineProperty>[1]) => {\n const name = p(comp, key);\n obj.defineProperty(name, def);\n allPropertyNames.push(name);\n };\n\n // Build a lookup from \"type:id\" to component config for WS dispatch\n const compLookup = new Map<string, ShellyComponentConfig>();\n for (const comp of components) compLookup.set(`${comp.type}:${comp.id}`, comp);\n\n // ── Switch properties ─────────────────────────────────\n\n for (const comp of byType.get(\"switch\") ?? []) {\n define(comp, \"on\", { type: \"boolean\", writable: true });\n if (comp.metering) {\n define(comp, \"power\", { type: \"float\", writable: false });\n define(comp, \"voltage\", { type: \"float\", writable: false });\n define(comp, \"current\", { type: \"float\", writable: false });\n define(comp, \"energy\", { type: \"float\", writable: false });\n }\n define(comp, \"temperature\", { type: \"float\", writable: false });\n\n obj.onSet(p(comp, \"on\"), async (value) => {\n try {\n await rpc(host, \"Switch.Set\", { id: comp.id, on: value });\n obj.set(p(comp, \"on\"), value as boolean);\n plugin.log.info(`${id} ${prefix(comp)} → ${value ? \"ON\" : \"OFF\"}`);\n } catch (e) {\n plugin.log.error(`Failed to set ${id} switch: ${(e as Error).message}`);\n }\n });\n }\n\n // ── Input properties + events ─────────────────────────\n\n const inputs = byType.get(\"input\") ?? [];\n for (const comp of inputs) {\n define(comp, \"state\", { type: \"boolean\", writable: false });\n\n for (const name of INPUT_EVENT_NAMES) {\n obj.defineEvent(p(comp, name));\n }\n }\n\n // ── Cover properties ──────────────────────────────────\n\n for (const comp of byType.get(\"cover\") ?? []) {\n define(comp, \"state\", { type: \"string\", writable: false });\n define(comp, \"position\", { type: \"integer\", writable: true });\n if (comp.metering) {\n define(comp, \"power\", { type: \"float\", writable: false });\n define(comp, \"voltage\", { type: \"float\", writable: false });\n define(comp, \"current\", { type: \"float\", writable: false });\n }\n define(comp, \"temperature\", { type: \"float\", writable: false });\n\n obj.onSet(p(comp, \"position\"), async (value) => {\n try {\n await rpc(host, \"Cover.GoToPosition\", { id: comp.id, pos: value });\n plugin.log.info(`${id} ${prefix(comp)} → position ${value}`);\n } catch (e) {\n plugin.log.error(`Failed to set ${id} cover position: ${(e as Error).message}`);\n }\n });\n }\n\n // ── Light properties ──────────────────────────────────\n\n for (const comp of byType.get(\"light\") ?? []) {\n define(comp, \"on\", { type: \"boolean\", writable: true });\n define(comp, \"brightness\", { type: \"integer\", writable: true });\n\n obj.onSet(p(comp, \"on\"), async (value) => {\n try {\n await rpc(host, \"Light.Set\", { id: comp.id, on: value });\n obj.set(p(comp, \"on\"), value as boolean);\n plugin.log.info(`${id} ${prefix(comp)} → ${value ? \"ON\" : \"OFF\"}`);\n } catch (e) {\n plugin.log.error(`Failed to set ${id} light: ${(e as Error).message}`);\n }\n });\n\n obj.onSet(p(comp, \"brightness\"), async (value) => {\n try {\n await rpc(host, \"Light.Set\", { id: comp.id, brightness: value });\n obj.set(p(comp, \"brightness\"), value as number);\n plugin.log.info(`${id} ${prefix(comp)} → brightness ${value}`);\n } catch (e) {\n plugin.log.error(`Failed to set ${id} brightness: ${(e as Error).message}`);\n }\n });\n }\n\n // ── Firmware properties ──────────────────────────────────\n\n if (trackFirmware) {\n obj.defineProperty(\"firmware.installedVersion\", { type: \"string\", writable: false });\n obj.defineProperty(\"firmware.latestVersion\", { type: \"string\", writable: false });\n obj.defineProperty(\"firmware.updateAvailable\", { type: \"boolean\", writable: false });\n allPropertyNames.push(\"firmware.installedVersion\", \"firmware.latestVersion\", \"firmware.updateAvailable\");\n }\n\n // ── Availability ──────────────────────────────────────\n\n // Firmware state tracked across two RPC responses (GetDeviceInfo + GetStatus sys)\n let fwInstalled: string | null = null;\n let fwChecked = false;\n let fwStableVersion: string | null = null;\n\n const updateFirmwareProps = () => {\n if (!trackFirmware) return;\n obj.set(\"firmware.installedVersion\", fwInstalled);\n if (fwChecked) {\n obj.set(\"firmware.latestVersion\", fwStableVersion ?? fwInstalled);\n obj.set(\"firmware.updateAvailable\", fwStableVersion !== null);\n }\n };\n\n const resetProperties = () => {\n for (const name of allPropertyNames) obj.set(name, null);\n fwInstalled = null;\n fwChecked = false;\n fwStableVersion = null;\n };\n\n // ── WebSocket ─────────────────────────────────────────\n\n const connect = () => {\n const ws = connectWs(\n host,\n (type, compId, data) => {\n const comp = compLookup.get(`${type}:${compId}`);\n if (!comp) return;\n\n switch (type) {\n case \"switch\": {\n if (\"output\" in data) obj.set(p(comp, \"on\"), data.output);\n if (comp.metering) {\n if (\"apower\" in data) obj.set(p(comp, \"power\"), data.apower);\n if (\"voltage\" in data) obj.set(p(comp, \"voltage\"), data.voltage);\n if (\"current\" in data) obj.set(p(comp, \"current\"), data.current);\n if (data.aenergy && \"total\" in data.aenergy) obj.set(p(comp, \"energy\"), data.aenergy.total);\n }\n if (data.temperature && \"tC\" in data.temperature) obj.set(p(comp, \"temperature\"), data.temperature.tC);\n break;\n }\n case \"input\": {\n if (\"state\" in data) obj.set(p(comp, \"state\"), data.state);\n break;\n }\n case \"cover\": {\n if (\"state\" in data) obj.set(p(comp, \"state\"), data.state);\n if (\"current_pos\" in data && data.current_pos !== null) obj.set(p(comp, \"position\"), data.current_pos);\n if (comp.metering) {\n if (\"apower\" in data) obj.set(p(comp, \"power\"), data.apower);\n if (\"voltage\" in data) obj.set(p(comp, \"voltage\"), data.voltage);\n if (\"current\" in data) obj.set(p(comp, \"current\"), data.current);\n }\n if (data.temperature && \"tC\" in data.temperature) obj.set(p(comp, \"temperature\"), data.temperature.tC);\n break;\n }\n case \"light\": {\n if (\"output\" in data) obj.set(p(comp, \"on\"), data.output);\n if (\"brightness\" in data) obj.set(p(comp, \"brightness\"), data.brightness);\n break;\n }\n }\n },\n (sysData) => {\n const stable = sysData.available_updates?.stable;\n fwChecked = true;\n fwStableVersion = stable?.version ?? null;\n updateFirmwareProps();\n },\n (info) => {\n if (info.ver) {\n fwInstalled = info.ver;\n updateFirmwareProps();\n }\n },\n (inputEvent) => {\n const comp = compLookup.get(`input:${inputEvent.id}`);\n if (!comp) return;\n const name = SHELLY_EVENT_MAP[inputEvent.event];\n if (name) obj.emitEvent(p(comp, name));\n },\n (err) => plugin.log.error(`WS ${id} (${host}): ${err.message}`),\n () => {\n plugin.log.warn(`WS ${id} (${host}) disconnected, reconnecting in 5s`);\n resetProperties();\n setTimeout(connect, 5000);\n },\n );\n wsConnections.push(ws);\n };\n\n connect();\n\n const summary = [...byType.entries()].map(([t, cs]) => `${cs.length} ${t}`).join(\", \");\n plugin.log.info(`Registered ${id} at ${host} [${summary}]`);\n };\n\n // ── Load known objects from disk ────────────────────────\n\n const loadKnownObjects = () => {\n const ids = plugin.config.list().filter((id) => id !== plugin.pluginId);\n for (const id of ids) {\n const raw = plugin.config.load<Record<string, any>>(id);\n if (!raw) continue;\n const deviceConfig = migrateConfig(raw);\n setupObject(id, deviceConfig, id);\n }\n return ids.length;\n };\n\n // ── mDNS discovery ──────────────────────────────────────\n\n let discoveryTimer: ReturnType<typeof setInterval> | null = null;\n\n const runDiscovery = async () => {\n try {\n const bonjour = new Bonjour();\n const found = new Map<string, string>();\n\n const browser = bonjour.find({ type: \"shelly\" }, (service) => {\n const host = service.referer?.address ?? service.addresses?.[0];\n if (host) found.set(host, service.name);\n });\n\n setTimeout(async () => {\n browser.stop();\n bonjour.destroy();\n\n for (const [host] of found) {\n const info = await probeShelly(host);\n if (!info) continue;\n\n const id = info.id.toLowerCase().replace(/[^a-z0-9-]/g, \"-\");\n if (registeredObjects.has(id)) continue;\n\n const deviceConfig: ShellyDeviceConfig = {\n host,\n model: info.model,\n mac: info.mac,\n components: info.components,\n };\n\n plugin.config.save(id, deviceConfig);\n setupObject(id, deviceConfig, info.name);\n\n const summary = info.components.map((c) => c.type).join(\", \");\n plugin.log.info(`Discovered: ${info.name} (${id}) at ${host} [${summary}]`);\n }\n }, 5000);\n } catch (e) {\n plugin.log.error(`Discovery failed: ${(e as Error).message}`);\n }\n };\n\n // ── Start ────────────────────────────────────────────────\n\n const knownCount = loadKnownObjects();\n plugin.log.info(`Shelly plugin started — ${knownCount} known object(s)`);\n runDiscovery();\n discoveryTimer = setInterval(runDiscovery, discoveryInterval);\n\n // Wire up config change handlers after discovery timer is created\n configObject.onSet(\"config.discoveryInterval\", (value) => {\n discoveryInterval = value as number;\n saveConfig();\n configObject.set(\"config.discoveryInterval\", value);\n if (discoveryTimer) clearInterval(discoveryTimer);\n discoveryTimer = setInterval(runDiscovery, discoveryInterval);\n plugin.log.info(`Discovery interval changed to ${discoveryInterval}ms`);\n });\n\n configObject.onSet(\"config.trackFirmware\", (value) => {\n trackFirmware = value as boolean;\n saveConfig();\n configObject.set(\"config.trackFirmware\", value);\n plugin.log.info(`Firmware tracking ${trackFirmware ? \"enabled\" : \"disabled\"} (takes effect on next reload)`);\n });\n\n plugin.on(\"stop\", async () => {\n for (const ws of wsConnections) ws.close();\n if (discoveryTimer) clearInterval(discoveryTimer);\n plugin.log.info(\"Shelly plugin stopped\");\n });\n}\n"]}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
// Shelly plugin for Tomfy
|
|
2
|
+
// Supports Gen2 devices via auto-detected components.
|
|
3
|
+
//
|
|
4
|
+
// Configs are stored in ~/.tomfy/config/shelly/<object-id>.json
|
|
5
|
+
// New devices are auto-discovered via mDNS and saved automatically.
|
|
6
|
+
// Uses WebSocket for real-time push updates.
|
|
7
|
+
//
|
|
8
|
+
// Components detected from Shelly.GetStatus:
|
|
9
|
+
// switch — on, power*, voltage*, current*, temperature, energy* (* PM models only)
|
|
10
|
+
// input — state property + singlePush/doublePush/longPush events
|
|
11
|
+
// cover — state, position, power*, voltage*, current*, temperature
|
|
12
|
+
// light — on, brightness
|
|
13
|
+
//
|
|
14
|
+
// Object-level properties (all objects):
|
|
15
|
+
// firmware.installedVersion, firmware.latestVersion, firmware.updateAvailable
|
|
16
|
+
// (only when config.trackFirmware is true)
|
|
17
|
+
//
|
|
18
|
+
// Naming convention (same for all component types):
|
|
19
|
+
// Single component: <property> e.g. on, power
|
|
20
|
+
// Multiple: <name>.<property> e.g. output0.on, kitchen.power
|
|
21
|
+
// Component names use camelCased Shelly config name when available,
|
|
22
|
+
// otherwise fall back to output0/input0/cover0/light0.
|
|
23
|
+
//
|
|
24
|
+
// Plugin config is exposed as properties on an object named "shelly":
|
|
25
|
+
// tomfy get shelly.config — view all config
|
|
26
|
+
// tomfy set shelly.config.discoveryInterval 120000 — update config
|
|
27
|
+
// tomfy set shelly.config.trackFirmware true — enable firmware tracking
|
|
28
|
+
import { toCamelCase } from "@tomfy/core";
|
|
29
|
+
import { Bonjour } from "bonjour-service";
|
|
30
|
+
const migrateConfig = (raw) => {
|
|
31
|
+
if (raw.components)
|
|
32
|
+
return raw;
|
|
33
|
+
// Convert old outputs/inputs format to components array
|
|
34
|
+
const legacy = raw;
|
|
35
|
+
const components = [];
|
|
36
|
+
const outputs = legacy.outputs ?? 0;
|
|
37
|
+
for (let i = 0; i < outputs; i++) {
|
|
38
|
+
components.push({
|
|
39
|
+
type: "switch",
|
|
40
|
+
id: i,
|
|
41
|
+
name: legacy.outputNames?.[i] ?? null,
|
|
42
|
+
metering: true, // assume PM for old configs; re-probe will correct
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const inputs = legacy.inputs ?? 0;
|
|
46
|
+
for (let i = 0; i < inputs; i++) {
|
|
47
|
+
components.push({
|
|
48
|
+
type: "input",
|
|
49
|
+
id: i,
|
|
50
|
+
name: legacy.inputNames?.[i] ?? null,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { host: legacy.host, model: legacy.model, mac: legacy.mac, components };
|
|
54
|
+
};
|
|
55
|
+
// ── HTTP API ────────────────────────────────────────────────
|
|
56
|
+
const rpc = async (host, method, params = {}) => {
|
|
57
|
+
const res = await fetch(`http://${host}/rpc/${method}`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify(params),
|
|
61
|
+
signal: AbortSignal.timeout(5000),
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok)
|
|
64
|
+
throw new Error(`HTTP ${res.status}`);
|
|
65
|
+
};
|
|
66
|
+
const COMPONENT_RE = /^(switch|input|cover|light):(\d+)$/;
|
|
67
|
+
const connectWs = (host, onComponentStatus, onSysStatus, onDeviceInfo, onInputEvent, onError, onClose) => {
|
|
68
|
+
const ws = new WebSocket(`ws://${host}/rpc`);
|
|
69
|
+
let rpcId = 1;
|
|
70
|
+
let alive = true;
|
|
71
|
+
const extractComponents = (obj) => {
|
|
72
|
+
for (const [key, data] of Object.entries(obj)) {
|
|
73
|
+
if (key === "sys" && data && typeof data === "object") {
|
|
74
|
+
onSysStatus(data);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const m = COMPONENT_RE.exec(key);
|
|
78
|
+
if (m && data && typeof data === "object") {
|
|
79
|
+
onComponentStatus(m[1], parseInt(m[2], 10), data);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
ws.addEventListener("open", () => {
|
|
84
|
+
ws.send(JSON.stringify({ jsonrpc: "2.0", id: rpcId++, src: "tomfy", method: "Shelly.GetStatus" }));
|
|
85
|
+
ws.send(JSON.stringify({ jsonrpc: "2.0", id: rpcId++, src: "tomfy", method: "Shelly.GetDeviceInfo" }));
|
|
86
|
+
});
|
|
87
|
+
ws.addEventListener("message", (event) => {
|
|
88
|
+
try {
|
|
89
|
+
const msg = JSON.parse(String(event.data));
|
|
90
|
+
if (msg.result) {
|
|
91
|
+
// Distinguish GetDeviceInfo (has "ver") from GetStatus (has component keys)
|
|
92
|
+
if ("ver" in msg.result) {
|
|
93
|
+
onDeviceInfo(msg.result);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
extractComponents(msg.result);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (msg.method === "NotifyStatus" && msg.params)
|
|
100
|
+
extractComponents(msg.params);
|
|
101
|
+
if (msg.method === "NotifyFullStatus" && msg.params)
|
|
102
|
+
extractComponents(msg.params);
|
|
103
|
+
if (msg.method === "NotifyEvent" && msg.params?.events) {
|
|
104
|
+
for (const evt of msg.params.events) {
|
|
105
|
+
if (evt.component?.startsWith("input:"))
|
|
106
|
+
onInputEvent(evt);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
onError(e);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
ws.addEventListener("error", () => onError(new Error(`WebSocket error for ${host}`)));
|
|
115
|
+
ws.addEventListener("close", () => {
|
|
116
|
+
if (alive)
|
|
117
|
+
onClose();
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
close() {
|
|
121
|
+
alive = false;
|
|
122
|
+
ws.close();
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const probeShelly = async (host) => {
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch(`http://${host}/shelly`, { signal: AbortSignal.timeout(3000) });
|
|
129
|
+
if (!res.ok)
|
|
130
|
+
return null;
|
|
131
|
+
const data = (await res.json());
|
|
132
|
+
const gen = data.gen ?? 1;
|
|
133
|
+
if (gen < 2)
|
|
134
|
+
return null;
|
|
135
|
+
// Detect components from Shelly.GetStatus
|
|
136
|
+
let status = {};
|
|
137
|
+
try {
|
|
138
|
+
const statusRes = await fetch(`http://${host}/rpc/Shelly.GetStatus`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: { "Content-Type": "application/json" },
|
|
141
|
+
body: JSON.stringify({ id: 1 }),
|
|
142
|
+
signal: AbortSignal.timeout(3000),
|
|
143
|
+
});
|
|
144
|
+
if (statusRes.ok)
|
|
145
|
+
status = (await statusRes.json());
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* empty status */
|
|
149
|
+
}
|
|
150
|
+
// Fetch config for component names
|
|
151
|
+
let cfg = {};
|
|
152
|
+
try {
|
|
153
|
+
const configRes = await fetch(`http://${host}/rpc/Shelly.GetConfig`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "Content-Type": "application/json" },
|
|
156
|
+
body: JSON.stringify({ id: 1 }),
|
|
157
|
+
signal: AbortSignal.timeout(3000),
|
|
158
|
+
});
|
|
159
|
+
if (configRes.ok)
|
|
160
|
+
cfg = (await configRes.json());
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
/* no config */
|
|
164
|
+
}
|
|
165
|
+
const components = [];
|
|
166
|
+
for (const key of Object.keys(status)) {
|
|
167
|
+
const m = COMPONENT_RE.exec(key);
|
|
168
|
+
if (!m)
|
|
169
|
+
continue;
|
|
170
|
+
const type = m[1];
|
|
171
|
+
const id = parseInt(m[2], 10);
|
|
172
|
+
const compStatus = status[key];
|
|
173
|
+
const compCfg = cfg[key];
|
|
174
|
+
const comp = {
|
|
175
|
+
type,
|
|
176
|
+
id,
|
|
177
|
+
name: compCfg?.name || null,
|
|
178
|
+
};
|
|
179
|
+
// Detect power metering for switch and cover
|
|
180
|
+
if ((type === "switch" || type === "cover") && compStatus && "apower" in compStatus) {
|
|
181
|
+
comp.metering = true;
|
|
182
|
+
}
|
|
183
|
+
components.push(comp);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
id: data.id ?? data.hostname ?? `shelly-${host.replace(/\./g, "-")}`,
|
|
187
|
+
name: data.name || data.app || data.id || host,
|
|
188
|
+
model: data.model ?? "unknown",
|
|
189
|
+
mac: data.mac ?? "",
|
|
190
|
+
gen,
|
|
191
|
+
components,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
// ── Plugin ──────────────────────────────────────────────────
|
|
199
|
+
const INPUT_EVENT_NAMES = ["buttonDown", "buttonUp", "push", "doublePush", "longPush"];
|
|
200
|
+
const SHELLY_EVENT_MAP = {
|
|
201
|
+
btn_down: "buttonDown",
|
|
202
|
+
btn_up: "buttonUp",
|
|
203
|
+
single_push: "push",
|
|
204
|
+
double_push: "doublePush",
|
|
205
|
+
long_push: "longPush",
|
|
206
|
+
};
|
|
207
|
+
const DEFAULT_PREFIXES = {
|
|
208
|
+
switch: "output",
|
|
209
|
+
input: "input",
|
|
210
|
+
cover: "cover",
|
|
211
|
+
light: "light",
|
|
212
|
+
};
|
|
213
|
+
export default function (plugin) {
|
|
214
|
+
// ── Config object ───────────────────────────────────────
|
|
215
|
+
// Expose plugin config as properties on an object named after the plugin.
|
|
216
|
+
// Persisted in ~/.tomfy/config/shelly/shelly.json alongside hardware objects.
|
|
217
|
+
const savedConfig = plugin.config.load(plugin.pluginId);
|
|
218
|
+
let discoveryInterval = typeof savedConfig?.discoveryInterval === "number" ? savedConfig.discoveryInterval : 60000;
|
|
219
|
+
let trackFirmware = typeof savedConfig?.trackFirmware === "boolean" ? savedConfig.trackFirmware : false;
|
|
220
|
+
const configObject = plugin.registerObject(plugin.pluginId, { name: plugin.pluginId });
|
|
221
|
+
configObject.defineProperty("config.discoveryInterval", {
|
|
222
|
+
type: "integer",
|
|
223
|
+
writable: true,
|
|
224
|
+
value: discoveryInterval,
|
|
225
|
+
});
|
|
226
|
+
configObject.defineProperty("config.trackFirmware", {
|
|
227
|
+
type: "boolean",
|
|
228
|
+
writable: true,
|
|
229
|
+
value: trackFirmware,
|
|
230
|
+
});
|
|
231
|
+
const saveConfig = () => {
|
|
232
|
+
const existing = plugin.config.load(plugin.pluginId) ?? {};
|
|
233
|
+
plugin.config.save(plugin.pluginId, { ...existing, discoveryInterval, trackFirmware });
|
|
234
|
+
};
|
|
235
|
+
const wsConnections = [];
|
|
236
|
+
const registeredObjects = new Set();
|
|
237
|
+
// ── Register an object ──────────────────────────────────
|
|
238
|
+
const setupObject = (id, deviceConfig, displayName) => {
|
|
239
|
+
if (registeredObjects.has(id))
|
|
240
|
+
return;
|
|
241
|
+
registeredObjects.add(id);
|
|
242
|
+
const { host, components } = deviceConfig;
|
|
243
|
+
const obj = plugin.registerObject(id, { name: displayName });
|
|
244
|
+
// Group components by type for single/multi naming
|
|
245
|
+
const byType = new Map();
|
|
246
|
+
for (const comp of components) {
|
|
247
|
+
const list = byType.get(comp.type) ?? [];
|
|
248
|
+
list.push(comp);
|
|
249
|
+
byType.set(comp.type, list);
|
|
250
|
+
}
|
|
251
|
+
// Naming: single component of a type → flat, multiple → prefixed
|
|
252
|
+
const prefix = (comp) => {
|
|
253
|
+
return comp.name ? toCamelCase(comp.name) : `${DEFAULT_PREFIXES[comp.type]}${comp.id}`;
|
|
254
|
+
};
|
|
255
|
+
const p = (comp, key) => {
|
|
256
|
+
const siblings = byType.get(comp.type);
|
|
257
|
+
return siblings.length === 1 ? key : `${prefix(comp)}.${key}`;
|
|
258
|
+
};
|
|
259
|
+
// Track all property names for reset on disconnect
|
|
260
|
+
const allPropertyNames = [];
|
|
261
|
+
const define = (comp, key, def) => {
|
|
262
|
+
const name = p(comp, key);
|
|
263
|
+
obj.defineProperty(name, def);
|
|
264
|
+
allPropertyNames.push(name);
|
|
265
|
+
};
|
|
266
|
+
// Build a lookup from "type:id" to component config for WS dispatch
|
|
267
|
+
const compLookup = new Map();
|
|
268
|
+
for (const comp of components)
|
|
269
|
+
compLookup.set(`${comp.type}:${comp.id}`, comp);
|
|
270
|
+
// ── Switch properties ─────────────────────────────────
|
|
271
|
+
for (const comp of byType.get("switch") ?? []) {
|
|
272
|
+
define(comp, "on", { type: "boolean", writable: true });
|
|
273
|
+
if (comp.metering) {
|
|
274
|
+
define(comp, "power", { type: "float", writable: false });
|
|
275
|
+
define(comp, "voltage", { type: "float", writable: false });
|
|
276
|
+
define(comp, "current", { type: "float", writable: false });
|
|
277
|
+
define(comp, "energy", { type: "float", writable: false });
|
|
278
|
+
}
|
|
279
|
+
define(comp, "temperature", { type: "float", writable: false });
|
|
280
|
+
obj.onSet(p(comp, "on"), async (value) => {
|
|
281
|
+
try {
|
|
282
|
+
await rpc(host, "Switch.Set", { id: comp.id, on: value });
|
|
283
|
+
obj.set(p(comp, "on"), value);
|
|
284
|
+
plugin.log.info(`${id} ${prefix(comp)} → ${value ? "ON" : "OFF"}`);
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
plugin.log.error(`Failed to set ${id} switch: ${e.message}`);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
// ── Input properties + events ─────────────────────────
|
|
292
|
+
const inputs = byType.get("input") ?? [];
|
|
293
|
+
for (const comp of inputs) {
|
|
294
|
+
define(comp, "state", { type: "boolean", writable: false });
|
|
295
|
+
for (const name of INPUT_EVENT_NAMES) {
|
|
296
|
+
obj.defineEvent(p(comp, name));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// ── Cover properties ──────────────────────────────────
|
|
300
|
+
for (const comp of byType.get("cover") ?? []) {
|
|
301
|
+
define(comp, "state", { type: "string", writable: false });
|
|
302
|
+
define(comp, "position", { type: "integer", writable: true });
|
|
303
|
+
if (comp.metering) {
|
|
304
|
+
define(comp, "power", { type: "float", writable: false });
|
|
305
|
+
define(comp, "voltage", { type: "float", writable: false });
|
|
306
|
+
define(comp, "current", { type: "float", writable: false });
|
|
307
|
+
}
|
|
308
|
+
define(comp, "temperature", { type: "float", writable: false });
|
|
309
|
+
obj.onSet(p(comp, "position"), async (value) => {
|
|
310
|
+
try {
|
|
311
|
+
await rpc(host, "Cover.GoToPosition", { id: comp.id, pos: value });
|
|
312
|
+
plugin.log.info(`${id} ${prefix(comp)} → position ${value}`);
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
plugin.log.error(`Failed to set ${id} cover position: ${e.message}`);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// ── Light properties ──────────────────────────────────
|
|
320
|
+
for (const comp of byType.get("light") ?? []) {
|
|
321
|
+
define(comp, "on", { type: "boolean", writable: true });
|
|
322
|
+
define(comp, "brightness", { type: "integer", writable: true });
|
|
323
|
+
obj.onSet(p(comp, "on"), async (value) => {
|
|
324
|
+
try {
|
|
325
|
+
await rpc(host, "Light.Set", { id: comp.id, on: value });
|
|
326
|
+
obj.set(p(comp, "on"), value);
|
|
327
|
+
plugin.log.info(`${id} ${prefix(comp)} → ${value ? "ON" : "OFF"}`);
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
plugin.log.error(`Failed to set ${id} light: ${e.message}`);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
obj.onSet(p(comp, "brightness"), async (value) => {
|
|
334
|
+
try {
|
|
335
|
+
await rpc(host, "Light.Set", { id: comp.id, brightness: value });
|
|
336
|
+
obj.set(p(comp, "brightness"), value);
|
|
337
|
+
plugin.log.info(`${id} ${prefix(comp)} → brightness ${value}`);
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
plugin.log.error(`Failed to set ${id} brightness: ${e.message}`);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// ── Firmware properties ──────────────────────────────────
|
|
345
|
+
if (trackFirmware) {
|
|
346
|
+
obj.defineProperty("firmware.installedVersion", { type: "string", writable: false });
|
|
347
|
+
obj.defineProperty("firmware.latestVersion", { type: "string", writable: false });
|
|
348
|
+
obj.defineProperty("firmware.updateAvailable", { type: "boolean", writable: false });
|
|
349
|
+
allPropertyNames.push("firmware.installedVersion", "firmware.latestVersion", "firmware.updateAvailable");
|
|
350
|
+
}
|
|
351
|
+
// ── Availability ──────────────────────────────────────
|
|
352
|
+
// Firmware state tracked across two RPC responses (GetDeviceInfo + GetStatus sys)
|
|
353
|
+
let fwInstalled = null;
|
|
354
|
+
let fwChecked = false;
|
|
355
|
+
let fwStableVersion = null;
|
|
356
|
+
const updateFirmwareProps = () => {
|
|
357
|
+
if (!trackFirmware)
|
|
358
|
+
return;
|
|
359
|
+
obj.set("firmware.installedVersion", fwInstalled);
|
|
360
|
+
if (fwChecked) {
|
|
361
|
+
obj.set("firmware.latestVersion", fwStableVersion ?? fwInstalled);
|
|
362
|
+
obj.set("firmware.updateAvailable", fwStableVersion !== null);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const resetProperties = () => {
|
|
366
|
+
for (const name of allPropertyNames)
|
|
367
|
+
obj.set(name, null);
|
|
368
|
+
fwInstalled = null;
|
|
369
|
+
fwChecked = false;
|
|
370
|
+
fwStableVersion = null;
|
|
371
|
+
};
|
|
372
|
+
// ── WebSocket ─────────────────────────────────────────
|
|
373
|
+
const connect = () => {
|
|
374
|
+
const ws = connectWs(host, (type, compId, data) => {
|
|
375
|
+
const comp = compLookup.get(`${type}:${compId}`);
|
|
376
|
+
if (!comp)
|
|
377
|
+
return;
|
|
378
|
+
switch (type) {
|
|
379
|
+
case "switch": {
|
|
380
|
+
if ("output" in data)
|
|
381
|
+
obj.set(p(comp, "on"), data.output);
|
|
382
|
+
if (comp.metering) {
|
|
383
|
+
if ("apower" in data)
|
|
384
|
+
obj.set(p(comp, "power"), data.apower);
|
|
385
|
+
if ("voltage" in data)
|
|
386
|
+
obj.set(p(comp, "voltage"), data.voltage);
|
|
387
|
+
if ("current" in data)
|
|
388
|
+
obj.set(p(comp, "current"), data.current);
|
|
389
|
+
if (data.aenergy && "total" in data.aenergy)
|
|
390
|
+
obj.set(p(comp, "energy"), data.aenergy.total);
|
|
391
|
+
}
|
|
392
|
+
if (data.temperature && "tC" in data.temperature)
|
|
393
|
+
obj.set(p(comp, "temperature"), data.temperature.tC);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
case "input": {
|
|
397
|
+
if ("state" in data)
|
|
398
|
+
obj.set(p(comp, "state"), data.state);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case "cover": {
|
|
402
|
+
if ("state" in data)
|
|
403
|
+
obj.set(p(comp, "state"), data.state);
|
|
404
|
+
if ("current_pos" in data && data.current_pos !== null)
|
|
405
|
+
obj.set(p(comp, "position"), data.current_pos);
|
|
406
|
+
if (comp.metering) {
|
|
407
|
+
if ("apower" in data)
|
|
408
|
+
obj.set(p(comp, "power"), data.apower);
|
|
409
|
+
if ("voltage" in data)
|
|
410
|
+
obj.set(p(comp, "voltage"), data.voltage);
|
|
411
|
+
if ("current" in data)
|
|
412
|
+
obj.set(p(comp, "current"), data.current);
|
|
413
|
+
}
|
|
414
|
+
if (data.temperature && "tC" in data.temperature)
|
|
415
|
+
obj.set(p(comp, "temperature"), data.temperature.tC);
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case "light": {
|
|
419
|
+
if ("output" in data)
|
|
420
|
+
obj.set(p(comp, "on"), data.output);
|
|
421
|
+
if ("brightness" in data)
|
|
422
|
+
obj.set(p(comp, "brightness"), data.brightness);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}, (sysData) => {
|
|
427
|
+
const stable = sysData.available_updates?.stable;
|
|
428
|
+
fwChecked = true;
|
|
429
|
+
fwStableVersion = stable?.version ?? null;
|
|
430
|
+
updateFirmwareProps();
|
|
431
|
+
}, (info) => {
|
|
432
|
+
if (info.ver) {
|
|
433
|
+
fwInstalled = info.ver;
|
|
434
|
+
updateFirmwareProps();
|
|
435
|
+
}
|
|
436
|
+
}, (inputEvent) => {
|
|
437
|
+
const comp = compLookup.get(`input:${inputEvent.id}`);
|
|
438
|
+
if (!comp)
|
|
439
|
+
return;
|
|
440
|
+
const name = SHELLY_EVENT_MAP[inputEvent.event];
|
|
441
|
+
if (name)
|
|
442
|
+
obj.emitEvent(p(comp, name));
|
|
443
|
+
}, (err) => plugin.log.error(`WS ${id} (${host}): ${err.message}`), () => {
|
|
444
|
+
plugin.log.warn(`WS ${id} (${host}) disconnected, reconnecting in 5s`);
|
|
445
|
+
resetProperties();
|
|
446
|
+
setTimeout(connect, 5000);
|
|
447
|
+
});
|
|
448
|
+
wsConnections.push(ws);
|
|
449
|
+
};
|
|
450
|
+
connect();
|
|
451
|
+
const summary = [...byType.entries()].map(([t, cs]) => `${cs.length} ${t}`).join(", ");
|
|
452
|
+
plugin.log.info(`Registered ${id} at ${host} [${summary}]`);
|
|
453
|
+
};
|
|
454
|
+
// ── Load known objects from disk ────────────────────────
|
|
455
|
+
const loadKnownObjects = () => {
|
|
456
|
+
const ids = plugin.config.list().filter((id) => id !== plugin.pluginId);
|
|
457
|
+
for (const id of ids) {
|
|
458
|
+
const raw = plugin.config.load(id);
|
|
459
|
+
if (!raw)
|
|
460
|
+
continue;
|
|
461
|
+
const deviceConfig = migrateConfig(raw);
|
|
462
|
+
setupObject(id, deviceConfig, id);
|
|
463
|
+
}
|
|
464
|
+
return ids.length;
|
|
465
|
+
};
|
|
466
|
+
// ── mDNS discovery ──────────────────────────────────────
|
|
467
|
+
let discoveryTimer = null;
|
|
468
|
+
const runDiscovery = async () => {
|
|
469
|
+
try {
|
|
470
|
+
const bonjour = new Bonjour();
|
|
471
|
+
const found = new Map();
|
|
472
|
+
const browser = bonjour.find({ type: "shelly" }, (service) => {
|
|
473
|
+
const host = service.referer?.address ?? service.addresses?.[0];
|
|
474
|
+
if (host)
|
|
475
|
+
found.set(host, service.name);
|
|
476
|
+
});
|
|
477
|
+
setTimeout(async () => {
|
|
478
|
+
browser.stop();
|
|
479
|
+
bonjour.destroy();
|
|
480
|
+
for (const [host] of found) {
|
|
481
|
+
const info = await probeShelly(host);
|
|
482
|
+
if (!info)
|
|
483
|
+
continue;
|
|
484
|
+
const id = info.id.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
485
|
+
if (registeredObjects.has(id))
|
|
486
|
+
continue;
|
|
487
|
+
const deviceConfig = {
|
|
488
|
+
host,
|
|
489
|
+
model: info.model,
|
|
490
|
+
mac: info.mac,
|
|
491
|
+
components: info.components,
|
|
492
|
+
};
|
|
493
|
+
plugin.config.save(id, deviceConfig);
|
|
494
|
+
setupObject(id, deviceConfig, info.name);
|
|
495
|
+
const summary = info.components.map((c) => c.type).join(", ");
|
|
496
|
+
plugin.log.info(`Discovered: ${info.name} (${id}) at ${host} [${summary}]`);
|
|
497
|
+
}
|
|
498
|
+
}, 5000);
|
|
499
|
+
}
|
|
500
|
+
catch (e) {
|
|
501
|
+
plugin.log.error(`Discovery failed: ${e.message}`);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
// ── Start ────────────────────────────────────────────────
|
|
505
|
+
const knownCount = loadKnownObjects();
|
|
506
|
+
plugin.log.info(`Shelly plugin started — ${knownCount} known object(s)`);
|
|
507
|
+
runDiscovery();
|
|
508
|
+
discoveryTimer = setInterval(runDiscovery, discoveryInterval);
|
|
509
|
+
// Wire up config change handlers after discovery timer is created
|
|
510
|
+
configObject.onSet("config.discoveryInterval", (value) => {
|
|
511
|
+
discoveryInterval = value;
|
|
512
|
+
saveConfig();
|
|
513
|
+
configObject.set("config.discoveryInterval", value);
|
|
514
|
+
if (discoveryTimer)
|
|
515
|
+
clearInterval(discoveryTimer);
|
|
516
|
+
discoveryTimer = setInterval(runDiscovery, discoveryInterval);
|
|
517
|
+
plugin.log.info(`Discovery interval changed to ${discoveryInterval}ms`);
|
|
518
|
+
});
|
|
519
|
+
configObject.onSet("config.trackFirmware", (value) => {
|
|
520
|
+
trackFirmware = value;
|
|
521
|
+
saveConfig();
|
|
522
|
+
configObject.set("config.trackFirmware", value);
|
|
523
|
+
plugin.log.info(`Firmware tracking ${trackFirmware ? "enabled" : "disabled"} (takes effect on next reload)`);
|
|
524
|
+
});
|
|
525
|
+
plugin.on("stop", async () => {
|
|
526
|
+
for (const ws of wsConnections)
|
|
527
|
+
ws.close();
|
|
528
|
+
if (discoveryTimer)
|
|
529
|
+
clearInterval(discoveryTimer);
|
|
530
|
+
plugin.log.info("Shelly plugin stopped");
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,sDAAsD;AACtD,EAAE;AACF,gEAAgE;AAChE,oEAAoE;AACpE,6CAA6C;AAC7C,EAAE;AACF,6CAA6C;AAC7C,uFAAuF;AACvF,qEAAqE;AACrE,uEAAuE;AACvE,6BAA6B;AAC7B,EAAE;AACF,yCAAyC;AACzC,gFAAgF;AAChF,6CAA6C;AAC7C,EAAE;AACF,oDAAoD;AACpD,8DAA8D;AAC9D,+EAA+E;AAC/E,sEAAsE;AACtE,yDAAyD;AACzD,EAAE;AACF,sEAAsE;AACtE,2DAA2D;AAC3D,sEAAsE;AACtE,iFAAiF;AAGjF,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAuC1C,MAAM,aAAa,GAAG,CAAC,GAAwB,EAAsB,EAAE;IACrE,IAAI,GAAG,CAAC,UAAU;QAAE,OAAO,GAAyB,CAAC;IAErD,wDAAwD;IACxD,MAAM,MAAM,GAAG,GAAyB,CAAC;IACzC,MAAM,UAAU,GAA4B,EAAE,CAAC;IAE/C,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;IACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC;QACjC,UAAU,CAAC,IAAI,CAAC;YACd,IAAI,EAAE,QAAQ;YACd,EAAE,EAAE,CAAC;YACL,IAAI,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI;YACrC,QAAQ,EAAE,IAAI,EAAE,mDAAmD;SACpE,CAAC,CAAC;IACL,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,UAAU,CAAC,IAAI,CAAC;YACd,IAAI,EAAE,OAAO;YACb,EAAE,EAAE,CAAC;YACL,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI;SACrC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC;AACjF,CAAC,CAAC;AAEF,+DAA+D;AAE/D,MAAM,GAAG,GAAG,KAAK,EAAE,IAAY,EAAE,MAAc,EAAE,MAAM,GAA4B,EAAE,EAAiB,EAAE;IACtG,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,UAAU,IAAI,QAAQ,MAAM,EAAE,EAAE;QACtD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;QAC5B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;KAClC,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;AACrD,CAAC,CAAC;AAQF,MAAM,YAAY,GAAG,oCAAoC,CAAC;AAE1D,MAAM,SAAS,GAAG,CAChB,IAAY,EACZ,iBAAgF,EAChF,WAAgD,EAChD,YAAiD,EACjD,YAA+C,EAC/C,OAA6B,EAC7B,OAAmB,EACL,EAAE;IAChB,MAAM,EAAE,GAAG,IAAI,SAAS,CAAC,QAAQ,IAAI,MAAM,CAAC,CAAC;IAC7C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,KAAK,GAAG,IAAI,CAAC;IAEjB,MAAM,iBAAiB,GAAG,CAAC,GAAwB,EAAE,EAAE;QACrD,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9C,IAAI,GAAG,KAAK,KAAK,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACtD,WAAW,CAAC,IAA2B,CAAC,CAAC;gBACzC,SAAS;YACX,CAAC;YACD,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,CAAC,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC1C,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,IAA2B,CAAC,CAAC;YAC3E,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAC,CAAC;QACnG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC,CAAC,CAAC;IACzG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAmB,EAAE,EAAE;QACrD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAwB,CAAC;YAElE,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;gBACf,4EAA4E;gBAC5E,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;oBACxB,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBAC3B,CAAC;qBAAM,CAAC;oBACN,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC;YACD,IAAI,GAAG,CAAC,MAAM,KAAK,cAAc,IAAI,GAAG,CAAC,MAAM;gBAAE,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAC/E,IAAI,GAAG,CAAC,MAAM,KAAK,kBAAkB,IAAI,GAAG,CAAC,MAAM;gBAAE,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACnF,IAAI,GAAG,CAAC,MAAM,KAAK,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;gBACvD,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAA4B,EAAE,CAAC;oBAC1D,IAAI,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,QAAQ,CAAC;wBAAE,YAAY,CAAC,GAAG,CAAC,CAAC;gBAC7D,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,CAAU,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACtF,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;QAChC,IAAI,KAAK;YAAE,OAAO,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,KAAK;YACH,KAAK,GAAG,KAAK,CAAC;YACd,EAAE,CAAC,KAAK,EAAE,CAAC;QACb,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAaF,MAAM,WAAW,GAAG,KAAK,EAAE,IAAY,EAA+B,EAAE;IACtE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,UAAU,IAAI,SAAS,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QACzB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAwB,CAAC;QACvD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QAC1B,IAAI,GAAG,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAEzB,0CAA0C;QAC1C,IAAI,MAAM,GAAwB,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,UAAU,IAAI,uBAAuB,EAAE;gBACnE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;gBAC/B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;aAClC,CAAC,CAAC;YACH,IAAI,SAAS,CAAC,EAAE;gBAAE,MAAM,GAAG,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,CAAwB,CAAC;QAC7E,CAAC;QAAC,MAAM,CAAC;YACP,kBAAkB;QACpB,CAAC;QAED,mCAAmC;QACnC,IAAI,GAAG,GAAwB,EAAE,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,UAAU,IAAI,uBAAuB,EAAE;gBACnE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;gBAC/B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;aAClC,CAAC,CAAC;YACH,IAAI,SAAS,CAAC,EAAE;gBAAE,GAAG,GAAG,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,CAAwB,CAAC;QAC1E,CAAC;QAAC,MAAM,CAAC;YACP,eAAe;QACjB,CAAC;QAED,MAAM,UAAU,GAA4B,EAAE,CAAC;QAC/C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YACtC,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,CAAC,CAAC;gBAAE,SAAS;YAEjB,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAwB,CAAC;YACzC,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC9B,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAoC,CAAC;YAClE,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAoC,CAAC;YAE5D,MAAM,IAAI,GAA0B;gBAClC,IAAI;gBACJ,EAAE;gBACF,IAAI,EAAE,OAAO,EAAE,IAAI,IAAI,IAAI;aAC5B,CAAC;YAEF,6CAA6C;YAC7C,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,OAAO,CAAC,IAAI,UAAU,IAAI,QAAQ,IAAI,UAAU,EAAE,CAAC;gBACpF,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACvB,CAAC;YAED,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;QAED,OAAO;YACL,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,QAAQ,IAAI,UAAU,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE;YACpE,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI;YAC9C,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,SAAS;YAC9B,GAAG,EAAE,IAAI,CAAC,GAAG,IAAI,EAAE;YACnB,GAAG;YACH,UAAU;SACX,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,+DAA+D;AAE/D,MAAM,iBAAiB,GAAG,CAAC,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,CAAU,CAAC;AAChG,MAAM,gBAAgB,GAA2B;IAC/C,QAAQ,EAAE,YAAY;IACtB,MAAM,EAAE,UAAU;IAClB,WAAW,EAAE,MAAM;IACnB,WAAW,EAAE,YAAY;IACzB,SAAS,EAAE,UAAU;CACtB,CAAC;AAEF,MAAM,gBAAgB,GAAwC;IAC5D,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;CACf,CAAC;AAEF,MAAM,CAAC,OAAO,WAAW,MAAiB;IACxC,2DAA2D;IAC3D,0EAA0E;IAC1E,8EAA8E;IAE9E,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA0B,MAAM,CAAC,QAAQ,CAAC,CAAC;IACjF,IAAI,iBAAiB,GAAG,OAAO,WAAW,EAAE,iBAAiB,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,iBAAiB,CAAC,CAAC,CAAC,KAAK,CAAC;IACnH,IAAI,aAAa,GAAG,OAAO,WAAW,EAAE,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC;IAExG,MAAM,YAAY,GAAG,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IACvF,YAAY,CAAC,cAAc,CAAC,0BAA0B,EAAE;QACtD,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE,IAAI;QACd,KAAK,EAAE,iBAAiB;KACzB,CAAC,CAAC;IACH,YAAY,CAAC,cAAc,CAAC,sBAAsB,EAAE;QAClD,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE,IAAI;QACd,KAAK,EAAE,aAAa;KACrB,CAAC,CAAC;IAEH,MAAM,UAAU,GAAG,GAAG,EAAE;QACtB,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA0B,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACpF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,GAAG,QAAQ,EAAE,iBAAiB,EAAE,aAAa,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC;IAEF,MAAM,aAAa,GAAmB,EAAE,CAAC;IACzC,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAU,CAAC;IAE5C,2DAA2D;IAE3D,MAAM,WAAW,GAAG,CAAC,EAAU,EAAE,YAAgC,EAAE,WAAmB,EAAE,EAAE;QACxF,IAAI,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO;QACtC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAE1B,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,YAAY,CAAC;QAC1C,MAAM,GAAG,GAAG,MAAM,CAAC,cAAc,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;QAE7D,mDAAmD;QACnD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAmC,CAAC;QAC1D,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACzC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAChB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC9B,CAAC;QAED,iEAAiE;QACjE,MAAM,MAAM,GAAG,CAAC,IAA2B,EAAU,EAAE;YACrD,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC;QACzF,CAAC,CAAC;QACF,MAAM,CAAC,GAAG,CAAC,IAA2B,EAAE,GAAW,EAAU,EAAE;YAC7D,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAE,CAAC;YACxC,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;QAChE,CAAC,CAAC;QAEF,mDAAmD;QACnD,MAAM,gBAAgB,GAAa,EAAE,CAAC;QACtC,MAAM,MAAM,GAAG,CAAC,IAA2B,EAAE,GAAW,EAAE,GAA6C,EAAE,EAAE;YACzG,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC1B,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC9B,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC,CAAC;QAEF,oEAAoE;QACpE,MAAM,UAAU,GAAG,IAAI,GAAG,EAAiC,CAAC;QAC5D,KAAK,MAAM,IAAI,IAAI,UAAU;YAAE,UAAU,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QAE/E,yDAAyD;QAEzD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YAC9C,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YACxD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClB,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC1D,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC5D,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC5D,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YAC7D,CAAC;YACD,MAAM,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YAEhE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBACvC,IAAI,CAAC;oBACH,MAAM,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;oBAC1D,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,KAAgB,CAAC,CAAC;oBACzC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;gBACrE,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,iBAAiB,EAAE,YAAa,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC1E,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAED,yDAAyD;QAEzD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACzC,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YAE5D,KAAK,MAAM,IAAI,IAAI,iBAAiB,EAAE,CAAC;gBACrC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAED,yDAAyD;QAEzD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;YAC7C,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YAC3D,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YAC9D,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClB,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC1D,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC5D,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YAC9D,CAAC;YACD,MAAM,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YAEhE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC7C,IAAI,CAAC;oBACH,MAAM,GAAG,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;oBACnE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,eAAe,KAAK,EAAE,CAAC,CAAC;gBAC/D,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,iBAAiB,EAAE,oBAAqB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;gBAClF,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAED,yDAAyD;QAEzD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;YAC7C,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YACxD,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YAEhE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBACvC,IAAI,CAAC;oBACH,MAAM,GAAG,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;oBACzD,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,KAAgB,CAAC,CAAC;oBACzC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;gBACrE,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,iBAAiB,EAAE,WAAY,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;gBACzE,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC/C,IAAI,CAAC;oBACH,MAAM,GAAG,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;oBACjE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,KAAe,CAAC,CAAC;oBAChD,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,iBAAiB,KAAK,EAAE,CAAC,CAAC;gBACjE,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,iBAAiB,EAAE,gBAAiB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC9E,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAED,4DAA4D;QAE5D,IAAI,aAAa,EAAE,CAAC;YAClB,GAAG,CAAC,cAAc,CAAC,2BAA2B,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YACrF,GAAG,CAAC,cAAc,CAAC,wBAAwB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YAClF,GAAG,CAAC,cAAc,CAAC,0BAA0B,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YACrF,gBAAgB,CAAC,IAAI,CAAC,2BAA2B,EAAE,wBAAwB,EAAE,0BAA0B,CAAC,CAAC;QAC3G,CAAC;QAED,yDAAyD;QAEzD,kFAAkF;QAClF,IAAI,WAAW,GAAkB,IAAI,CAAC;QACtC,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,IAAI,eAAe,GAAkB,IAAI,CAAC;QAE1C,MAAM,mBAAmB,GAAG,GAAG,EAAE;YAC/B,IAAI,CAAC,aAAa;gBAAE,OAAO;YAC3B,GAAG,CAAC,GAAG,CAAC,2BAA2B,EAAE,WAAW,CAAC,CAAC;YAClD,IAAI,SAAS,EAAE,CAAC;gBACd,GAAG,CAAC,GAAG,CAAC,wBAAwB,EAAE,eAAe,IAAI,WAAW,CAAC,CAAC;gBAClE,GAAG,CAAC,GAAG,CAAC,0BAA0B,EAAE,eAAe,KAAK,IAAI,CAAC,CAAC;YAChE,CAAC;QACH,CAAC,CAAC;QAEF,MAAM,eAAe,GAAG,GAAG,EAAE;YAC3B,KAAK,MAAM,IAAI,IAAI,gBAAgB;gBAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACzD,WAAW,GAAG,IAAI,CAAC;YACnB,SAAS,GAAG,KAAK,CAAC;YAClB,eAAe,GAAG,IAAI,CAAC;QACzB,CAAC,CAAC;QAEF,yDAAyD;QAEzD,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,MAAM,EAAE,GAAG,SAAS,CAClB,IAAI,EACJ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;gBACrB,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,IAAI,IAAI,MAAM,EAAE,CAAC,CAAC;gBACjD,IAAI,CAAC,IAAI;oBAAE,OAAO;gBAElB,QAAQ,IAAI,EAAE,CAAC;oBACb,KAAK,QAAQ,EAAE,CAAC;wBACd,IAAI,QAAQ,IAAI,IAAI;4BAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;wBAC1D,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;4BAClB,IAAI,QAAQ,IAAI,IAAI;gCAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;4BAC7D,IAAI,SAAS,IAAI,IAAI;gCAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;4BACjE,IAAI,SAAS,IAAI,IAAI;gCAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;4BACjE,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,IAAI,IAAI,CAAC,OAAO;gCAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;wBAC9F,CAAC;wBACD,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,IAAI,IAAI,CAAC,WAAW;4BAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,aAAa,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;wBACvG,MAAM;oBACR,CAAC;oBACD,KAAK,OAAO,EAAE,CAAC;wBACb,IAAI,OAAO,IAAI,IAAI;4BAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;wBAC3D,MAAM;oBACR,CAAC;oBACD,KAAK,OAAO,EAAE,CAAC;wBACb,IAAI,OAAO,IAAI,IAAI;4BAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;wBAC3D,IAAI,aAAa,IAAI,IAAI,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI;4BAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;wBACvG,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;4BAClB,IAAI,QAAQ,IAAI,IAAI;gCAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;4BAC7D,IAAI,SAAS,IAAI,IAAI;gCAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;4BACjE,IAAI,SAAS,IAAI,IAAI;gCAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;wBACnE,CAAC;wBACD,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,IAAI,IAAI,CAAC,WAAW;4BAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,aAAa,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;wBACvG,MAAM;oBACR,CAAC;oBACD,KAAK,OAAO,EAAE,CAAC;wBACb,IAAI,QAAQ,IAAI,IAAI;4BAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;wBAC1D,IAAI,YAAY,IAAI,IAAI;4BAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;wBAC1E,MAAM;oBACR,CAAC;gBACH,CAAC;YACH,CAAC,EACD,CAAC,OAAO,EAAE,EAAE;gBACV,MAAM,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,MAAM,CAAC;gBACjD,SAAS,GAAG,IAAI,CAAC;gBACjB,eAAe,GAAG,MAAM,EAAE,OAAO,IAAI,IAAI,CAAC;gBAC1C,mBAAmB,EAAE,CAAC;YACxB,CAAC,EACD,CAAC,IAAI,EAAE,EAAE;gBACP,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;oBACb,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC;oBACvB,mBAAmB,EAAE,CAAC;gBACxB,CAAC;YACH,CAAC,EACD,CAAC,UAAU,EAAE,EAAE;gBACb,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;gBACtD,IAAI,CAAC,IAAI;oBAAE,OAAO;gBAClB,MAAM,IAAI,GAAG,gBAAgB,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;gBAChD,IAAI,IAAI;oBAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;YACzC,CAAC,EACD,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC,EAC/D,GAAG,EAAE;gBACH,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,oCAAoC,CAAC,CAAC;gBACvE,eAAe,EAAE,CAAC;gBAClB,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAC5B,CAAC,CACF,CAAC;YACF,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACzB,CAAC,CAAC;QAEF,OAAO,EAAE,CAAC;QAEV,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvF,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,OAAO,IAAI,KAAK,OAAO,GAAG,CAAC,CAAC;IAC9D,CAAC,CAAC;IAEF,2DAA2D;IAE3D,MAAM,gBAAgB,GAAG,GAAG,EAAE;QAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,MAAM,CAAC,QAAQ,CAAC,CAAC;QACxE,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;YACrB,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAsB,EAAE,CAAC,CAAC;YACxD,IAAI,CAAC,GAAG;gBAAE,SAAS;YACnB,MAAM,YAAY,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;YACxC,WAAW,CAAC,EAAE,EAAE,YAAY,EAAE,EAAE,CAAC,CAAC;QACpC,CAAC;QACD,OAAO,GAAG,CAAC,MAAM,CAAC;IACpB,CAAC,CAAC;IAEF,2DAA2D;IAE3D,IAAI,cAAc,GAA0C,IAAI,CAAC;IAEjE,MAAM,YAAY,GAAG,KAAK,IAAI,EAAE;QAC9B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;YAExC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE;gBAC3D,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE,OAAO,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;gBAChE,IAAI,IAAI;oBAAE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1C,CAAC,CAAC,CAAC;YAEH,UAAU,CAAC,KAAK,IAAI,EAAE;gBACpB,OAAO,CAAC,IAAI,EAAE,CAAC;gBACf,OAAO,CAAC,OAAO,EAAE,CAAC;gBAElB,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC;oBAC3B,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;oBACrC,IAAI,CAAC,IAAI;wBAAE,SAAS;oBAEpB,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;oBAC7D,IAAI,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;wBAAE,SAAS;oBAExC,MAAM,YAAY,GAAuB;wBACvC,IAAI;wBACJ,KAAK,EAAE,IAAI,CAAC,KAAK;wBACjB,GAAG,EAAE,IAAI,CAAC,GAAG;wBACb,UAAU,EAAE,IAAI,CAAC,UAAU;qBAC5B,CAAC;oBAEF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;oBACrC,WAAW,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;oBAEzC,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBAC9D,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,IAAI,KAAK,EAAE,QAAQ,IAAI,KAAK,OAAO,GAAG,CAAC,CAAC;gBAC9E,CAAC;YACH,CAAC,EAAE,IAAI,CAAC,CAAC;QACX,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,qBAAsB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAChE,CAAC;IACH,CAAC,CAAC;IAEF,4DAA4D;IAE5D,MAAM,UAAU,GAAG,gBAAgB,EAAE,CAAC;IACtC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,2BAA2B,UAAU,kBAAkB,CAAC,CAAC;IACzE,YAAY,EAAE,CAAC;IACf,cAAc,GAAG,WAAW,CAAC,YAAY,EAAE,iBAAiB,CAAC,CAAC;IAE9D,kEAAkE;IAClE,YAAY,CAAC,KAAK,CAAC,0BAA0B,EAAE,CAAC,KAAK,EAAE,EAAE;QACvD,iBAAiB,GAAG,KAAe,CAAC;QACpC,UAAU,EAAE,CAAC;QACb,YAAY,CAAC,GAAG,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAC;QACpD,IAAI,cAAc;YAAE,aAAa,CAAC,cAAc,CAAC,CAAC;QAClD,cAAc,GAAG,WAAW,CAAC,YAAY,EAAE,iBAAiB,CAAC,CAAC;QAC9D,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,iCAAiC,iBAAiB,IAAI,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IAEH,YAAY,CAAC,KAAK,CAAC,sBAAsB,EAAE,CAAC,KAAK,EAAE,EAAE;QACnD,aAAa,GAAG,KAAgB,CAAC;QACjC,UAAU,EAAE,CAAC;QACb,YAAY,CAAC,GAAG,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;QAChD,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,qBAAqB,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,gCAAgC,CAAC,CAAC;IAC/G,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE;QAC3B,KAAK,MAAM,EAAE,IAAI,aAAa;YAAE,EAAE,CAAC,KAAK,EAAE,CAAC;QAC3C,IAAI,cAAc;YAAE,aAAa,CAAC,cAAc,CAAC,CAAC;QAClD,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// Shelly plugin for Tomfy\n// Supports Gen2 devices via auto-detected components.\n//\n// Configs are stored in ~/.tomfy/config/shelly/<object-id>.json\n// New devices are auto-discovered via mDNS and saved automatically.\n// Uses WebSocket for real-time push updates.\n//\n// Components detected from Shelly.GetStatus:\n// switch — on, power*, voltage*, current*, temperature, energy* (* PM models only)\n// input — state property + singlePush/doublePush/longPush events\n// cover — state, position, power*, voltage*, current*, temperature\n// light — on, brightness\n//\n// Object-level properties (all objects):\n// firmware.installedVersion, firmware.latestVersion, firmware.updateAvailable\n// (only when config.trackFirmware is true)\n//\n// Naming convention (same for all component types):\n// Single component: <property> e.g. on, power\n// Multiple: <name>.<property> e.g. output0.on, kitchen.power\n// Component names use camelCased Shelly config name when available,\n// otherwise fall back to output0/input0/cover0/light0.\n//\n// Plugin config is exposed as properties on an object named \"shelly\":\n// tomfy get shelly.config — view all config\n// tomfy set shelly.config.discoveryInterval 120000 — update config\n// tomfy set shelly.config.trackFirmware true — enable firmware tracking\n\nimport type { PluginAPI } from \"@tomfy/core\";\nimport { toCamelCase } from \"@tomfy/core\";\nimport { Bonjour } from \"bonjour-service\";\n\n// ── Types ───────────────────────────────────────────────────\n\ntype ShellyComponentType = \"switch\" | \"input\" | \"cover\" | \"light\";\n\ninterface ShellyComponentConfig {\n type: ShellyComponentType;\n id: number;\n name: string | null;\n metering?: boolean; // switch and cover: whether power metering is available\n}\n\ninterface ShellyDeviceConfig {\n host: string;\n model?: string;\n mac?: string;\n components: ShellyComponentConfig[];\n}\n\ninterface ShellyInputEvent {\n component: string; // \"input:0\"\n id: number;\n event: string; // \"single_push\", \"double_push\", \"long_push\"\n ts: number;\n}\n\n// ── Legacy config migration ─────────────────────────────────\n\ninterface LegacyDeviceConfig {\n host: string;\n model?: string;\n mac?: string;\n outputs?: number;\n outputNames?: (string | null)[];\n inputs?: number;\n inputNames?: (string | null)[];\n}\n\nconst migrateConfig = (raw: Record<string, any>): ShellyDeviceConfig => {\n if (raw.components) return raw as ShellyDeviceConfig;\n\n // Convert old outputs/inputs format to components array\n const legacy = raw as LegacyDeviceConfig;\n const components: ShellyComponentConfig[] = [];\n\n const outputs = legacy.outputs ?? 0;\n for (let i = 0; i < outputs; i++) {\n components.push({\n type: \"switch\",\n id: i,\n name: legacy.outputNames?.[i] ?? null,\n metering: true, // assume PM for old configs; re-probe will correct\n });\n }\n\n const inputs = legacy.inputs ?? 0;\n for (let i = 0; i < inputs; i++) {\n components.push({\n type: \"input\",\n id: i,\n name: legacy.inputNames?.[i] ?? null,\n });\n }\n\n return { host: legacy.host, model: legacy.model, mac: legacy.mac, components };\n};\n\n// ── HTTP API ────────────────────────────────────────────────\n\nconst rpc = async (host: string, method: string, params: Record<string, unknown> = {}): Promise<void> => {\n const res = await fetch(`http://${host}/rpc/${method}`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(params),\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n};\n\n// ── WebSocket connection ────────────────────────────────────\n\ninterface WsConnection {\n close(): void;\n}\n\nconst COMPONENT_RE = /^(switch|input|cover|light):(\\d+)$/;\n\nconst connectWs = (\n host: string,\n onComponentStatus: (type: string, id: number, data: Record<string, any>) => void,\n onSysStatus: (data: Record<string, any>) => void,\n onDeviceInfo: (data: Record<string, any>) => void,\n onInputEvent: (event: ShellyInputEvent) => void,\n onError: (err: Error) => void,\n onClose: () => void,\n): WsConnection => {\n const ws = new WebSocket(`ws://${host}/rpc`);\n let rpcId = 1;\n let alive = true;\n\n const extractComponents = (obj: Record<string, any>) => {\n for (const [key, data] of Object.entries(obj)) {\n if (key === \"sys\" && data && typeof data === \"object\") {\n onSysStatus(data as Record<string, any>);\n continue;\n }\n const m = COMPONENT_RE.exec(key);\n if (m && data && typeof data === \"object\") {\n onComponentStatus(m[1], parseInt(m[2], 10), data as Record<string, any>);\n }\n }\n };\n\n ws.addEventListener(\"open\", () => {\n ws.send(JSON.stringify({ jsonrpc: \"2.0\", id: rpcId++, src: \"tomfy\", method: \"Shelly.GetStatus\" }));\n ws.send(JSON.stringify({ jsonrpc: \"2.0\", id: rpcId++, src: \"tomfy\", method: \"Shelly.GetDeviceInfo\" }));\n });\n\n ws.addEventListener(\"message\", (event: MessageEvent) => {\n try {\n const msg = JSON.parse(String(event.data)) as Record<string, any>;\n\n if (msg.result) {\n // Distinguish GetDeviceInfo (has \"ver\") from GetStatus (has component keys)\n if (\"ver\" in msg.result) {\n onDeviceInfo(msg.result);\n } else {\n extractComponents(msg.result);\n }\n }\n if (msg.method === \"NotifyStatus\" && msg.params) extractComponents(msg.params);\n if (msg.method === \"NotifyFullStatus\" && msg.params) extractComponents(msg.params);\n if (msg.method === \"NotifyEvent\" && msg.params?.events) {\n for (const evt of msg.params.events as ShellyInputEvent[]) {\n if (evt.component?.startsWith(\"input:\")) onInputEvent(evt);\n }\n }\n } catch (e) {\n onError(e as Error);\n }\n });\n\n ws.addEventListener(\"error\", () => onError(new Error(`WebSocket error for ${host}`)));\n ws.addEventListener(\"close\", () => {\n if (alive) onClose();\n });\n\n return {\n close() {\n alive = false;\n ws.close();\n },\n };\n};\n\n// ── Probe device via HTTP ───────────────────────────────────\n\ninterface ProbeResult {\n id: string;\n name: string;\n model: string;\n mac: string;\n gen: number;\n components: ShellyComponentConfig[];\n}\n\nconst probeShelly = async (host: string): Promise<ProbeResult | null> => {\n try {\n const res = await fetch(`http://${host}/shelly`, { signal: AbortSignal.timeout(3000) });\n if (!res.ok) return null;\n const data = (await res.json()) as Record<string, any>;\n const gen = data.gen ?? 1;\n if (gen < 2) return null;\n\n // Detect components from Shelly.GetStatus\n let status: Record<string, any> = {};\n try {\n const statusRes = await fetch(`http://${host}/rpc/Shelly.GetStatus`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ id: 1 }),\n signal: AbortSignal.timeout(3000),\n });\n if (statusRes.ok) status = (await statusRes.json()) as Record<string, any>;\n } catch {\n /* empty status */\n }\n\n // Fetch config for component names\n let cfg: Record<string, any> = {};\n try {\n const configRes = await fetch(`http://${host}/rpc/Shelly.GetConfig`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ id: 1 }),\n signal: AbortSignal.timeout(3000),\n });\n if (configRes.ok) cfg = (await configRes.json()) as Record<string, any>;\n } catch {\n /* no config */\n }\n\n const components: ShellyComponentConfig[] = [];\n for (const key of Object.keys(status)) {\n const m = COMPONENT_RE.exec(key);\n if (!m) continue;\n\n const type = m[1] as ShellyComponentType;\n const id = parseInt(m[2], 10);\n const compStatus = status[key] as Record<string, any> | undefined;\n const compCfg = cfg[key] as Record<string, any> | undefined;\n\n const comp: ShellyComponentConfig = {\n type,\n id,\n name: compCfg?.name || null,\n };\n\n // Detect power metering for switch and cover\n if ((type === \"switch\" || type === \"cover\") && compStatus && \"apower\" in compStatus) {\n comp.metering = true;\n }\n\n components.push(comp);\n }\n\n return {\n id: data.id ?? data.hostname ?? `shelly-${host.replace(/\\./g, \"-\")}`,\n name: data.name || data.app || data.id || host,\n model: data.model ?? \"unknown\",\n mac: data.mac ?? \"\",\n gen,\n components,\n };\n } catch {\n return null;\n }\n};\n\n// ── Plugin ──────────────────────────────────────────────────\n\nconst INPUT_EVENT_NAMES = [\"buttonDown\", \"buttonUp\", \"push\", \"doublePush\", \"longPush\"] as const;\nconst SHELLY_EVENT_MAP: Record<string, string> = {\n btn_down: \"buttonDown\",\n btn_up: \"buttonUp\",\n single_push: \"push\",\n double_push: \"doublePush\",\n long_push: \"longPush\",\n};\n\nconst DEFAULT_PREFIXES: Record<ShellyComponentType, string> = {\n switch: \"output\",\n input: \"input\",\n cover: \"cover\",\n light: \"light\",\n};\n\nexport default function (plugin: PluginAPI) {\n // ── Config object ───────────────────────────────────────\n // Expose plugin config as properties on an object named after the plugin.\n // Persisted in ~/.tomfy/config/shelly/shelly.json alongside hardware objects.\n\n const savedConfig = plugin.config.load<Record<string, unknown>>(plugin.pluginId);\n let discoveryInterval = typeof savedConfig?.discoveryInterval === \"number\" ? savedConfig.discoveryInterval : 60000;\n let trackFirmware = typeof savedConfig?.trackFirmware === \"boolean\" ? savedConfig.trackFirmware : false;\n\n const configObject = plugin.registerObject(plugin.pluginId, { name: plugin.pluginId });\n configObject.defineProperty(\"config.discoveryInterval\", {\n type: \"integer\",\n writable: true,\n value: discoveryInterval,\n });\n configObject.defineProperty(\"config.trackFirmware\", {\n type: \"boolean\",\n writable: true,\n value: trackFirmware,\n });\n\n const saveConfig = () => {\n const existing = plugin.config.load<Record<string, unknown>>(plugin.pluginId) ?? {};\n plugin.config.save(plugin.pluginId, { ...existing, discoveryInterval, trackFirmware });\n };\n\n const wsConnections: WsConnection[] = [];\n const registeredObjects = new Set<string>();\n\n // ── Register an object ──────────────────────────────────\n\n const setupObject = (id: string, deviceConfig: ShellyDeviceConfig, displayName: string) => {\n if (registeredObjects.has(id)) return;\n registeredObjects.add(id);\n\n const { host, components } = deviceConfig;\n const obj = plugin.registerObject(id, { name: displayName });\n\n // Group components by type for single/multi naming\n const byType = new Map<string, ShellyComponentConfig[]>();\n for (const comp of components) {\n const list = byType.get(comp.type) ?? [];\n list.push(comp);\n byType.set(comp.type, list);\n }\n\n // Naming: single component of a type → flat, multiple → prefixed\n const prefix = (comp: ShellyComponentConfig): string => {\n return comp.name ? toCamelCase(comp.name) : `${DEFAULT_PREFIXES[comp.type]}${comp.id}`;\n };\n const p = (comp: ShellyComponentConfig, key: string): string => {\n const siblings = byType.get(comp.type)!;\n return siblings.length === 1 ? key : `${prefix(comp)}.${key}`;\n };\n\n // Track all property names for reset on disconnect\n const allPropertyNames: string[] = [];\n const define = (comp: ShellyComponentConfig, key: string, def: Parameters<typeof obj.defineProperty>[1]) => {\n const name = p(comp, key);\n obj.defineProperty(name, def);\n allPropertyNames.push(name);\n };\n\n // Build a lookup from \"type:id\" to component config for WS dispatch\n const compLookup = new Map<string, ShellyComponentConfig>();\n for (const comp of components) compLookup.set(`${comp.type}:${comp.id}`, comp);\n\n // ── Switch properties ─────────────────────────────────\n\n for (const comp of byType.get(\"switch\") ?? []) {\n define(comp, \"on\", { type: \"boolean\", writable: true });\n if (comp.metering) {\n define(comp, \"power\", { type: \"float\", writable: false });\n define(comp, \"voltage\", { type: \"float\", writable: false });\n define(comp, \"current\", { type: \"float\", writable: false });\n define(comp, \"energy\", { type: \"float\", writable: false });\n }\n define(comp, \"temperature\", { type: \"float\", writable: false });\n\n obj.onSet(p(comp, \"on\"), async (value) => {\n try {\n await rpc(host, \"Switch.Set\", { id: comp.id, on: value });\n obj.set(p(comp, \"on\"), value as boolean);\n plugin.log.info(`${id} ${prefix(comp)} → ${value ? \"ON\" : \"OFF\"}`);\n } catch (e) {\n plugin.log.error(`Failed to set ${id} switch: ${(e as Error).message}`);\n }\n });\n }\n\n // ── Input properties + events ─────────────────────────\n\n const inputs = byType.get(\"input\") ?? [];\n for (const comp of inputs) {\n define(comp, \"state\", { type: \"boolean\", writable: false });\n\n for (const name of INPUT_EVENT_NAMES) {\n obj.defineEvent(p(comp, name));\n }\n }\n\n // ── Cover properties ──────────────────────────────────\n\n for (const comp of byType.get(\"cover\") ?? []) {\n define(comp, \"state\", { type: \"string\", writable: false });\n define(comp, \"position\", { type: \"integer\", writable: true });\n if (comp.metering) {\n define(comp, \"power\", { type: \"float\", writable: false });\n define(comp, \"voltage\", { type: \"float\", writable: false });\n define(comp, \"current\", { type: \"float\", writable: false });\n }\n define(comp, \"temperature\", { type: \"float\", writable: false });\n\n obj.onSet(p(comp, \"position\"), async (value) => {\n try {\n await rpc(host, \"Cover.GoToPosition\", { id: comp.id, pos: value });\n plugin.log.info(`${id} ${prefix(comp)} → position ${value}`);\n } catch (e) {\n plugin.log.error(`Failed to set ${id} cover position: ${(e as Error).message}`);\n }\n });\n }\n\n // ── Light properties ──────────────────────────────────\n\n for (const comp of byType.get(\"light\") ?? []) {\n define(comp, \"on\", { type: \"boolean\", writable: true });\n define(comp, \"brightness\", { type: \"integer\", writable: true });\n\n obj.onSet(p(comp, \"on\"), async (value) => {\n try {\n await rpc(host, \"Light.Set\", { id: comp.id, on: value });\n obj.set(p(comp, \"on\"), value as boolean);\n plugin.log.info(`${id} ${prefix(comp)} → ${value ? \"ON\" : \"OFF\"}`);\n } catch (e) {\n plugin.log.error(`Failed to set ${id} light: ${(e as Error).message}`);\n }\n });\n\n obj.onSet(p(comp, \"brightness\"), async (value) => {\n try {\n await rpc(host, \"Light.Set\", { id: comp.id, brightness: value });\n obj.set(p(comp, \"brightness\"), value as number);\n plugin.log.info(`${id} ${prefix(comp)} → brightness ${value}`);\n } catch (e) {\n plugin.log.error(`Failed to set ${id} brightness: ${(e as Error).message}`);\n }\n });\n }\n\n // ── Firmware properties ──────────────────────────────────\n\n if (trackFirmware) {\n obj.defineProperty(\"firmware.installedVersion\", { type: \"string\", writable: false });\n obj.defineProperty(\"firmware.latestVersion\", { type: \"string\", writable: false });\n obj.defineProperty(\"firmware.updateAvailable\", { type: \"boolean\", writable: false });\n allPropertyNames.push(\"firmware.installedVersion\", \"firmware.latestVersion\", \"firmware.updateAvailable\");\n }\n\n // ── Availability ──────────────────────────────────────\n\n // Firmware state tracked across two RPC responses (GetDeviceInfo + GetStatus sys)\n let fwInstalled: string | null = null;\n let fwChecked = false;\n let fwStableVersion: string | null = null;\n\n const updateFirmwareProps = () => {\n if (!trackFirmware) return;\n obj.set(\"firmware.installedVersion\", fwInstalled);\n if (fwChecked) {\n obj.set(\"firmware.latestVersion\", fwStableVersion ?? fwInstalled);\n obj.set(\"firmware.updateAvailable\", fwStableVersion !== null);\n }\n };\n\n const resetProperties = () => {\n for (const name of allPropertyNames) obj.set(name, null);\n fwInstalled = null;\n fwChecked = false;\n fwStableVersion = null;\n };\n\n // ── WebSocket ─────────────────────────────────────────\n\n const connect = () => {\n const ws = connectWs(\n host,\n (type, compId, data) => {\n const comp = compLookup.get(`${type}:${compId}`);\n if (!comp) return;\n\n switch (type) {\n case \"switch\": {\n if (\"output\" in data) obj.set(p(comp, \"on\"), data.output);\n if (comp.metering) {\n if (\"apower\" in data) obj.set(p(comp, \"power\"), data.apower);\n if (\"voltage\" in data) obj.set(p(comp, \"voltage\"), data.voltage);\n if (\"current\" in data) obj.set(p(comp, \"current\"), data.current);\n if (data.aenergy && \"total\" in data.aenergy) obj.set(p(comp, \"energy\"), data.aenergy.total);\n }\n if (data.temperature && \"tC\" in data.temperature) obj.set(p(comp, \"temperature\"), data.temperature.tC);\n break;\n }\n case \"input\": {\n if (\"state\" in data) obj.set(p(comp, \"state\"), data.state);\n break;\n }\n case \"cover\": {\n if (\"state\" in data) obj.set(p(comp, \"state\"), data.state);\n if (\"current_pos\" in data && data.current_pos !== null) obj.set(p(comp, \"position\"), data.current_pos);\n if (comp.metering) {\n if (\"apower\" in data) obj.set(p(comp, \"power\"), data.apower);\n if (\"voltage\" in data) obj.set(p(comp, \"voltage\"), data.voltage);\n if (\"current\" in data) obj.set(p(comp, \"current\"), data.current);\n }\n if (data.temperature && \"tC\" in data.temperature) obj.set(p(comp, \"temperature\"), data.temperature.tC);\n break;\n }\n case \"light\": {\n if (\"output\" in data) obj.set(p(comp, \"on\"), data.output);\n if (\"brightness\" in data) obj.set(p(comp, \"brightness\"), data.brightness);\n break;\n }\n }\n },\n (sysData) => {\n const stable = sysData.available_updates?.stable;\n fwChecked = true;\n fwStableVersion = stable?.version ?? null;\n updateFirmwareProps();\n },\n (info) => {\n if (info.ver) {\n fwInstalled = info.ver;\n updateFirmwareProps();\n }\n },\n (inputEvent) => {\n const comp = compLookup.get(`input:${inputEvent.id}`);\n if (!comp) return;\n const name = SHELLY_EVENT_MAP[inputEvent.event];\n if (name) obj.emitEvent(p(comp, name));\n },\n (err) => plugin.log.error(`WS ${id} (${host}): ${err.message}`),\n () => {\n plugin.log.warn(`WS ${id} (${host}) disconnected, reconnecting in 5s`);\n resetProperties();\n setTimeout(connect, 5000);\n },\n );\n wsConnections.push(ws);\n };\n\n connect();\n\n const summary = [...byType.entries()].map(([t, cs]) => `${cs.length} ${t}`).join(\", \");\n plugin.log.info(`Registered ${id} at ${host} [${summary}]`);\n };\n\n // ── Load known objects from disk ────────────────────────\n\n const loadKnownObjects = () => {\n const ids = plugin.config.list().filter((id) => id !== plugin.pluginId);\n for (const id of ids) {\n const raw = plugin.config.load<Record<string, any>>(id);\n if (!raw) continue;\n const deviceConfig = migrateConfig(raw);\n setupObject(id, deviceConfig, id);\n }\n return ids.length;\n };\n\n // ── mDNS discovery ──────────────────────────────────────\n\n let discoveryTimer: ReturnType<typeof setInterval> | null = null;\n\n const runDiscovery = async () => {\n try {\n const bonjour = new Bonjour();\n const found = new Map<string, string>();\n\n const browser = bonjour.find({ type: \"shelly\" }, (service) => {\n const host = service.referer?.address ?? service.addresses?.[0];\n if (host) found.set(host, service.name);\n });\n\n setTimeout(async () => {\n browser.stop();\n bonjour.destroy();\n\n for (const [host] of found) {\n const info = await probeShelly(host);\n if (!info) continue;\n\n const id = info.id.toLowerCase().replace(/[^a-z0-9-]/g, \"-\");\n if (registeredObjects.has(id)) continue;\n\n const deviceConfig: ShellyDeviceConfig = {\n host,\n model: info.model,\n mac: info.mac,\n components: info.components,\n };\n\n plugin.config.save(id, deviceConfig);\n setupObject(id, deviceConfig, info.name);\n\n const summary = info.components.map((c) => c.type).join(\", \");\n plugin.log.info(`Discovered: ${info.name} (${id}) at ${host} [${summary}]`);\n }\n }, 5000);\n } catch (e) {\n plugin.log.error(`Discovery failed: ${(e as Error).message}`);\n }\n };\n\n // ── Start ────────────────────────────────────────────────\n\n const knownCount = loadKnownObjects();\n plugin.log.info(`Shelly plugin started — ${knownCount} known object(s)`);\n runDiscovery();\n discoveryTimer = setInterval(runDiscovery, discoveryInterval);\n\n // Wire up config change handlers after discovery timer is created\n configObject.onSet(\"config.discoveryInterval\", (value) => {\n discoveryInterval = value as number;\n saveConfig();\n configObject.set(\"config.discoveryInterval\", value);\n if (discoveryTimer) clearInterval(discoveryTimer);\n discoveryTimer = setInterval(runDiscovery, discoveryInterval);\n plugin.log.info(`Discovery interval changed to ${discoveryInterval}ms`);\n });\n\n configObject.onSet(\"config.trackFirmware\", (value) => {\n trackFirmware = value as boolean;\n saveConfig();\n configObject.set(\"config.trackFirmware\", value);\n plugin.log.info(`Firmware tracking ${trackFirmware ? \"enabled\" : \"disabled\"} (takes effect on next reload)`);\n });\n\n plugin.on(\"stop\", async () => {\n for (const ws of wsConnections) ws.close();\n if (discoveryTimer) clearInterval(discoveryTimer);\n plugin.log.info(\"Shelly plugin stopped\");\n });\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tomfy/shelly",
|
|
3
|
+
"version": "2026.3.4",
|
|
4
|
+
"description": "Shelly Gen2 plugin for Tomfy",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Kaspars Dancis",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"CHANGELOG.md"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"clean": "shx rm -rf dist",
|
|
16
|
+
"build": "tsgo -p tsconfig.build.json",
|
|
17
|
+
"dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@tomfy/core": "^2026.3.4",
|
|
21
|
+
"bonjour-service": "^1.3.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.13.5",
|
|
25
|
+
"@typescript/native-preview": "^7.0.0-dev.20260303.1"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22"
|
|
29
|
+
}
|
|
30
|
+
}
|