uc-config 0.2.3 → 0.2.5

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 CHANGED
@@ -9,6 +9,24 @@ npx uc-config init --refresh-docs
9
9
  npx uc-config compile && npx uc-config plan # must show 0 operations
10
10
  ```
11
11
 
12
+ ## 0.2.5
13
+
14
+ Action required: none.
15
+
16
+ - `plan` no longer rejects a `select_source` value on entities that publish no
17
+ `source_list` (e.g. the Onkyo/Pioneer/Integra eISCP driver, which takes free
18
+ text like `input-selector tv`). Any non-empty string is accepted there; a
19
+ published list is still enforced.
20
+
21
+ ## 0.2.4
22
+
23
+ Action required: none. Existing configs keep their keys.
24
+
25
+ - `import` builds readable keys from display names (`activity.play_ps5`,
26
+ `activity.play_ps5.button.mute_short_press`, `entity.living_room_lamp`)
27
+ instead of `activity8`. Duplicate names get `_2`, `_3`. To rename keys in an
28
+ existing config, use `state move OLD NEW`.
29
+
12
30
  ## 0.2.3
13
31
 
14
32
  Action required: none. Docs only.
package/dist/adapter.js CHANGED
@@ -530,7 +530,12 @@ export function validateCommandParameters(command, metadata, entity) {
530
530
  const choices = isObject(source)
531
531
  ? source[String(p.items.field)]
532
532
  : undefined;
533
- valid = Array.isArray(choices) && choices.includes(value);
533
+ // Some drivers (e.g. Onkyo eISCP) publish no list at all and accept free
534
+ // text such as "input-selector tv". With no list there is nothing to check
535
+ // against, so accept any non-empty string; a published list is enforced.
536
+ valid = Array.isArray(choices)
537
+ ? choices.includes(value)
538
+ : typeof value === "string" && value.trim() !== "";
534
539
  }
535
540
  else
536
541
  throw new Error(`Unsupported parameter metadata for ${key}`);
@@ -7,6 +7,8 @@ export interface Inventory {
7
7
  unsupported: string[];
8
8
  }
9
9
  export declare function inventory(client: CoreClient): Promise<Inventory>;
10
+ /** snake_case key segment; falls back to the id when the name has no letters/digits. */
11
+ export declare function slug(name: string | undefined, fallback: string): string;
10
12
  export declare function importConfig(client: CoreClient): Promise<{
11
13
  config: Config;
12
14
  warnings: string[];
package/dist/inventory.js CHANGED
@@ -58,13 +58,38 @@ export async function inventory(client) {
58
58
  }
59
59
  return result;
60
60
  }
61
+ /** Display name from a string or a localized {en: ...} map. */
62
+ function nameOf(item) {
63
+ const n = isObject(item) ? item.name : undefined;
64
+ if (typeof n === "string")
65
+ return n;
66
+ if (isObject(n)) {
67
+ const v = n.en ?? Object.values(n).find((x) => typeof x === "string");
68
+ if (typeof v === "string")
69
+ return v;
70
+ }
71
+ return undefined;
72
+ }
73
+ /** snake_case key segment; falls back to the id when the name has no letters/digits. */
74
+ export function slug(name, fallback) {
75
+ const s = (x) => x
76
+ .normalize("NFKD")
77
+ .replace(/[\u0300-\u036f]/g, "")
78
+ .toLowerCase()
79
+ .replace(/[^a-z0-9]+/g, "_")
80
+ .replace(/^_+|_+$/g, "");
81
+ return s(name ?? "") || s(fallback) || "item";
82
+ }
61
83
  export async function importConfig(client) {
62
84
  const inv = await inventory(client);
63
85
  const resources = {};
64
86
  const warnings = [...inv.unsupported.map((p) => `Unsupported endpoint ${p}`)];
65
- let serial = 0;
66
- const add = (r) => {
67
- const key = `${r.kind}${++serial}`;
87
+ // Readable keys from display names ("Play PS5" -> activity.play_ps5);
88
+ // collisions get _2, _3, ... in import order.
89
+ const add = (r, base) => {
90
+ let key = base;
91
+ for (let n = 2; key in resources; n++)
92
+ key = `${base}_${n}`;
68
93
  resources[key] = r;
69
94
  return key;
70
95
  };
@@ -93,7 +118,7 @@ export async function importConfig(client) {
93
118
  warnings.push(`${id}: secret-bearing settings require manual secret() references`);
94
119
  continue;
95
120
  }
96
- const key = add(r);
121
+ const key = add(r, `${kind}.${slug(nameOf(item), id)}`);
97
122
  if (kind === "integration" || kind === "dock")
98
123
  warnings.push(`${key}: existing resource imported; supply create/setup inputs and secret references to reproduce on a new target`);
99
124
  if (kind === "remote" && collection !== "externalRemotes")
@@ -112,7 +137,7 @@ export async function importConfig(client) {
112
137
  parent: { $ref: key },
113
138
  id: `${b.button}/${press}`,
114
139
  data: { [press]: b[press] },
115
- });
140
+ }, `${key}.button.${slug(String(b.button), "button")}_${press}`);
116
141
  const pages = await client.list(`/${prefix}/${encodeURIComponent(id)}/ui/pages`);
117
142
  for (const p of pages)
118
143
  add({
@@ -120,7 +145,7 @@ export async function importConfig(client) {
120
145
  parent: { $ref: key },
121
146
  id: String(p.page_id),
122
147
  data: writable(`/${prefix}/{entityId}/ui/pages/{pageId}`, "patch", p),
123
- });
148
+ }, `${key}.page.${slug(nameOf(p), String(p.page_id))}`);
124
149
  }
125
150
  catch (e) {
126
151
  if (e instanceof ApiError && [404, 405].includes(e.status))
@@ -139,7 +164,7 @@ export async function importConfig(client) {
139
164
  parent: { $ref: key },
140
165
  id: String(c[group ? "group_id" : "page_id"]),
141
166
  data: writable(`/profiles/{profileId}/${child}/{${group ? "groupId" : "pageId"}}`, "patch", c),
142
- });
167
+ }, `${key}.${group ? "group" : "page"}.${slug(nameOf(c), String(c[group ? "group_id" : "page_id"]))}`);
143
168
  }
144
169
  }
145
170
  if (collection === "irRemotes") {
@@ -158,7 +183,7 @@ export async function importConfig(client) {
158
183
  format: code.code.format,
159
184
  value: code.code.value,
160
185
  },
161
- });
186
+ }, `${key}.ir.${slug(code.cmd_id, "code")}`);
162
187
  }
163
188
  }
164
189
  }
@@ -172,7 +197,7 @@ export async function importConfig(client) {
172
197
  id: String(item.entity_id),
173
198
  parent: item.integration_id,
174
199
  data: writable("/entities/{entityId}", "patch", item),
175
- });
200
+ }, `entity.${slug(nameOf(item), String(item.entity_id))}`);
176
201
  }
177
202
  const usedDrivers = new Set((inv.collections.integrations ?? []).map((i) => i.driver_id));
178
203
  for (const item of inv.collections.drivers ?? []) {
@@ -185,7 +210,7 @@ export async function importConfig(client) {
185
210
  warnings.push(`${item.driver_id}: driver settings contain secrets; provide secret references manually`);
186
211
  continue;
187
212
  }
188
- add({ kind: "driver", id: String(item.driver_id), data });
213
+ add({ kind: "driver", id: String(item.driver_id), data }, `driver.${slug(nameOf(item), String(item.driver_id))}`);
189
214
  }
190
215
  for (const [section, data] of Object.entries(inv.settings)) {
191
216
  const picked = writable(`/cfg/${section}`, "patch", data);
@@ -198,7 +223,7 @@ export async function importConfig(client) {
198
223
  if (!Object.keys(picked).length)
199
224
  continue;
200
225
  if (!secretFields(picked).length)
201
- add({ kind: "settings", id: section, data: picked });
226
+ add({ kind: "settings", id: section, data: picked }, `settings.${section}`);
202
227
  else
203
228
  warnings.push(`${section}: secret-bearing settings omitted; configure secret references`);
204
229
  }
package/docs/snippets.md CHANGED
@@ -138,6 +138,12 @@ sequences: {
138
138
 
139
139
  `select_source` values are entity-specific. Copy them from an existing sequence
140
140
  or the entity's `attributes.source_list` (`npm run uc -- api GET /entities/<id>`).
141
+ When the entity publishes a `source_list`, the planner rejects values outside it.
142
+ Some drivers publish none and accept free text instead; the planner then accepts
143
+ any non-empty string, so take the value from the driver's documentation. For
144
+ example, the Onkyo/Pioneer/Integra eISCP driver takes `input-selector <name>`
145
+ (`input-selector tv`, `input-selector bd`, `input-selector game`; see its
146
+ [text commands](https://github.com/EddyMcNut/uc-intg-onkyo-avr/blob/main/docs/cheats.md#text-commands)).
141
147
 
142
148
  ## Create a new activity
143
149
 
@@ -276,8 +282,9 @@ npm run uc -- compile --config .uc/imported-$(date +%F).config.ts --out .uc/impo
276
282
  diff <(jq -S . .uc/build.json) <(jq -S . .uc/imported-build.json) | less
277
283
  ```
278
284
 
279
- Imported keys are generic (`activity3`). Match resources by `kind` + `id` and
280
- copy the changed fields into `remote.config.ts`. Then `plan` should report
285
+ Imported keys come from display names (`activity.watch_tv`), so they usually
286
+ match yours, but a renamed activity gets a different key. Match resources by
287
+ `kind` + `id` and copy the changed fields into `remote.config.ts`. Then `plan` should report
281
288
  0 operations.
282
289
 
283
290
  ## Undo the last apply
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uc-config",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "description": "Configuration-as-code for the Unfolded Circle Remote 3, designed to be driven by coding agents",
6
6
  "license": "MIT",