@khalido/home 0.1.3 → 0.2.1
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/.env.example +2 -2
- package/CHANGELOG.md +56 -1
- package/README.md +7 -2
- package/dist/config.d.ts +5 -3
- package/dist/config.js +5 -1
- package/dist/connector.d.ts +2 -0
- package/dist/connector.js +3 -0
- package/dist/connectors/amber/client.d.ts +1 -1
- package/dist/connectors/amber/client.js +1 -1
- package/dist/connectors/amber/index.js +2 -2
- package/dist/connectors/cast/index.d.ts +1 -1
- package/dist/connectors/cast/index.js +1 -1
- package/dist/connectors/cast/state.d.ts +17 -3
- package/dist/connectors/cast/state.js +26 -7
- package/dist/connectors/daikin/client.d.ts +45 -2
- package/dist/connectors/daikin/client.js +67 -2
- package/dist/connectors/daikin/index.d.ts +2 -2
- package/dist/connectors/daikin/index.js +4 -3
- package/dist/connectors/enphase/client.d.ts +1 -1
- package/dist/connectors/enphase/client.js +1 -1
- package/dist/connectors/enphase/index.js +2 -1
- package/dist/connectors/hue/ca.d.ts +2 -0
- package/dist/connectors/hue/ca.js +38 -0
- package/dist/connectors/hue/client.d.ts +81 -6
- package/dist/connectors/hue/client.js +217 -6
- package/dist/connectors/hue/color.d.ts +30 -0
- package/dist/connectors/hue/color.js +92 -0
- package/dist/connectors/hue/events.d.ts +0 -2
- package/dist/connectors/hue/events.js +1 -2
- package/dist/connectors/hue/index.d.ts +15 -4
- package/dist/connectors/hue/index.js +27 -10
- package/dist/connectors/hue/schemas.d.ts +27 -0
- package/dist/connectors/hue/schemas.js +26 -0
- package/dist/connectors/hue/state.d.ts +10 -1
- package/dist/connectors/hue/state.js +20 -1
- package/dist/connectors/printers/client.d.ts +41 -6
- package/dist/connectors/printers/client.js +81 -8
- package/dist/connectors/printers/codec.d.ts +17 -3
- package/dist/connectors/printers/codec.js +32 -8
- package/dist/connectors/printers/index.d.ts +2 -0
- package/dist/connectors/printers/index.js +4 -0
- package/dist/connectors/printers/raster.d.ts +30 -0
- package/dist/connectors/printers/raster.js +175 -0
- package/dist/connectors/printers/state.d.ts +2 -0
- package/dist/connectors/spotify/changes.d.ts +19 -5
- package/dist/connectors/spotify/changes.js +29 -7
- package/dist/connectors/spotify/index.d.ts +1 -1
- package/dist/connectors/spotify/index.js +20 -3
- package/dist/connectors/up/client.d.ts +1 -1
- package/dist/connectors/up/client.js +1 -1
- package/dist/connectors/up/index.js +2 -2
- package/dist/connectors/weather/google.js +2 -2
- package/dist/devices.d.ts +24 -0
- package/dist/devices.js +56 -2
- package/dist/discover.js +1 -1
- package/dist/finders-entry.d.ts +1 -1
- package/dist/finders-entry.js +1 -1
- package/dist/finders.d.ts +2 -1
- package/dist/finders.js +16 -5
- package/dist/home.d.ts +11 -2
- package/dist/home.js +98 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node-fetch.d.ts +2 -2
- package/dist/node-fetch.js +3 -2
- package/docs/cli.md +15 -7
- package/docs/config.md +11 -3
- package/docs/decisions.md +19 -0
- package/docs/hue.md +161 -0
- package/docs/ideas.md +2 -0
- package/docs/using.md +98 -3
- package/examples/home.jsonc +2 -1
- package/home.schema.json +10 -3
- package/llms.txt +19 -6
- package/package.json +1 -1
- package/skills/draw-home/README.md +2 -0
- package/skills/draw-home/SKILL.md +1 -1
- package/skills/use-home/SKILL.md +3 -3
- package/src/cli/cast.ts +51 -5
- package/src/cli/context.ts +2 -0
- package/src/cli/daikin.ts +71 -0
- package/src/cli/enphase.ts +31 -5
- package/src/cli/hue.ts +147 -59
- package/src/cli/main.ts +43 -13
- package/src/cli/printers.ts +254 -3
- package/src/cli/probe.ts +21 -17
- package/src/cli/status.ts +61 -10
- package/src/config.ts +8 -2
- package/src/connector.ts +5 -0
- package/src/connectors/amber/README.md +1 -1
- package/src/connectors/amber/client.ts +1 -1
- package/src/connectors/amber/index.ts +2 -2
- package/src/connectors/cast/README.md +6 -3
- package/src/connectors/cast/index.ts +7 -1
- package/src/connectors/cast/state.ts +32 -5
- package/src/connectors/daikin/README.md +16 -4
- package/src/connectors/daikin/client.ts +104 -3
- package/src/connectors/daikin/index.ts +8 -2
- package/src/connectors/enphase/README.md +3 -1
- package/src/connectors/enphase/client.ts +1 -1
- package/src/connectors/enphase/index.ts +2 -1
- package/src/connectors/hue/README.md +62 -17
- package/src/connectors/hue/ca.ts +39 -0
- package/src/connectors/hue/client.ts +362 -15
- package/src/connectors/hue/color.ts +123 -0
- package/src/connectors/hue/events.ts +1 -4
- package/src/connectors/hue/index.ts +46 -13
- package/src/connectors/hue/schemas.ts +26 -0
- package/src/connectors/hue/state.ts +27 -1
- package/src/connectors/printers/README.md +9 -4
- package/src/connectors/printers/client.ts +186 -12
- package/src/connectors/printers/codec.ts +44 -10
- package/src/connectors/printers/index.ts +17 -0
- package/src/connectors/printers/raster.ts +215 -0
- package/src/connectors/printers/state.ts +2 -0
- package/src/connectors/spotify/README.md +10 -2
- package/src/connectors/spotify/changes.ts +38 -9
- package/src/connectors/spotify/index.ts +21 -2
- package/src/connectors/up/README.md +1 -1
- package/src/connectors/up/client.ts +1 -1
- package/src/connectors/up/index.ts +2 -2
- package/src/connectors/weather/google.ts +6 -2
- package/src/devices.ts +83 -3
- package/src/discover.ts +1 -2
- package/src/finders-entry.ts +1 -1
- package/src/finders.ts +18 -6
- package/src/home.ts +99 -39
- package/src/index.ts +7 -1
- package/src/node-fetch.ts +6 -2
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { asOf, realClock } from "../../clock.js";
|
|
2
|
-
import { errorText } from "../../connector.js";
|
|
2
|
+
import { errorText, isRejectedKey } from "../../connector.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { lostDetail, relocator } from "../../discover.js";
|
|
5
5
|
import { envName, filePath, deviceRecords, } from "../../home.js";
|
|
6
6
|
import { bunPinnedFetch, hueClient } from "./client.js";
|
|
7
|
-
import { hueEvents
|
|
7
|
+
import { hueEvents } from "./events.js";
|
|
8
8
|
import { groups, lights, reduce, snapshotFrom, } from "./state.js";
|
|
9
|
-
export {
|
|
9
|
+
export { HUE_BRIDGE_ROOT_CA } from "./ca.js";
|
|
10
|
+
export { colorToXy, kelvinToMirek, mirekToKelvin, xyToHex, } from "./color.js";
|
|
11
|
+
export { bunPinnedFetch, CONFIRM_MS, HueError, hueClient, pair, pairWhenPressed, } from "./client.js";
|
|
10
12
|
export { groups, homeGroupedLightId, lights, scenes, } from "./state.js";
|
|
11
13
|
/** A Hue bridge in a home config: what it takes to reach it. Its model and software come live into the snapshot. */
|
|
12
14
|
export const configSchema = z.looseObject({
|
|
@@ -16,7 +18,9 @@ export const configSchema = z.looseObject({
|
|
|
16
18
|
.string()
|
|
17
19
|
.describe("the bridge id, the CN its certificate must carry: its identity"),
|
|
18
20
|
key: envName.describe("env name holding the application key from `home pair`"),
|
|
19
|
-
ca: filePath
|
|
21
|
+
ca: filePath
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("a root CA (PEM) to trust instead of Signify's two, which the package carries; relative to the config file"),
|
|
20
24
|
});
|
|
21
25
|
/**
|
|
22
26
|
* Hue as a home integration: a full read on every (re)connect, then patched from the event stream; `refresh` re-reads it all.
|
|
@@ -162,8 +166,20 @@ const view = (s) => ({
|
|
|
162
166
|
brightness,
|
|
163
167
|
})),
|
|
164
168
|
});
|
|
165
|
-
/** "Kitchen: on · 40%", "Kitchen: off", or the name alone when the bridge hasn't said. */
|
|
166
|
-
const
|
|
169
|
+
/** A light, room or zone in a line: "Kitchen: on · 40%", "Desk: on · 40% · 2700 K", "Strip: on · 80% · #ff8800", "Kitchen: off", or the name alone when the bridge hasn't said. */
|
|
170
|
+
export const hueText = (v) => {
|
|
171
|
+
if (v.on === undefined)
|
|
172
|
+
return v.name ?? v.id;
|
|
173
|
+
const parts = v.on
|
|
174
|
+
? [
|
|
175
|
+
"on",
|
|
176
|
+
v.brightness === undefined ? "" : `${Math.round(v.brightness)}%`,
|
|
177
|
+
// A colour light showing a white reports both; the white is what it is showing.
|
|
178
|
+
v.kelvin !== undefined ? `${v.kelvin} K` : (v.hex ?? ""),
|
|
179
|
+
]
|
|
180
|
+
: ["off"];
|
|
181
|
+
return `${v.name ?? v.id}: ${parts.filter(Boolean).join(" · ")}`;
|
|
182
|
+
};
|
|
167
183
|
/** Every light, then every room and zone. A light's `available` is its Zigbee link; a group's `members` are its lights. */
|
|
168
184
|
export function hueHomeDevices(part) {
|
|
169
185
|
const s = part?.state;
|
|
@@ -181,7 +197,7 @@ export function hueHomeDevices(part) {
|
|
|
181
197
|
...(l.reachable === undefined ? {} : { available: l.reachable }),
|
|
182
198
|
...on(l),
|
|
183
199
|
asOf: s.asOf,
|
|
184
|
-
text:
|
|
200
|
+
text: hueText(l),
|
|
185
201
|
detail: l,
|
|
186
202
|
})),
|
|
187
203
|
...groups(s).map((g) => ({
|
|
@@ -191,7 +207,7 @@ export function hueHomeDevices(part) {
|
|
|
191
207
|
kind: "group",
|
|
192
208
|
...on(g),
|
|
193
209
|
asOf: s.asOf,
|
|
194
|
-
text:
|
|
210
|
+
text: hueText(g),
|
|
195
211
|
members: g.lightIds,
|
|
196
212
|
detail: g,
|
|
197
213
|
})),
|
|
@@ -223,7 +239,8 @@ export const manifest = {
|
|
|
223
239
|
{
|
|
224
240
|
name: "HUE_CA_FILE",
|
|
225
241
|
example: "/path/to/hue-root-ca.pem",
|
|
226
|
-
|
|
242
|
+
optional: true,
|
|
243
|
+
what: "a root CA to trust instead of Signify's two, which the package carries",
|
|
227
244
|
},
|
|
228
245
|
],
|
|
229
246
|
files: ["ca"],
|
|
@@ -232,7 +249,7 @@ export const manifest = {
|
|
|
232
249
|
if (!state)
|
|
233
250
|
return [];
|
|
234
251
|
const { lightsOn } = view(state);
|
|
235
|
-
return [`${lightsOn} lights on`, ...groups(state).map(
|
|
252
|
+
return [`${lightsOn} lights on`, ...groups(state).map(hueText)];
|
|
236
253
|
},
|
|
237
254
|
record: ({ state }) => (state ? view(state) : {}),
|
|
238
255
|
};
|
|
@@ -19,6 +19,33 @@ export declare const Resource: z.ZodObject<{
|
|
|
19
19
|
dimming: z.ZodOptional<z.ZodObject<{
|
|
20
20
|
brightness: z.ZodNumber;
|
|
21
21
|
}, z.core.$loose>>;
|
|
22
|
+
color: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
23
|
+
xy: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
24
|
+
x: z.ZodNumber;
|
|
25
|
+
y: z.ZodNumber;
|
|
26
|
+
}, z.core.$loose>>>;
|
|
27
|
+
gamut: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
28
|
+
red: z.ZodObject<{
|
|
29
|
+
x: z.ZodNumber;
|
|
30
|
+
y: z.ZodNumber;
|
|
31
|
+
}, z.core.$loose>;
|
|
32
|
+
green: z.ZodObject<{
|
|
33
|
+
x: z.ZodNumber;
|
|
34
|
+
y: z.ZodNumber;
|
|
35
|
+
}, z.core.$loose>;
|
|
36
|
+
blue: z.ZodObject<{
|
|
37
|
+
x: z.ZodNumber;
|
|
38
|
+
y: z.ZodNumber;
|
|
39
|
+
}, z.core.$loose>;
|
|
40
|
+
}, z.core.$loose>>>;
|
|
41
|
+
}, z.core.$loose>>>;
|
|
42
|
+
color_temperature: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
43
|
+
mirek: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
44
|
+
mirek_schema: z.ZodOptional<z.ZodObject<{
|
|
45
|
+
mirek_minimum: z.ZodOptional<z.ZodNumber>;
|
|
46
|
+
mirek_maximum: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
}, z.core.$loose>>;
|
|
48
|
+
}, z.core.$loose>>>;
|
|
22
49
|
children: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
23
50
|
rid: z.ZodString;
|
|
24
51
|
rtype: z.ZodString;
|
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
// field is unconfirmed from the primary source. Only the fields the snapshot reads are parsed;
|
|
5
5
|
// all are optional and unknown fields pass through, so a schema change degrades, never throws.
|
|
6
6
|
const ref = z.object({ rid: z.string(), rtype: z.string() });
|
|
7
|
+
const xy = z.looseObject({ x: z.number(), y: z.number() });
|
|
7
8
|
/** The resource kinds this library reads. */
|
|
8
9
|
export const KINDS = [
|
|
9
10
|
"light",
|
|
@@ -22,6 +23,31 @@ export const Resource = z.looseObject({
|
|
|
22
23
|
metadata: z.looseObject({ name: z.string().optional() }).optional(),
|
|
23
24
|
on: z.looseObject({ on: z.boolean() }).optional(),
|
|
24
25
|
dimming: z.looseObject({ brightness: z.number() }).optional(),
|
|
26
|
+
// A light's colour and the triangle of colours it can show; absent on a white light. A shape this doesn't expect
|
|
27
|
+
// reads as absent, so one odd light never fails the whole read.
|
|
28
|
+
color: z
|
|
29
|
+
.looseObject({
|
|
30
|
+
xy: xy.optional().catch(undefined),
|
|
31
|
+
gamut: z
|
|
32
|
+
.looseObject({ red: xy, green: xy, blue: xy })
|
|
33
|
+
.optional()
|
|
34
|
+
.catch(undefined),
|
|
35
|
+
})
|
|
36
|
+
.optional()
|
|
37
|
+
.catch(undefined),
|
|
38
|
+
// mirek is null while the light shows a colour; the schema is this light's own range.
|
|
39
|
+
color_temperature: z
|
|
40
|
+
.looseObject({
|
|
41
|
+
mirek: z.number().nullable().optional(),
|
|
42
|
+
mirek_schema: z
|
|
43
|
+
.looseObject({
|
|
44
|
+
mirek_minimum: z.number().optional(),
|
|
45
|
+
mirek_maximum: z.number().optional(),
|
|
46
|
+
})
|
|
47
|
+
.optional(),
|
|
48
|
+
})
|
|
49
|
+
.optional()
|
|
50
|
+
.catch(undefined),
|
|
25
51
|
children: z.array(ref).optional(),
|
|
26
52
|
services: z.array(ref).optional(),
|
|
27
53
|
group: ref.optional(), // scene: the room or zone it belongs to
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Gamut, type XY } from "./color.ts";
|
|
1
2
|
import type { HueEvent, Resource } from "./schemas.ts";
|
|
2
3
|
/** Every resource on the bridge by id, from the last full read plus every event since. `asOf` is local time it was last known fresh. */
|
|
3
4
|
export type HueSnapshot = {
|
|
@@ -17,8 +18,16 @@ export type LightView = {
|
|
|
17
18
|
on?: boolean;
|
|
18
19
|
brightness?: number;
|
|
19
20
|
reachable?: boolean;
|
|
21
|
+
/** White temperature in kelvin, while the light shows a white; its range is `kelvinRange` (warmest, coolest). */
|
|
22
|
+
kelvin?: number;
|
|
23
|
+
kelvinRange?: [number, number];
|
|
24
|
+
/** The colour it shows: the bridge's `xy`, and `hex` for a screen (the hue at full brightness). Colour lights only. */
|
|
25
|
+
xy?: XY;
|
|
26
|
+
hex?: string;
|
|
27
|
+
/** The colours it can show. Present on a colour light, so its presence says "takes a colour". */
|
|
28
|
+
gamut?: Gamut;
|
|
20
29
|
};
|
|
21
|
-
/** Every light, with on, brightness and
|
|
30
|
+
/** Every light, with on, brightness, reachability, and its white temperature and colour where it has them. */
|
|
22
31
|
export declare function lights(s: HueSnapshot): LightView[];
|
|
23
32
|
/** A room or zone as a row shows it, with its grouped light's state. */
|
|
24
33
|
export type GroupView = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mirekToKelvin, xyToHex } from "./color.js";
|
|
1
2
|
/** A snapshot from one full `get()`. */
|
|
2
3
|
export function snapshotFrom(resources, asOf) {
|
|
3
4
|
return {
|
|
@@ -32,7 +33,7 @@ const ofType = (s, type) => Object.values(s.resources).filter((r) => r.type ===
|
|
|
32
33
|
const groupedLightOf = (s, r) => r.services
|
|
33
34
|
?.map((x) => s.resources[x.rid])
|
|
34
35
|
.find((x) => x?.type === "grouped_light");
|
|
35
|
-
/** Every light, with on, brightness and
|
|
36
|
+
/** Every light, with on, brightness, reachability, and its white temperature and colour where it has them. */
|
|
36
37
|
export function lights(s) {
|
|
37
38
|
const zigbee = ofType(s, "zigbee_connectivity");
|
|
38
39
|
return ofType(s, "light").map((l) => {
|
|
@@ -43,9 +44,27 @@ export function lights(s) {
|
|
|
43
44
|
on: l.on?.on,
|
|
44
45
|
brightness: l.dimming?.brightness,
|
|
45
46
|
reachable: link ? link.status === "connected" : undefined,
|
|
47
|
+
...colorOf(l),
|
|
46
48
|
};
|
|
47
49
|
});
|
|
48
50
|
}
|
|
51
|
+
/** A light's temperature and colour fields, only those the bridge reported. */
|
|
52
|
+
function colorOf(l) {
|
|
53
|
+
const out = {};
|
|
54
|
+
const ct = l.color_temperature;
|
|
55
|
+
if (typeof ct?.mirek === "number")
|
|
56
|
+
out.kelvin = mirekToKelvin(ct.mirek);
|
|
57
|
+
const { mirek_minimum: min, mirek_maximum: max } = ct?.mirek_schema ?? {};
|
|
58
|
+
if (min !== undefined && max !== undefined)
|
|
59
|
+
out.kelvinRange = [mirekToKelvin(max), mirekToKelvin(min)];
|
|
60
|
+
if (l.color?.xy) {
|
|
61
|
+
out.xy = l.color.xy;
|
|
62
|
+
out.hex = xyToHex(l.color.xy);
|
|
63
|
+
}
|
|
64
|
+
if (l.color?.gamut)
|
|
65
|
+
out.gamut = l.color.gamut;
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
49
68
|
/** Rooms and zones. A room's children are devices; a zone's are lights. */
|
|
50
69
|
export function groups(s) {
|
|
51
70
|
const all = ofType(s, "light");
|
|
@@ -17,12 +17,47 @@ export declare function printerUrl(a: PrinterAddress): {
|
|
|
17
17
|
secure: boolean;
|
|
18
18
|
path: string;
|
|
19
19
|
};
|
|
20
|
-
|
|
21
|
-
* Get-Printer-Attributes over HTTP, through `fetchTimeout`. A non-ok HTTP status or an IPP status of 0x0100 and up throws a
|
|
22
|
-
* ConnectorError saying which. Returns the whole response; `attributesOf` gives the printer's attributes.
|
|
23
|
-
*/
|
|
24
|
-
export declare function getPrinterAttributes(fetcher: typeof fetch, address: PrinterAddress, options?: {
|
|
20
|
+
type CallOptions = {
|
|
25
21
|
requestId?: number;
|
|
26
22
|
signal?: AbortSignal;
|
|
27
23
|
timeoutMs?: number;
|
|
28
|
-
}
|
|
24
|
+
};
|
|
25
|
+
/** Get-Printer-Attributes: the whole response; `attributesOf` gives the printer's attributes. `requested` replaces the poll's list. */
|
|
26
|
+
export declare const getPrinterAttributes: (fetcher: typeof fetch, address: PrinterAddress, options?: CallOptions & {
|
|
27
|
+
requested?: string[];
|
|
28
|
+
}) => Promise<IppMessage>;
|
|
29
|
+
/** Identify-Printer: the printer shows or sounds that it is the one (PWG 5100.13). No paper. */
|
|
30
|
+
export declare const identifyPrinter: (fetcher: typeof fetch, address: PrinterAddress, options?: CallOptions) => Promise<unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* What a job says about itself. `format` is the document's MIME type and must be one the printer lists; `media` a PWG
|
|
33
|
+
* media name (`iso_a4_210x297mm`). `sides` defaults to `one-sided`, sent explicitly so a printer's duplex default
|
|
34
|
+
* never applies to a document made for one side.
|
|
35
|
+
*/
|
|
36
|
+
export type PrintJob = {
|
|
37
|
+
format: string;
|
|
38
|
+
name?: string;
|
|
39
|
+
user?: string;
|
|
40
|
+
media?: string;
|
|
41
|
+
sides?: "one-sided" | "two-sided-long-edge" | "two-sided-short-edge";
|
|
42
|
+
copies?: number;
|
|
43
|
+
};
|
|
44
|
+
/** A job the printer took: its id, and its state when it answered (RFC 8011: 3 pending, 5 processing, 9 completed). */
|
|
45
|
+
export type PrintedJob = {
|
|
46
|
+
jobId?: number;
|
|
47
|
+
state?: number;
|
|
48
|
+
reasons?: string[];
|
|
49
|
+
};
|
|
50
|
+
/** Validate-Job: the printer checks the job's attributes as Print-Job would, and prints nothing. */
|
|
51
|
+
export declare const validateJob: (fetcher: typeof fetch, address: PrinterAddress, job: PrintJob, options?: CallOptions) => Promise<unknown>;
|
|
52
|
+
/** No upload waits longer than this: a page of raster is a few hundred kB, and a printer can be slow to take it. */
|
|
53
|
+
export declare const PRINT_TIMEOUT_MS = 60000;
|
|
54
|
+
/**
|
|
55
|
+
* Print-Job: the job's attributes and the document in one request. A timeout leaves it unknown whether the printer took
|
|
56
|
+
* the job, so the error says not to send it again blindly.
|
|
57
|
+
*/
|
|
58
|
+
export declare function printJob(fetcher: typeof fetch, address: PrinterAddress, job: PrintJob, document: Uint8Array, options?: CallOptions): Promise<PrintedJob>;
|
|
59
|
+
/** Cancel-Job: stop a job by its id, from Print-Job's answer. */
|
|
60
|
+
export declare const cancelJob: (fetcher: typeof fetch, address: PrinterAddress, jobId: number, options?: CallOptions & {
|
|
61
|
+
user?: string;
|
|
62
|
+
}) => Promise<unknown>;
|
|
63
|
+
export {};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ConnectorError, fetchTimeout } from "../../connector.js";
|
|
2
|
-
import { attributesOf, decodeMessage, getPrinterAttributesRequest, } from "./codec.js";
|
|
3
|
-
// Pure
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
import { attributesOf, decodeMessage, getPrinterAttributesRequest, OPERATION, operationRequest, TAG, } from "./codec.js";
|
|
3
|
+
// Pure calls: a `fetch`, an address and the bytes to send come in, the printer's answer goes out. The reads are
|
|
4
|
+
// Get-Printer-Attributes; the writes Identify-Printer, Validate-Job, Print-Job and Cancel-Job. The document is the caller's,
|
|
5
|
+
// already in a format the printer lists (`document-format-supported`): this package makes no PDF into raster.
|
|
6
6
|
/** Everything the integration reads from one printer. */
|
|
7
7
|
export const REQUESTED = [
|
|
8
8
|
"printer-state",
|
|
@@ -51,15 +51,15 @@ const STATUS = {
|
|
|
51
51
|
0x0507: "server-error-busy",
|
|
52
52
|
};
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
* ConnectorError saying which
|
|
54
|
+
* One IPP request over HTTP, through `fetchTimeout`. A non-ok HTTP status or an IPP status of 0x0100 and up throws a
|
|
55
|
+
* ConnectorError saying which (`code: "ipp"`, the IPP status in `status`).
|
|
56
56
|
*/
|
|
57
|
-
|
|
57
|
+
async function call(fetcher, address, body, options = {}) {
|
|
58
58
|
const url = printerUrl(address);
|
|
59
59
|
const res = await fetchTimeout(fetcher, url.http, {
|
|
60
60
|
method: "POST",
|
|
61
61
|
headers: { "content-type": "application/ipp", accept: "application/ipp" },
|
|
62
|
-
body:
|
|
62
|
+
body: body(url.ipp),
|
|
63
63
|
}, options);
|
|
64
64
|
if (!res.ok)
|
|
65
65
|
throw new ConnectorError(`HTTP ${res.status} on POST /${url.path}`, {
|
|
@@ -72,3 +72,76 @@ export async function getPrinterAttributes(fetcher, address, options = {}) {
|
|
|
72
72
|
}
|
|
73
73
|
return message;
|
|
74
74
|
}
|
|
75
|
+
/** Get-Printer-Attributes: the whole response; `attributesOf` gives the printer's attributes. `requested` replaces the poll's list. */
|
|
76
|
+
export const getPrinterAttributes = (fetcher, address, options = {}) => call(fetcher, address, (uri) => getPrinterAttributesRequest(uri, options.requested ?? REQUESTED, options.requestId), options);
|
|
77
|
+
/** Identify-Printer: the printer shows or sounds that it is the one (PWG 5100.13). No paper. */
|
|
78
|
+
export const identifyPrinter = (fetcher, address, options = {}) => call(fetcher, address, (uri) => operationRequest(OPERATION.identifyPrinter, uri, {
|
|
79
|
+
requestId: options.requestId,
|
|
80
|
+
}), options);
|
|
81
|
+
/** Who asks: RFC 8011 has a client send it on every operation. `home` unless the caller names someone. */
|
|
82
|
+
const userAttribute = (user = "home") => ({
|
|
83
|
+
name: "requesting-user-name",
|
|
84
|
+
tag: TAG.nameWithoutLanguage,
|
|
85
|
+
values: [user],
|
|
86
|
+
});
|
|
87
|
+
function jobAttributes(job) {
|
|
88
|
+
const attributes = [
|
|
89
|
+
userAttribute(job.user),
|
|
90
|
+
{
|
|
91
|
+
name: "job-name",
|
|
92
|
+
tag: TAG.nameWithoutLanguage,
|
|
93
|
+
values: [job.name ?? "home"],
|
|
94
|
+
},
|
|
95
|
+
{ name: "document-format", tag: TAG.mimeMediaType, values: [job.format] },
|
|
96
|
+
];
|
|
97
|
+
const attrs = [
|
|
98
|
+
{ name: "sides", tag: TAG.keyword, values: [job.sides ?? "one-sided"] },
|
|
99
|
+
];
|
|
100
|
+
if (job.media)
|
|
101
|
+
attrs.push({ name: "media", tag: TAG.keyword, values: [job.media] });
|
|
102
|
+
if (job.copies !== undefined)
|
|
103
|
+
attrs.push({ name: "copies", tag: TAG.integer, values: [job.copies] });
|
|
104
|
+
return { attributes, job: attrs };
|
|
105
|
+
}
|
|
106
|
+
/** Validate-Job: the printer checks the job's attributes as Print-Job would, and prints nothing. */
|
|
107
|
+
export const validateJob = (fetcher, address, job, options = {}) => call(fetcher, address, (uri) => operationRequest(OPERATION.validateJob, uri, {
|
|
108
|
+
...jobAttributes(job),
|
|
109
|
+
requestId: options.requestId,
|
|
110
|
+
}), options);
|
|
111
|
+
/** No upload waits longer than this: a page of raster is a few hundred kB, and a printer can be slow to take it. */
|
|
112
|
+
export const PRINT_TIMEOUT_MS = 60_000;
|
|
113
|
+
/**
|
|
114
|
+
* Print-Job: the job's attributes and the document in one request. A timeout leaves it unknown whether the printer took
|
|
115
|
+
* the job, so the error says not to send it again blindly.
|
|
116
|
+
*/
|
|
117
|
+
export async function printJob(fetcher, address, job, document, options = {}) {
|
|
118
|
+
let answer;
|
|
119
|
+
try {
|
|
120
|
+
answer = await call(fetcher, address, (uri) => operationRequest(OPERATION.printJob, uri, {
|
|
121
|
+
...jobAttributes(job),
|
|
122
|
+
data: document,
|
|
123
|
+
requestId: options.requestId,
|
|
124
|
+
}), { timeoutMs: PRINT_TIMEOUT_MS, ...options });
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof Error && error.name === "TimeoutError")
|
|
128
|
+
throw new ConnectorError("the printer did not answer the print job in time; it may still print, so check the printer before sending it again", { code: "timeout", cause: error });
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
const a = attributesOf(answer, TAG.job);
|
|
132
|
+
const n = (v) => (typeof v === "number" ? v : undefined);
|
|
133
|
+
const reasons = a["job-state-reasons"]?.filter((r) => typeof r === "string");
|
|
134
|
+
return {
|
|
135
|
+
jobId: n(a["job-id"]?.[0]),
|
|
136
|
+
state: n(a["job-state"]?.[0]),
|
|
137
|
+
reasons: reasons?.length ? reasons : undefined,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** Cancel-Job: stop a job by its id, from Print-Job's answer. */
|
|
141
|
+
export const cancelJob = (fetcher, address, jobId, options = {}) => call(fetcher, address, (uri) => operationRequest(OPERATION.cancelJob, uri, {
|
|
142
|
+
requestId: options.requestId,
|
|
143
|
+
attributes: [
|
|
144
|
+
{ name: "job-id", tag: TAG.integer, values: [jobId] },
|
|
145
|
+
userAttribute(options.user), // printers often let only the job's owner cancel it
|
|
146
|
+
],
|
|
147
|
+
}), options);
|
|
@@ -23,9 +23,13 @@ export declare const TAG: {
|
|
|
23
23
|
readonly naturalLanguage: 72;
|
|
24
24
|
readonly mimeMediaType: 73;
|
|
25
25
|
};
|
|
26
|
-
/** The operation ids this package sends
|
|
26
|
+
/** The operation ids this package sends (RFC 8011; Identify-Printer from PWG 5100.13). */
|
|
27
27
|
export declare const OPERATION: {
|
|
28
|
+
readonly printJob: 2;
|
|
29
|
+
readonly validateJob: 4;
|
|
30
|
+
readonly cancelJob: 8;
|
|
28
31
|
readonly getPrinterAttributes: 11;
|
|
32
|
+
readonly identifyPrinter: 60;
|
|
29
33
|
};
|
|
30
34
|
export type IppRange = {
|
|
31
35
|
lower: number;
|
|
@@ -65,8 +69,18 @@ export type IppMessage = {
|
|
|
65
69
|
};
|
|
66
70
|
/** A message as bytes. Collections are not encoded (a Get-Printer-Attributes request has none). */
|
|
67
71
|
export declare function encodeMessage(m: IppMessage): Uint8Array<ArrayBuffer>;
|
|
68
|
-
/**
|
|
69
|
-
|
|
72
|
+
/**
|
|
73
|
+
* A request to one printer: IPP 2.0, utf-8, English, the printer's `ipp://` URI, then `attributes` in the operation
|
|
74
|
+
* group, `job` (when given) in a job group, and `data` (a document) after the end tag.
|
|
75
|
+
*/
|
|
76
|
+
export declare function operationRequest(code: number, printerUri: string, options?: {
|
|
77
|
+
attributes?: IppAttribute[];
|
|
78
|
+
job?: IppAttribute[];
|
|
79
|
+
data?: Uint8Array;
|
|
80
|
+
requestId?: number;
|
|
81
|
+
}): Uint8Array<ArrayBuffer>;
|
|
82
|
+
/** The Get-Printer-Attributes request: the attributes wanted. */
|
|
83
|
+
export declare const getPrinterAttributesRequest: (printerUri: string, requested: string[], requestId?: number) => Uint8Array<ArrayBuffer>;
|
|
70
84
|
/**
|
|
71
85
|
* A message's bytes as groups of attributes. An unknown or malformed value is kept as `IppUnknown`, never thrown; only a
|
|
72
86
|
* message that cannot be walked (cut short, a value before any attribute) throws. A collection and its members are skipped.
|
|
@@ -27,8 +27,14 @@ export const TAG = {
|
|
|
27
27
|
naturalLanguage: 0x48,
|
|
28
28
|
mimeMediaType: 0x49,
|
|
29
29
|
};
|
|
30
|
-
/** The operation ids this package sends
|
|
31
|
-
export const OPERATION = {
|
|
30
|
+
/** The operation ids this package sends (RFC 8011; Identify-Printer from PWG 5100.13). */
|
|
31
|
+
export const OPERATION = {
|
|
32
|
+
printJob: 0x0002,
|
|
33
|
+
validateJob: 0x0004,
|
|
34
|
+
cancelJob: 0x0008,
|
|
35
|
+
getPrinterAttributes: 0x000b,
|
|
36
|
+
identifyPrinter: 0x003c,
|
|
37
|
+
};
|
|
32
38
|
const STRING_TAGS = new Set([
|
|
33
39
|
TAG.textWithoutLanguage,
|
|
34
40
|
TAG.nameWithoutLanguage,
|
|
@@ -94,14 +100,21 @@ export function encodeMessage(m) {
|
|
|
94
100
|
});
|
|
95
101
|
}
|
|
96
102
|
out.push(TAG.end);
|
|
97
|
-
|
|
103
|
+
const bytes = new Uint8Array(out.length + (m.data?.length ?? 0));
|
|
104
|
+
bytes.set(out);
|
|
105
|
+
if (m.data)
|
|
106
|
+
bytes.set(m.data, out.length);
|
|
107
|
+
return bytes;
|
|
98
108
|
}
|
|
99
|
-
/**
|
|
100
|
-
|
|
109
|
+
/**
|
|
110
|
+
* A request to one printer: IPP 2.0, utf-8, English, the printer's `ipp://` URI, then `attributes` in the operation
|
|
111
|
+
* group, `job` (when given) in a job group, and `data` (a document) after the end tag.
|
|
112
|
+
*/
|
|
113
|
+
export function operationRequest(code, printerUri, options = {}) {
|
|
101
114
|
return encodeMessage({
|
|
102
115
|
version: [2, 0],
|
|
103
|
-
code
|
|
104
|
-
requestId,
|
|
116
|
+
code,
|
|
117
|
+
requestId: options.requestId ?? 1,
|
|
105
118
|
groups: [
|
|
106
119
|
{
|
|
107
120
|
tag: TAG.operation,
|
|
@@ -113,12 +126,23 @@ export function getPrinterAttributesRequest(printerUri, requested, requestId = 1
|
|
|
113
126
|
values: ["en"],
|
|
114
127
|
},
|
|
115
128
|
{ name: "printer-uri", tag: TAG.uri, values: [printerUri] },
|
|
116
|
-
|
|
129
|
+
...(options.attributes ?? []),
|
|
117
130
|
],
|
|
118
131
|
},
|
|
132
|
+
...(options.job?.length
|
|
133
|
+
? [{ tag: TAG.job, attributes: options.job }]
|
|
134
|
+
: []),
|
|
119
135
|
],
|
|
136
|
+
data: options.data,
|
|
120
137
|
});
|
|
121
138
|
}
|
|
139
|
+
/** The Get-Printer-Attributes request: the attributes wanted. */
|
|
140
|
+
export const getPrinterAttributesRequest = (printerUri, requested, requestId = 1) => operationRequest(OPERATION.getPrinterAttributes, printerUri, {
|
|
141
|
+
requestId,
|
|
142
|
+
attributes: [
|
|
143
|
+
{ name: "requested-attributes", tag: TAG.keyword, values: requested },
|
|
144
|
+
],
|
|
145
|
+
});
|
|
122
146
|
const fail = (why) => {
|
|
123
147
|
throw new Error(`ipp: ${why}`);
|
|
124
148
|
};
|
|
@@ -5,6 +5,8 @@ import { type DeviceRecord, type Integration, type Part } from "../../home.ts";
|
|
|
5
5
|
import type { Manifest } from "../../manifest.ts";
|
|
6
6
|
import { type Printer, type PrinterStatus, type PrintersState } from "./state.ts";
|
|
7
7
|
export { type Marker, type Printer, type PrintersState, type PrinterStatus, printerText, } from "./state.ts";
|
|
8
|
+
export { cancelJob, identifyPrinter, type PrintedJob, type PrinterAddress, type PrintJob, printJob, validateJob, } from "./client.ts";
|
|
9
|
+
export { type GreyPage, mediaPixels, pwgRaster, type RasterMedia, } from "./raster.ts";
|
|
8
10
|
/** One printer in a home config. Everything the printer reports about itself (model, markers, state) comes live; `host` and `port` are only where it last answered. */
|
|
9
11
|
export declare const configSchema: z.ZodObject<{
|
|
10
12
|
connector: z.ZodLiteral<"printers">;
|
|
@@ -8,6 +8,8 @@ import { getPrinterAttributes, printerUrl } from "./client.js";
|
|
|
8
8
|
import { attributesOf } from "./codec.js";
|
|
9
9
|
import { printerKey, printerText, toState, } from "./state.js";
|
|
10
10
|
export { printerText, } from "./state.js";
|
|
11
|
+
export { cancelJob, identifyPrinter, printJob, validateJob, } from "./client.js";
|
|
12
|
+
export { mediaPixels, pwgRaster, } from "./raster.js";
|
|
11
13
|
/** One printer in a home config. Everything the printer reports about itself (model, markers, state) comes live; `host` and `port` are only where it last answered. */
|
|
12
14
|
export const configSchema = z.looseObject({
|
|
13
15
|
connector: z.literal("printers"),
|
|
@@ -92,6 +94,8 @@ export function printers(config = {}) {
|
|
|
92
94
|
name: t.name,
|
|
93
95
|
model: good.model ?? t.foundModel,
|
|
94
96
|
host: t.host,
|
|
97
|
+
port: t.port,
|
|
98
|
+
rp: t.rp,
|
|
95
99
|
room: t.room,
|
|
96
100
|
...(offline ? { state: "offline" } : {}),
|
|
97
101
|
ipps: t.ipps,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** One page: `pixels` is `width × height` bytes, row by row from the top, 0 black to 255 white. */
|
|
2
|
+
export type GreyPage = {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
pixels: Uint8Array;
|
|
6
|
+
};
|
|
7
|
+
/** What every page is printed as: the printer's resolution and the PWG media name and size it lists. */
|
|
8
|
+
export type RasterMedia = {
|
|
9
|
+
/** PWG self-describing name, as the printer lists it in `media-supported`: `iso_a4_210x297mm`. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** dots per inch, one of the printer's `pwg-raster-document-resolution-supported`. */
|
|
12
|
+
dpi: number;
|
|
13
|
+
};
|
|
14
|
+
/** The pixels a page of this media is at its resolution, rounded down as macOS's filters do: A4 at 600 dpi is 4960 × 7015. */
|
|
15
|
+
export declare function mediaPixels(media: RasterMedia): {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Pages as one PWG Raster document, `image/pwg-raster`. Every page must be `width × height` bytes. Pages may come one at
|
|
21
|
+
* a time (a generator), so only the packed document and the current page are held; then `total` is the page count.
|
|
22
|
+
*/
|
|
23
|
+
export declare function pwgRaster(pages: Iterable<GreyPage>, media: RasterMedia, total?: number): Uint8Array<ArrayBuffer>;
|
|
24
|
+
/** A PWG Raster document back as pages and their headers' key fields; for tests and for checking a file before it prints. */
|
|
25
|
+
export declare function readPwgRaster(file: Uint8Array): (GreyPage & {
|
|
26
|
+
dpi: number;
|
|
27
|
+
pageSizeName: string;
|
|
28
|
+
colorSpace: number;
|
|
29
|
+
bitsPerPixel: number;
|
|
30
|
+
})[];
|