@shepherdjerred/home-assistant 0.1.0-dev.14474

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +674 -0
  3. package/README.md +126 -0
  4. package/demos/codegen-fixture.cast +40 -0
  5. package/demos/home-assistant-fixture.json +33 -0
  6. package/demos/record-codegen-demo.sh +119 -0
  7. package/dist/codegen/cli.d.ts +3 -0
  8. package/dist/codegen/cli.d.ts.map +1 -0
  9. package/dist/codegen/cli.js +118 -0
  10. package/dist/codegen/cli.js.map +1 -0
  11. package/dist/codegen/emit.d.ts +8 -0
  12. package/dist/codegen/emit.d.ts.map +1 -0
  13. package/dist/codegen/emit.js +143 -0
  14. package/dist/codegen/emit.js.map +1 -0
  15. package/dist/codegen/introspect.d.ts +193 -0
  16. package/dist/codegen/introspect.d.ts.map +1 -0
  17. package/dist/codegen/introspect.js +131 -0
  18. package/dist/codegen/introspect.js.map +1 -0
  19. package/dist/codegen/known-events.d.ts +10 -0
  20. package/dist/codegen/known-events.d.ts.map +1 -0
  21. package/dist/codegen/known-events.js +25 -0
  22. package/dist/codegen/known-events.js.map +1 -0
  23. package/dist/codegen/selector-to-ts.d.ts +15 -0
  24. package/dist/codegen/selector-to-ts.d.ts.map +1 -0
  25. package/dist/codegen/selector-to-ts.js +110 -0
  26. package/dist/codegen/selector-to-ts.js.map +1 -0
  27. package/dist/index.d.ts +14 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +7 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/rest/client.d.ts +34 -0
  32. package/dist/rest/client.d.ts.map +1 -0
  33. package/dist/rest/client.js +112 -0
  34. package/dist/rest/client.js.map +1 -0
  35. package/dist/rest/errors.d.ts +12 -0
  36. package/dist/rest/errors.d.ts.map +1 -0
  37. package/dist/rest/errors.js +23 -0
  38. package/dist/rest/errors.js.map +1 -0
  39. package/dist/rest/schemas.d.ts +124 -0
  40. package/dist/rest/schemas.d.ts.map +1 -0
  41. package/dist/rest/schemas.js +64 -0
  42. package/dist/rest/schemas.js.map +1 -0
  43. package/dist/schema/types.d.ts +138 -0
  44. package/dist/schema/types.d.ts.map +1 -0
  45. package/dist/schema/types.js +12 -0
  46. package/dist/schema/types.js.map +1 -0
  47. package/dist/shared/config.d.ts +6 -0
  48. package/dist/shared/config.d.ts.map +1 -0
  49. package/dist/shared/config.js +4 -0
  50. package/dist/shared/config.js.map +1 -0
  51. package/dist/ws/client.d.ts +58 -0
  52. package/dist/ws/client.d.ts.map +1 -0
  53. package/dist/ws/client.js +343 -0
  54. package/dist/ws/client.js.map +1 -0
  55. package/dist/ws/errors.d.ts +14 -0
  56. package/dist/ws/errors.d.ts.map +1 -0
  57. package/dist/ws/errors.js +27 -0
  58. package/dist/ws/errors.js.map +1 -0
  59. package/dist/ws/messages.d.ts +130 -0
  60. package/dist/ws/messages.d.ts.map +1 -0
  61. package/dist/ws/messages.js +79 -0
  62. package/dist/ws/messages.js.map +1 -0
  63. package/dist/ws/socket-helpers.d.ts +23 -0
  64. package/dist/ws/socket-helpers.d.ts.map +1 -0
  65. package/dist/ws/socket-helpers.js +73 -0
  66. package/dist/ws/socket-helpers.js.map +1 -0
  67. package/dist/ws/subscriptions.d.ts +37 -0
  68. package/dist/ws/subscriptions.d.ts.map +1 -0
  69. package/dist/ws/subscriptions.js +58 -0
  70. package/dist/ws/subscriptions.js.map +1 -0
  71. package/examples/basic-client.mjs +18 -0
  72. package/examples/codegen-fixture.mjs +95 -0
  73. package/package.json +69 -0
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # @shepherdjerred/home-assistant
2
+
3
+ Type-safe access to the [Home Assistant](https://www.home-assistant.io/) REST
4
+ and WebSocket APIs for Node.js and Bun. It validates wire data with Zod and can
5
+ generate a schema for one Home Assistant instance, so entity IDs, services, and
6
+ event types are checked before deployment.
7
+
8
+ Requires Node.js 24 or newer, or Bun.
9
+
10
+ ## Why use this?
11
+
12
+ Home Assistant integrations tend to put entity IDs, service names, and payloads
13
+ in string literals. Those mistakes otherwise appear only when an automation
14
+ reaches a real home. This package validates API responses at runtime and can
15
+ generate a schema from one instance, moving those mistakes into the editor and
16
+ TypeScript before deployment.
17
+
18
+ Use it when you are building:
19
+
20
+ - A Node.js service that reads states or calls services with explicit error
21
+ types for API, authentication, and WebSocket failures.
22
+ - A typed automation codebase where `light.kitchen`, `light.turn_on`, and its
23
+ payload must match the Home Assistant instance it will control.
24
+ - A real-time integration that subscribes to Home Assistant events over the
25
+ WebSocket API instead of polling REST endpoints.
26
+
27
+ ## Quick start
28
+
29
+ Install the package and provide a Home Assistant long-lived access token through
30
+ your environment:
31
+
32
+ ```bash
33
+ bun add @shepherdjerred/home-assistant
34
+ ```
35
+
36
+ ```ts
37
+ import { HomeAssistantRestClient } from "@shepherdjerred/home-assistant";
38
+
39
+ const { HA_TOKEN: token, HA_URL: baseUrl } = process.env;
40
+ if (baseUrl === undefined || token === undefined) {
41
+ throw new Error("Set HA_URL and HA_TOKEN before running this program.");
42
+ }
43
+
44
+ const client = new HomeAssistantRestClient({
45
+ baseUrl,
46
+ token,
47
+ });
48
+
49
+ const state = await client.getState("light.kitchen");
50
+ console.log(state.state);
51
+
52
+ await client.callService("light", "turn_on", {
53
+ entity_id: "light.kitchen",
54
+ brightness: 200,
55
+ });
56
+ ```
57
+
58
+ The runnable [`examples/basic-client.mjs`](examples/basic-client.mjs) requires
59
+ `HA_URL`, `HA_TOKEN`, and `HA_ENTITY_ID`; it never prints the token.
60
+
61
+ ## Generate an instance schema
62
+
63
+ `ha-codegen` reads Home Assistant's states, services, events, and configuration
64
+ endpoints, then writes a TypeScript module that describes that instance.
65
+
66
+ ```bash
67
+ ha-codegen \
68
+ --url "$HA_URL" \
69
+ --out src/generated/ha-schema.ts \
70
+ --name MyHomeSchema
71
+ ```
72
+
73
+ Do not pass credentials on the command line. The generated module includes
74
+ private entity IDs and service metadata, so add it to `.gitignore` and
75
+ regenerate it when your instance changes.
76
+
77
+ Use the generated schema as the client type parameter:
78
+
79
+ ```ts
80
+ import { HomeAssistantRestClient } from "@shepherdjerred/home-assistant";
81
+ import type { MyHomeSchema } from "./generated/ha-schema.js";
82
+
83
+ const { HA_TOKEN: token, HA_URL: baseUrl } = process.env;
84
+ if (baseUrl === undefined || token === undefined) {
85
+ throw new Error("Set HA_URL and HA_TOKEN before running this program.");
86
+ }
87
+
88
+ const client = new HomeAssistantRestClient<MyHomeSchema>({
89
+ baseUrl,
90
+ token,
91
+ });
92
+ ```
93
+
94
+ ## API reference
95
+
96
+ - `HomeAssistantRestClient` reads state and calls REST services.
97
+ - `HomeAssistantEventClient` connects to the WebSocket API and supports event
98
+ subscriptions, triggers, service calls, and state reads.
99
+ - `HaApiError`, `HaAuthError`, and `HaWebSocketError` distinguish API,
100
+ authentication, and socket failures.
101
+ - `ha-codegen --help` lists the command's supported options.
102
+
103
+ Both clients use the runtime's standard `fetch` and `WebSocket` implementations;
104
+ there is no Bun-only runtime dependency.
105
+
106
+ ## Demo recording
107
+
108
+ The package includes a replayable asciicast generated from a synthetic local
109
+ Home Assistant API fixture—no live instance data or credentials are recorded.
110
+ It walks from the API data through the generated module to the compile-time
111
+ checks that catch an invalid entity, domain, service, or payload:
112
+
113
+ ```bash
114
+ asciinema play node_modules/@shepherdjerred/home-assistant/demos/codegen-fixture.cast
115
+ ```
116
+
117
+ ## Security and compatibility
118
+
119
+ Use a dedicated long-lived token with only the access your integration needs.
120
+ Keep `HA_TOKEN` in your environment or secret manager, never in shell history,
121
+ source code, or a generated schema. The package targets Node.js 24+ and Bun;
122
+ earlier Node versions are not supported.
123
+
124
+ ## License
125
+
126
+ GPL-3.0-only.
@@ -0,0 +1,40 @@
1
+ {"version":3,"term":{"cols":100,"rows":28},"timestamp":1788642698,"idle_time_limit":12.0,"command":"./demos/record-codegen-demo.sh","title":"Home Assistant: API data to compiler-checked client","env":{"SHELL":"/opt/homebrew/bin/fish"}}
2
+ [0.016, "o", "\u001b[2J\u001b[H\u001b[1;36m━━━ Home Assistant -> a typed client ━━━\u001b[0m\r\n\r\n"]
3
+ [2.010, "o", "\u001b[2J\u001b[H\u001b[1;36m━━━ 1/4 Start with runtime API data ━━━\u001b[0m\r\nEntity IDs, service fields, and events begin as JSON from Home Assistant.\r\n"]
4
+ [5.018, "o", "\u001b[2m$ jq -C <selected fields> demos/home-assistant-fixture.json\u001b[0m\r\n\u001b[0;34m{\u001b[0m\r\n \u001b[1;35m\"entity_id\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32m\"light.demo_lamp\"\u001b[0m\u001b[0;34m,\u001b[0m\r\n \u001b[1;35m\"state\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32m\"off\"\u001b[0m\u001b[0;34m,\u001b[0m\r\n \u001b[1;35m\"service\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32m\"light.turn_on\"\u001b[0m\u001b[0;34m,\u001b[0m\r\n \u001b[1;35m\"field\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m{\u001b[0m\r\n \u001b[1;35m\"brightness\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32m\"number (optional)\"\u001b[0m\r\n \u001b[0;34m}\u001b[0m\u001b[0;34m,\u001b[0m\r\n \u001b[1;35m\"event\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32m\"state_changed\"\u001b[0m\r\n\u001b[0;34m}\u001b[0m\r\n"]
5
+ [4.008, "o", "\u001b[2J\u001b[H\u001b[1;36m━━━ 2/4 Generate the instance schema ━━━\u001b[0m\r\nha-codegen reads that API and emits a normal TypeScript module.\r\n"]
6
+ [4.008, "o", "\u001b[2m$ node examples/codegen-fixture.mjs | bat --language ts\u001b[0m\r\n"]
7
+ [0.161, "o", "\u001b[38;2;136;57;239mexport\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mconst\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mentities\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;23;146;153m=\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\r\n"]
8
+ [0.000, "o", "\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43mlight.demo_lamp\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mdomain\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43mlight\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;124;127;147m,\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mattributes\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43mfriendly_name\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m}\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m}\u001b[0m\u001b[38;2;124;127;147m,\u001b[0m\r\n"]
9
+ [0.001, "o", "\u001b[38;2;124;127;147m}\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mas\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mconst\u001b[0m\u001b[38;2;124;127;147m;\u001b[0m\r\n\r\n"]
10
+ [0.001, "o", "\u001b[38;2;136;57;239mexport\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mconst\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mservices\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;23;146;153m=\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\r\n"]
11
+ [0.000, "o", "\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43mlight\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\r\n"]
12
+ [0.000, "o", "\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43mturn_on\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mfields\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\r\n"]
13
+ [0.000, "o", "\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43mbrightness\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mtype\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43mnumber\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;124;127;147m,\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mrequired\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;254;100;11mfalse\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m}\u001b[0m\r\n"]
14
+ [0.000, "o", "\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m}\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m}\u001b[0m\u001b[38;2;124;127;147m,\u001b[0m\r\n\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m}\u001b[0m\u001b[38;2;124;127;147m,\u001b[0m\r\n"]
15
+ [0.000, "o", "\u001b[38;2;124;127;147m}\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mas\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mconst\u001b[0m\u001b[38;2;124;127;147m;\u001b[0m\r\n"]
16
+ [0.000, "o", "\r\n"]
17
+ [0.000, "o", "\u001b[38;2;136;57;239mexport\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mconst\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mevents\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;23;146;153m=\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105m[\u001b[0m\r\n"]
18
+ [0.000, "o", "\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43mstate_changed\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;124;127;147m,\u001b[0m\r\n"]
19
+ [0.000, "o", "\u001b[38;2;76;79;105m]\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mas\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mconst\u001b[0m\u001b[38;2;124;127;147m;\u001b[0m\r\n\r\n"]
20
+ [0.001, "o", "\u001b[38;2;136;57;239mexport\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;136;57;239mtype\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;223;142;29mEventData\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;23;146;153m=\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\r\n"]
21
+ [0.001, "o", "\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43mstate_changed\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;76;79;105mentity_id\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;136;57;239mstring\u001b[0m\u001b[38;2;124;127;147m;\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;76;79;105mold_state\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;136;57;239munknown\u001b[0m\u001b[38;2;124;127;147m;\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;76;79;105mnew_state\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;136;57;239munknown\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;124;127;147m}\u001b[0m\u001b[38;2;124;127;147m;\u001b[0m\r\n"]
22
+ [0.000, "o", "\u001b[38;2;124;127;147m}\u001b[0m\u001b[38;2;124;127;147m;\u001b[0m\r\n"]
23
+ [12.012, "o", "\u001b[2J\u001b[H\u001b[1;36m━━━ 3/4 See the compiler reject a mistyped entity ━━━\u001b[0m\r\nThis temporary client imports the emitted HaSchema and deliberately misspells the entity ID.\r\n"]
24
+ [6.141, "o", "\u001b[2m$ bat invalid.ts\u001b[0m\r\n"]
25
+ [0.019, "o", "\u001b[38;2;136;57;239mimport\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mtype\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mHomeAssistantRestClient\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m}\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mfrom\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43m<package>/dist/index.js\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;124;127;147m;\u001b[0m\r\n"]
26
+ [0.000, "o", "\u001b[38;2;136;57;239mimport\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mtype\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m{\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mHaSchema\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;124;127;147m}\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mfrom\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43m./ha-schema.js\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;124;127;147m;\u001b[0m\r\n"]
27
+ [0.000, "o", "\r\n"]
28
+ [0.002, "o", "\u001b[38;2;136;57;239mdeclare\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mconst\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mclient\u001b[0m\u001b[38;2;23;146;153m:\u001b[0m\u001b[38;2;223;142;29m \u001b[0m\u001b[38;2;223;142;29mHomeAssistantRestClient\u001b[0m\u001b[38;2;4;165;229m<\u001b[0m\u001b[38;2;223;142;29mHaSchema\u001b[0m\u001b[38;2;4;165;229m>\u001b[0m\u001b[38;2;124;127;147m;\u001b[0m\r\n"]
29
+ [0.000, "o", "\u001b[38;2;76;79;105mclient\u001b[0m\u001b[38;2;23;146;153m.\u001b[0m\u001b[38;2;30;102;245mgetState\u001b[0m\u001b[38;2;76;79;105m(\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;64;160;43mlight.demo_lmp\u001b[0m\u001b[38;2;64;160;43m\"\u001b[0m\u001b[38;2;76;79;105m)\u001b[0m\u001b[38;2;124;127;147m;\u001b[0m\r\n"]
30
+ [5.012, "o", "\u001b[2m$ tsc --noEmit invalid.ts\u001b[0m\r\n"]
31
+ [0.260, "o", "\u001b[38;2;76;79;105minvalid\u001b[0m\u001b[38;2;23;146;153m.\u001b[0m\u001b[38;2;30;102;245mts\u001b[0m\u001b[38;2;76;79;105m(\u001b[0m\u001b[38;2;254;100;11m5\u001b[0m\u001b[38;2;124;127;147m,\u001b[0m\u001b[38;2;254;100;11m17\u001b[0m\u001b[38;2;76;79;105m)\u001b[0m\u001b[38;2;76;79;105m: \u001b[0m\u001b[38;2;76;79;105merror\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;23;146;153mTS2345\u001b[0m\u001b[38;2;124;127;147m:\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mArgument\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mof\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mtype\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m'\u001b[0m\u001b[38;2;64;160;43m\"light.demo_lmp\"\u001b[0m\u001b[38;2;64;160;43m'\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mis\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mnot\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105massignable\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mto\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mparameter\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;136;57;239mof\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;76;79;105mtype\u001b[0m\u001b[38;2;76;79;105m \u001b[0m\u001b[38;2;64;160;43m'\u001b[0m\u001b[38;2;64;160;43m\"light.demo_lamp\"\u001b[0m\u001b[38;2;64;160;43m'\u001b[0m\u001b[38;2;23;146;153m.\u001b[0m\r\n"]
32
+ [4.011, "o", "\u001b[2J\u001b[H\u001b[1;36m━━━ 4/4 Correct the entity before the request runs ━━━\u001b[0m\r\nThe generated schema gives the editor and CI the same precise feedback.\r\n"]
33
+ [5.009, "o", "\u001b[2m$ delta --color-only --paging never\u001b[0m\r\n"]
34
+ [0.016, "o", "diff --git a/client.ts b/client.ts\r\n--- a/client.ts\r\n+++ b/client.ts\r\n"]
35
+ [0.001, "o", "@@ -3,3 +3,3 @@\r\n"]
36
+ [0.007, "o", "\u001b[2m declare const client: HomeAssistantRestClient<HaSchema>;\u001b[0m\r\n"]
37
+ [0.000, "o", "\u001b[1;31m-client.getState(\"light.demo_lmp\");\u001b[0m\r\n\u001b[1;32m+client.getState(\"light.demo_lamp\");\u001b[0m\r\n\u001b[2m \u001b[0m\r\n"]
38
+ [0.000, "o", "\u001b[1;31m-// The typo would become a failing API request.\u001b[0m\r\n\u001b[1;32m+// The typo is corrected before the request can run.\u001b[0m\r\n"]
39
+ [10.013, "o", "\r\n\u001b[1;32m✓ The generated schema catches the typo before runtime.\u001b[0m\r\n"]
40
+ [0.003, "x", "0"]
@@ -0,0 +1,33 @@
1
+ {
2
+ "states": [
3
+ {
4
+ "entity_id": "light.demo_lamp",
5
+ "state": "off",
6
+ "attributes": {
7
+ "friendly_name": "Demo lamp"
8
+ }
9
+ }
10
+ ],
11
+ "services": [
12
+ {
13
+ "domain": "light",
14
+ "services": {
15
+ "turn_on": {
16
+ "fields": {
17
+ "brightness": {
18
+ "required": false,
19
+ "selector": {
20
+ "number": {}
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ],
28
+ "events": [
29
+ {
30
+ "event": "state_changed"
31
+ }
32
+ ]
33
+ }
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ cd "$(dirname "$0")/.."
5
+
6
+ pause_for() {
7
+ word_count="$(printf '%s' "$1" | wc -w | tr -d '[:space:]')"
8
+ seconds=$(((word_count + 3) / 4))
9
+
10
+ if ((seconds < 2)); then
11
+ seconds=2
12
+ elif ((seconds > 12)); then
13
+ seconds=12
14
+ fi
15
+
16
+ sleep "$seconds"
17
+ }
18
+
19
+ section() {
20
+ printf '\033[2J\033[H\033[1;36m━━━ %s ━━━\033[0m\n%s\n' "$1" "$2"
21
+ pause_for "$1 $2"
22
+ }
23
+
24
+ section \
25
+ "Home Assistant -> a typed client" \
26
+ ""
27
+
28
+ section \
29
+ "1/4 Start with runtime API data" \
30
+ "Entity IDs, service fields, and events begin as JSON from Home Assistant."
31
+ fixture_summary="$(jq -C '{
32
+ entity_id: .states[0].entity_id,
33
+ state: .states[0].state,
34
+ service: "light.turn_on",
35
+ field: { brightness: "number (optional)" },
36
+ event: .events[0].event
37
+ }' demos/home-assistant-fixture.json)"
38
+ printf '\033[2m$ jq -C <selected fields> demos/home-assistant-fixture.json\033[0m\n%s\n' "$fixture_summary"
39
+ pause_for "$fixture_summary"
40
+
41
+ section \
42
+ "2/4 Generate the instance schema" \
43
+ "ha-codegen reads that API and emits a normal TypeScript module."
44
+ printf '\033[2m$ node examples/codegen-fixture.mjs | bat --language ts\033[0m\n'
45
+ generated_schema_preview="$(node examples/codegen-fixture.mjs | sed -n '6,25p')"
46
+ printf '%s\n' "$generated_schema_preview" \
47
+ | bat --color=always --language=typescript --paging=never --plain --style=numbers \
48
+ --line-range 1:20
49
+ pause_for "$generated_schema_preview"
50
+
51
+ section \
52
+ "3/4 See the compiler reject a mistyped entity" \
53
+ "This temporary client imports the emitted HaSchema and deliberately misspells the entity ID."
54
+ demo_directory="$(mktemp -d -t home-assistant-demo)"
55
+ trap 'rm -r -- "$demo_directory"' EXIT
56
+ package_directory="$(pwd)"
57
+ generated_schema="$demo_directory/ha-schema.ts"
58
+ invalid_client="$demo_directory/invalid.ts"
59
+ diagnostics="$demo_directory/diagnostics.txt"
60
+ node examples/codegen-fixture.mjs >"$generated_schema"
61
+ printf '%s\n' \
62
+ "import type { HomeAssistantRestClient } from \"${package_directory}/dist/index.js\";" \
63
+ 'import type { HaSchema } from "./ha-schema.js";' \
64
+ '' \
65
+ 'declare const client: HomeAssistantRestClient<HaSchema>;' \
66
+ 'client.getState("light.demo_lmp");' \
67
+ >"$invalid_client"
68
+ printf '\033[2m$ bat invalid.ts\033[0m\n'
69
+ invalid_client_preview="$(sed "s#${package_directory}#<package>#g" "$invalid_client")"
70
+ printf '%s\n' "$invalid_client_preview" \
71
+ | bat --color=always --language=typescript --paging=never --plain --style=numbers
72
+ pause_for "$invalid_client_preview"
73
+ printf '\033[2m$ tsc --noEmit invalid.ts\033[0m\n'
74
+ if (
75
+ cd "$demo_directory"
76
+ PATH="$package_directory/node_modules/@typescript/native/bin:$PATH" tsc \
77
+ --noEmit \
78
+ --ignoreConfig \
79
+ --strict \
80
+ --module NodeNext \
81
+ --moduleResolution NodeNext \
82
+ --target ES2024 \
83
+ invalid.ts
84
+ ) >"$diagnostics" 2>&1; then
85
+ printf '\033[1;31mExpected the invalid entity ID to fail type checking.\033[0m\n' >&2
86
+ exit 1
87
+ fi
88
+ diagnostic_preview="$(<"$diagnostics")"
89
+ printf '%s\n' "$diagnostic_preview" \
90
+ | bat --color=always --language=typescript --paging=never --plain --style=numbers
91
+ pause_for "$diagnostic_preview"
92
+
93
+ section \
94
+ "4/4 Correct the entity before the request runs" \
95
+ "The generated schema gives the editor and CI the same precise feedback."
96
+ printf '\033[2m$ delta --color-only --paging never\033[0m\n'
97
+ diff_preview="$(printf '%s\n' \
98
+ 'diff --git a/client.ts b/client.ts' \
99
+ '--- a/client.ts' \
100
+ '+++ b/client.ts' \
101
+ '@@ -3,3 +3,3 @@' \
102
+ ' declare const client: HomeAssistantRestClient<HaSchema>;' \
103
+ '-client.getState("light.demo_lmp");' \
104
+ '+client.getState("light.demo_lamp");' \
105
+ ' ' \
106
+ '-// The typo would become a failing API request.' \
107
+ '+// The typo is corrected before the request can run.')"
108
+ printf '%s\n' "$diff_preview" \
109
+ | delta \
110
+ --color-only \
111
+ --paging=never \
112
+ --syntax-theme Dracula \
113
+ --minus-style 'red bold' \
114
+ --minus-emph-style 'red bold' \
115
+ --plus-style 'green bold' \
116
+ --plus-emph-style 'green bold' \
117
+ --zero-style dim
118
+ pause_for "$diff_preview"
119
+ printf '\n\033[1;32m✓ The generated schema catches the typo before runtime.\033[0m\n'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/codegen/cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { emitSchemaModule } from "./emit.js";
5
+ import { introspect } from "./introspect.js";
6
+ const HELP_TEXT = `
7
+ ha-codegen - Generate a typed Home Assistant schema from a live instance
8
+
9
+ USAGE:
10
+ HA_TOKEN=<long-lived-access-token> ha-codegen --url <base-url> --out <path> [options]
11
+
12
+ OPTIONS:
13
+ --url <base-url> Home Assistant base URL (defaults to HA_URL)
14
+ --out <path> Generated TypeScript module path (required)
15
+ --name <schema-name> Generated schema type name (default: HaSchema)
16
+ --help, -h Show this help message
17
+
18
+ The generated module contains entity IDs and service definitions from your
19
+ instance. Treat it as private: gitignore it and regenerate it when needed.
20
+ `;
21
+ function parseArgs(argv) {
22
+ const map = new Map();
23
+ let help = false;
24
+ for (let i = 0; i < argv.length; i += 1) {
25
+ const arg = argv[i];
26
+ if (arg === "--help" || arg === "-h") {
27
+ help = true;
28
+ continue;
29
+ }
30
+ const key = parseOptionKey(arg);
31
+ const next = argv[i + 1];
32
+ if (next?.startsWith("--") !== false) {
33
+ map.set(key, "true");
34
+ continue;
35
+ }
36
+ map.set(key, next);
37
+ i += 1;
38
+ }
39
+ const url = map.get("url") ?? environmentValue("HA_URL");
40
+ const token = environmentValue("HA_TOKEN");
41
+ const out = map.get("out");
42
+ const name = map.get("name") ?? "HaSchema";
43
+ if (help) {
44
+ return { url: "", token: "", out: "", name: "HaSchema", help: true };
45
+ }
46
+ if (url === undefined || url === "") {
47
+ fail("Missing --url (or HA_URL env var)");
48
+ }
49
+ if (token === undefined || token === "") {
50
+ fail("Missing HA_TOKEN environment variable");
51
+ }
52
+ if (out === undefined || out === "") {
53
+ fail("Missing --out <path>");
54
+ }
55
+ return { url, token, out, name, help: false };
56
+ }
57
+ function parseOptionKey(arg) {
58
+ if (arg?.startsWith("--") !== true) {
59
+ fail(`Unexpected argument: ${arg ?? ""}`);
60
+ }
61
+ const key = arg.slice(2);
62
+ if (key !== "url" && key !== "out" && key !== "name") {
63
+ fail(`Unknown option: ${arg}`);
64
+ }
65
+ return key;
66
+ }
67
+ function environmentValue(name) {
68
+ return globalThis.process.env[name];
69
+ }
70
+ function fail(message) {
71
+ console.error(`ha-codegen: ${message}`);
72
+ console.error("Usage: ha-codegen --url <base-url> --out <path> [--name <SchemaName>]");
73
+ process.exit(2);
74
+ }
75
+ async function main() {
76
+ const args = parseArgs(process.argv.slice(2));
77
+ if (args.help) {
78
+ process.stdout.write(HELP_TEXT);
79
+ return;
80
+ }
81
+ const intro = await introspect(args.url, args.token);
82
+ const sourceHost = safeHost(args.url);
83
+ const module = emitSchemaModule(intro, {
84
+ schemaName: args.name,
85
+ sourceHost,
86
+ generatedAt: new Date().toISOString(),
87
+ });
88
+ const outPath = path.resolve(args.out);
89
+ await mkdir(path.dirname(outPath), { recursive: true });
90
+ await writeFile(outPath, module, "utf8");
91
+ console.warn(`ha-codegen: wrote ${String(intro.states.length)} entities, ` +
92
+ `${String(totalServices(intro))} services, ` +
93
+ `${String(intro.events.length)} event types → ${outPath}`);
94
+ }
95
+ function totalServices(intro) {
96
+ let n = 0;
97
+ for (const entry of intro.services) {
98
+ n += Object.keys(entry.services).length;
99
+ }
100
+ return n;
101
+ }
102
+ function safeHost(url) {
103
+ try {
104
+ return new URL(url).host;
105
+ }
106
+ catch {
107
+ return "<unparsed>";
108
+ }
109
+ }
110
+ try {
111
+ await main();
112
+ }
113
+ catch (error) {
114
+ const message = error instanceof Error ? error.message : String(error);
115
+ console.error(`ha-codegen: ${message}`);
116
+ process.exit(1);
117
+ }
118
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/codegen/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { HaIntrospection } from "./introspect.js";
2
+ export type EmitOptions = {
3
+ schemaName: string;
4
+ sourceHost: string;
5
+ generatedAt: string;
6
+ };
7
+ export declare function emitSchemaModule(intro: HaIntrospection, options: EmitOptions): string;
8
+ //# sourceMappingURL=emit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/codegen/emit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAQvD,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,WAAW,GACnB,MAAM,CAkBR"}
@@ -0,0 +1,143 @@
1
+ import { KNOWN_EVENT_DATA } from "./known-events.js";
2
+ import { fieldMetaFromSpec, targetDomainFromSpec, } from "./selector-to-ts.js";
3
+ export function emitSchemaModule(intro, options) {
4
+ const entitiesLiteral = emitEntities(intro);
5
+ const servicesLiteral = emitServices(intro);
6
+ const eventsLiteral = emitEvents(intro);
7
+ const eventDataLiteral = emitEventData(intro);
8
+ return [
9
+ header(options),
10
+ entitiesLiteral,
11
+ "",
12
+ servicesLiteral,
13
+ "",
14
+ eventsLiteral,
15
+ "",
16
+ eventDataLiteral,
17
+ "",
18
+ emitSchemaType(options.schemaName),
19
+ "",
20
+ ].join("\n");
21
+ }
22
+ function header(options) {
23
+ return [
24
+ `// AUTO-GENERATED by @shepherdjerred/home-assistant ha-codegen — do not edit, do not commit.`,
25
+ `// Source host: ${options.sourceHost}`,
26
+ `// Generated at: ${options.generatedAt}`,
27
+ `/* eslint-disable */`,
28
+ ``,
29
+ ].join("\n");
30
+ }
31
+ function emitEntities(intro) {
32
+ const entries = [];
33
+ const sorted = intro.states.toSorted((a, b) => a.entity_id.localeCompare(b.entity_id));
34
+ for (const state of sorted) {
35
+ const domain = state.entity_id.includes(".")
36
+ ? state.entity_id.slice(0, state.entity_id.indexOf("."))
37
+ : "";
38
+ const attributes = emitAttributeSample(state.attributes);
39
+ entries.push(` ${JSON.stringify(state.entity_id)}: { domain: ${JSON.stringify(domain)}, attributes: ${attributes} },`);
40
+ }
41
+ return `export const entities = {\n${entries.join("\n")}\n} as const;`;
42
+ }
43
+ function emitAttributeSample(attributes) {
44
+ const props = [];
45
+ const sortedKeys = Object.keys(attributes).toSorted((a, b) => a.localeCompare(b));
46
+ for (const key of sortedKeys) {
47
+ const value = attributes[key];
48
+ const sample = sampleValueLiteral(value);
49
+ if (sample !== undefined) {
50
+ props.push(`${JSON.stringify(key)}: ${sample}`);
51
+ }
52
+ }
53
+ return props.length === 0 ? `{}` : `{ ${props.join(", ")} }`;
54
+ }
55
+ function sampleValueLiteral(value) {
56
+ if (typeof value === "string") {
57
+ return `""`;
58
+ }
59
+ if (typeof value === "number") {
60
+ return `0`;
61
+ }
62
+ if (typeof value === "boolean") {
63
+ return `false`;
64
+ }
65
+ if (value === null) {
66
+ return `null`;
67
+ }
68
+ if (Array.isArray(value)) {
69
+ return `[] as readonly unknown[]`;
70
+ }
71
+ if (typeof value === "object") {
72
+ return `{}`;
73
+ }
74
+ return undefined;
75
+ }
76
+ function emitServices(intro) {
77
+ const domains = [];
78
+ const sorted = intro.services.toSorted((a, b) => a.domain.localeCompare(b.domain));
79
+ for (const entry of sorted) {
80
+ const services = [];
81
+ const serviceEntries = Object.entries(entry.services).toSorted(([a], [b]) => a.localeCompare(b));
82
+ for (const [serviceName, spec] of serviceEntries) {
83
+ const fields = Object.entries(spec.fields ?? {})
84
+ .toSorted(([a], [b]) => a.localeCompare(b))
85
+ .map(([fieldName, fieldSpec]) => {
86
+ const meta = fieldMetaFromSpec(fieldSpec);
87
+ return ` ${JSON.stringify(fieldName)}: ${emitFieldMeta(meta)}`;
88
+ })
89
+ .join(",\n");
90
+ const targetDomain = targetDomainFromSpec(spec.target);
91
+ const targetLiteral = targetDomain === undefined
92
+ ? ""
93
+ : `, target: { domain: ${JSON.stringify(targetDomain)} }`;
94
+ const fieldsBlock = fields === "" ? `{}` : `{\n${fields}\n }`;
95
+ services.push(` ${JSON.stringify(serviceName)}: { fields: ${fieldsBlock}${targetLiteral} },`);
96
+ }
97
+ domains.push(` ${JSON.stringify(entry.domain)}: {\n${services.join("\n")}\n },`);
98
+ }
99
+ return `export const services = {\n${domains.join("\n")}\n} as const;`;
100
+ }
101
+ function emitFieldMeta(meta) {
102
+ const parts = [`type: ${JSON.stringify(meta.type)}`];
103
+ parts.push(`required: ${meta.required ? "true" : "false"}`);
104
+ if (meta.domain !== undefined) {
105
+ parts.push(`domain: ${JSON.stringify(meta.domain)}`);
106
+ }
107
+ if (meta.options !== undefined) {
108
+ const opts = meta.options.map((v) => JSON.stringify(v)).join(", ");
109
+ parts.push(`options: [${opts}] as const`);
110
+ }
111
+ return `{ ${parts.join(", ")} }`;
112
+ }
113
+ function emitEvents(intro) {
114
+ const names = intro.events
115
+ .map((e) => e.event)
116
+ .toSorted((a, b) => a.localeCompare(b));
117
+ const literal = names.length === 0
118
+ ? "[]"
119
+ : `[\n${names.map((n) => ` ${JSON.stringify(n)},`).join("\n")}\n]`;
120
+ return `export const events = ${literal} as const;`;
121
+ }
122
+ function emitEventData(intro) {
123
+ const lines = [];
124
+ const names = new Set(intro.events.map((e) => e.event));
125
+ for (const [name, shape] of Object.entries(KNOWN_EVENT_DATA).toSorted(([a], [b]) => a.localeCompare(b))) {
126
+ if (!names.has(name)) {
127
+ continue;
128
+ }
129
+ lines.push(` ${JSON.stringify(name)}: ${shape};`);
130
+ }
131
+ return `export type EventData = {\n${lines.join("\n")}\n};`;
132
+ }
133
+ function emitSchemaType(name) {
134
+ return [
135
+ `export type ${name} = {`,
136
+ ` entities: typeof entities;`,
137
+ ` services: typeof services;`,
138
+ ` events: (typeof events)[number];`,
139
+ ` eventData: EventData;`,
140
+ `};`,
141
+ ].join("\n");
142
+ }
143
+ //# sourceMappingURL=emit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit.js","sourceRoot":"","sources":["../../src/codegen/emit.ts"],"names":[],"mappings":"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"}