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 +18 -0
- package/dist/adapter.js +6 -1
- package/dist/inventory.d.ts +2 -0
- package/dist/inventory.js +36 -11
- package/docs/snippets.md +9 -2
- package/package.json +1 -1
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
|
-
|
|
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}`);
|
package/dist/inventory.d.ts
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
280
|
-
|
|
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
|