@khalido/home 0.0.0-stage → 0.1.0
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 +25 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/clock.d.ts +17 -0
- package/dist/clock.js +43 -0
- package/dist/config.d.ts +273 -0
- package/dist/config.js +392 -0
- package/dist/connector.d.ts +34 -0
- package/dist/connector.js +47 -0
- package/dist/connectors/amber/client.d.ts +61 -0
- package/dist/connectors/amber/client.js +106 -0
- package/dist/connectors/amber/index.d.ts +30 -0
- package/dist/connectors/amber/index.js +94 -0
- package/dist/connectors/cast/codec.d.ts +38 -0
- package/dist/connectors/cast/codec.js +102 -0
- package/dist/connectors/cast/index.d.ts +60 -0
- package/dist/connectors/cast/index.js +426 -0
- package/dist/connectors/cast/schemas.d.ts +167 -0
- package/dist/connectors/cast/schemas.js +87 -0
- package/dist/connectors/cast/serve.d.ts +11 -0
- package/dist/connectors/cast/serve.js +57 -0
- package/dist/connectors/cast/state.d.ts +62 -0
- package/dist/connectors/cast/state.js +133 -0
- package/dist/connectors/cast/tls.d.ts +3 -0
- package/dist/connectors/cast/tls.js +67 -0
- package/dist/connectors/cast/writes.d.ts +80 -0
- package/dist/connectors/cast/writes.js +390 -0
- package/dist/connectors/daikin/client.d.ts +65 -0
- package/dist/connectors/daikin/client.js +221 -0
- package/dist/connectors/daikin/index.d.ts +41 -0
- package/dist/connectors/daikin/index.js +108 -0
- package/dist/connectors/enphase/client.d.ts +65 -0
- package/dist/connectors/enphase/client.js +173 -0
- package/dist/connectors/enphase/index.d.ts +43 -0
- package/dist/connectors/enphase/index.js +174 -0
- package/dist/connectors/enphase/schemas.d.ts +51 -0
- package/dist/connectors/enphase/schemas.js +34 -0
- package/dist/connectors/hue/client.d.ts +48 -0
- package/dist/connectors/hue/client.js +95 -0
- package/dist/connectors/hue/events.d.ts +41 -0
- package/dist/connectors/hue/events.js +156 -0
- package/dist/connectors/hue/index.d.ts +42 -0
- package/dist/connectors/hue/index.js +205 -0
- package/dist/connectors/hue/schemas.d.ts +65 -0
- package/dist/connectors/hue/schemas.js +48 -0
- package/dist/connectors/hue/state.d.ts +43 -0
- package/dist/connectors/hue/state.js +84 -0
- package/dist/connectors/printers/client.d.ts +28 -0
- package/dist/connectors/printers/client.js +74 -0
- package/dist/connectors/printers/codec.d.ts +76 -0
- package/dist/connectors/printers/codec.js +246 -0
- package/dist/connectors/printers/found.d.ts +15 -0
- package/dist/connectors/printers/found.js +35 -0
- package/dist/connectors/printers/index.d.ts +52 -0
- package/dist/connectors/printers/index.js +282 -0
- package/dist/connectors/printers/state.d.ts +60 -0
- package/dist/connectors/printers/state.js +99 -0
- package/dist/connectors/spotify/changes.d.ts +32 -0
- package/dist/connectors/spotify/changes.js +84 -0
- package/dist/connectors/spotify/client.d.ts +199 -0
- package/dist/connectors/spotify/client.js +270 -0
- package/dist/connectors/spotify/index.d.ts +92 -0
- package/dist/connectors/spotify/index.js +745 -0
- package/dist/connectors/up/client.d.ts +183 -0
- package/dist/connectors/up/client.js +308 -0
- package/dist/connectors/up/index.d.ts +91 -0
- package/dist/connectors/up/index.js +213 -0
- package/dist/connectors/up/spend.d.ts +78 -0
- package/dist/connectors/up/spend.js +213 -0
- package/dist/connectors/up/webhook.d.ts +13 -0
- package/dist/connectors/up/webhook.js +32 -0
- package/dist/connectors/weather/blocks.d.ts +51 -0
- package/dist/connectors/weather/blocks.js +106 -0
- package/dist/connectors/weather/bom.d.ts +37 -0
- package/dist/connectors/weather/bom.js +237 -0
- package/dist/connectors/weather/ftp.d.ts +5 -0
- package/dist/connectors/weather/ftp.js +46 -0
- package/dist/connectors/weather/google.d.ts +31 -0
- package/dist/connectors/weather/google.js +364 -0
- package/dist/connectors/weather/index.d.ts +53 -0
- package/dist/connectors/weather/index.js +267 -0
- package/dist/connectors/weather/open-meteo.d.ts +15 -0
- package/dist/connectors/weather/open-meteo.js +285 -0
- package/dist/connectors/weather/rain.d.ts +19 -0
- package/dist/connectors/weather/rain.js +51 -0
- package/dist/connectors/weather/source.d.ts +149 -0
- package/dist/connectors/weather/source.js +77 -0
- package/dist/connectors/weather/sun.d.ts +48 -0
- package/dist/connectors/weather/sun.js +65 -0
- package/dist/discover.d.ts +195 -0
- package/dist/discover.js +446 -0
- package/dist/energy.d.ts +61 -0
- package/dist/energy.js +162 -0
- package/dist/finders-entry.d.ts +1 -0
- package/dist/finders-entry.js +2 -0
- package/dist/finders.d.ts +45 -0
- package/dist/finders.js +173 -0
- package/dist/health.d.ts +28 -0
- package/dist/health.js +35 -0
- package/dist/home.d.ts +81 -0
- package/dist/home.js +114 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/lane.d.ts +17 -0
- package/dist/lane.js +74 -0
- package/dist/locate.d.ts +27 -0
- package/dist/locate.js +59 -0
- package/dist/manifest.d.ts +31 -0
- package/dist/manifest.js +4 -0
- package/dist/manifests.d.ts +5 -0
- package/dist/manifests.js +23 -0
- package/dist/node-fetch.d.ts +9 -0
- package/dist/node-fetch.js +25 -0
- package/dist/poll.d.ts +10 -0
- package/dist/poll.js +33 -0
- package/docs/cast.md +133 -0
- package/docs/cli.md +39 -0
- package/docs/config.md +36 -0
- package/docs/decisions.md +150 -0
- package/docs/devices.md +128 -0
- package/docs/enphase-notes.md +24 -0
- package/docs/google.md +92 -0
- package/docs/ideas.md +69 -0
- package/docs/matter.md +55 -0
- package/docs/performance.md +12 -0
- package/docs/philosophy.md +23 -0
- package/docs/up.md +84 -0
- package/docs/using.md +65 -0
- package/docs/weather-probe.md +63 -0
- package/examples/home.jsonc +74 -0
- package/examples/home.svg +76 -0
- package/home.schema.json +474 -0
- package/llms.txt +32 -0
- package/package.json +143 -4
- package/skills/draw-home/README.md +26 -0
- package/skills/draw-home/SKILL.md +82 -0
- package/skills/use-home/SKILL.md +86 -0
- package/src/cli/cache.ts +29 -0
- package/src/cli/cast.ts +172 -0
- package/src/cli/config.ts +52 -0
- package/src/cli/context.ts +118 -0
- package/src/cli/discover.ts +175 -0
- package/src/cli/enphase.ts +62 -0
- package/src/cli/hue.ts +124 -0
- package/src/cli/locate.ts +47 -0
- package/src/cli/main.ts +182 -0
- package/src/cli/printers.ts +21 -0
- package/src/cli/probe.ts +274 -0
- package/src/cli/spotify.ts +76 -0
- package/src/cli/status.ts +228 -0
- package/src/cli/up.ts +350 -0
- package/src/clock.ts +62 -0
- package/src/config.ts +578 -0
- package/src/connector.ts +77 -0
- package/src/connectors/amber/README.md +40 -0
- package/src/connectors/amber/client.ts +157 -0
- package/src/connectors/amber/index.ts +118 -0
- package/src/connectors/cast/README.md +43 -0
- package/src/connectors/cast/codec.ts +126 -0
- package/src/connectors/cast/index.ts +551 -0
- package/src/connectors/cast/schemas.ts +101 -0
- package/src/connectors/cast/serve.ts +72 -0
- package/src/connectors/cast/state.ts +203 -0
- package/src/connectors/cast/tls.ts +100 -0
- package/src/connectors/cast/writes.ts +572 -0
- package/src/connectors/daikin/README.md +78 -0
- package/src/connectors/daikin/client.ts +295 -0
- package/src/connectors/daikin/index.ts +145 -0
- package/src/connectors/enphase/README.md +64 -0
- package/src/connectors/enphase/client.ts +244 -0
- package/src/connectors/enphase/index.ts +241 -0
- package/src/connectors/enphase/schemas.ts +45 -0
- package/src/connectors/hue/README.md +50 -0
- package/src/connectors/hue/client.ts +157 -0
- package/src/connectors/hue/events.ts +182 -0
- package/src/connectors/hue/index.ts +261 -0
- package/src/connectors/hue/schemas.ts +60 -0
- package/src/connectors/hue/state.ts +124 -0
- package/src/connectors/printers/README.md +46 -0
- package/src/connectors/printers/client.ts +114 -0
- package/src/connectors/printers/codec.ts +292 -0
- package/src/connectors/printers/found.ts +46 -0
- package/src/connectors/printers/index.ts +385 -0
- package/src/connectors/printers/state.ts +182 -0
- package/src/connectors/spotify/README.md +80 -0
- package/src/connectors/spotify/changes.ts +127 -0
- package/src/connectors/spotify/client.ts +387 -0
- package/src/connectors/spotify/index.ts +929 -0
- package/src/connectors/up/README.md +35 -0
- package/src/connectors/up/client.ts +518 -0
- package/src/connectors/up/index.ts +370 -0
- package/src/connectors/up/spend.ts +280 -0
- package/src/connectors/up/webhook.ts +62 -0
- package/src/connectors/weather/README.md +84 -0
- package/src/connectors/weather/blocks.ts +173 -0
- package/src/connectors/weather/bom.ts +344 -0
- package/src/connectors/weather/ftp.ts +48 -0
- package/src/connectors/weather/google.ts +425 -0
- package/src/connectors/weather/index.ts +380 -0
- package/src/connectors/weather/open-meteo.ts +309 -0
- package/src/connectors/weather/rain.ts +73 -0
- package/src/connectors/weather/source.ts +236 -0
- package/src/connectors/weather/sun.ts +114 -0
- package/src/discover.ts +634 -0
- package/src/energy.ts +221 -0
- package/src/finders-entry.ts +2 -0
- package/src/finders.ts +247 -0
- package/src/health.ts +74 -0
- package/src/home.ts +205 -0
- package/src/index.ts +62 -0
- package/src/lane.ts +110 -0
- package/src/locate.ts +85 -0
- package/src/manifest.ts +43 -0
- package/src/manifests.ts +27 -0
- package/src/node-fetch.ts +41 -0
- package/src/poll.ts +33 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** The resource kinds this library reads. */
|
|
3
|
+
export declare const KINDS: readonly ["light", "room", "zone", "grouped_light", "scene", "bridge_home", "zigbee_connectivity"];
|
|
4
|
+
export type Kind = (typeof KINDS)[number];
|
|
5
|
+
/** One CLIP v2 resource, loosely typed: id and type, plus the optional fields the snapshot uses. */
|
|
6
|
+
export declare const Resource: z.ZodObject<{
|
|
7
|
+
id: z.ZodString;
|
|
8
|
+
type: z.ZodString;
|
|
9
|
+
owner: z.ZodOptional<z.ZodObject<{
|
|
10
|
+
rid: z.ZodString;
|
|
11
|
+
rtype: z.ZodString;
|
|
12
|
+
}, z.core.$strip>>;
|
|
13
|
+
metadata: z.ZodOptional<z.ZodObject<{
|
|
14
|
+
name: z.ZodOptional<z.ZodString>;
|
|
15
|
+
}, z.core.$loose>>;
|
|
16
|
+
on: z.ZodOptional<z.ZodObject<{
|
|
17
|
+
on: z.ZodBoolean;
|
|
18
|
+
}, z.core.$loose>>;
|
|
19
|
+
dimming: z.ZodOptional<z.ZodObject<{
|
|
20
|
+
brightness: z.ZodNumber;
|
|
21
|
+
}, z.core.$loose>>;
|
|
22
|
+
children: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
23
|
+
rid: z.ZodString;
|
|
24
|
+
rtype: z.ZodString;
|
|
25
|
+
}, z.core.$strip>>>;
|
|
26
|
+
services: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
27
|
+
rid: z.ZodString;
|
|
28
|
+
rtype: z.ZodString;
|
|
29
|
+
}, z.core.$strip>>>;
|
|
30
|
+
group: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
rid: z.ZodString;
|
|
32
|
+
rtype: z.ZodString;
|
|
33
|
+
}, z.core.$strip>>;
|
|
34
|
+
status: z.ZodOptional<z.ZodUnknown>;
|
|
35
|
+
}, z.core.$loose>;
|
|
36
|
+
export type Resource = z.infer<typeof Resource>;
|
|
37
|
+
/** The CLIP v2 response envelope: `{ errors, data }`. */
|
|
38
|
+
export declare const Envelope: z.ZodObject<{
|
|
39
|
+
errors: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
40
|
+
description: z.ZodString;
|
|
41
|
+
}, z.core.$strip>>>;
|
|
42
|
+
data: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
|
|
43
|
+
}, z.core.$strip>;
|
|
44
|
+
/** One event-stream batch. `data` holds partial resources: only the fields that changed. */
|
|
45
|
+
export declare const HueEvent: z.ZodObject<{
|
|
46
|
+
id: z.ZodString;
|
|
47
|
+
type: z.ZodString;
|
|
48
|
+
creationtime: z.ZodString;
|
|
49
|
+
data: z.ZodArray<z.ZodObject<{
|
|
50
|
+
id: z.ZodString;
|
|
51
|
+
type: z.ZodString;
|
|
52
|
+
}, z.core.$loose>>;
|
|
53
|
+
}, z.core.$strip>;
|
|
54
|
+
export type HueEvent = z.infer<typeof HueEvent>;
|
|
55
|
+
/** `POST /api` answers with a one-element array holding `success` or `error` (v1 shape). */
|
|
56
|
+
export declare const PairResponse: z.ZodArray<z.ZodObject<{
|
|
57
|
+
success: z.ZodOptional<z.ZodObject<{
|
|
58
|
+
username: z.ZodString;
|
|
59
|
+
clientkey: z.ZodOptional<z.ZodString>;
|
|
60
|
+
}, z.core.$strip>>;
|
|
61
|
+
error: z.ZodOptional<z.ZodObject<{
|
|
62
|
+
type: z.ZodNumber;
|
|
63
|
+
description: z.ZodString;
|
|
64
|
+
}, z.core.$strip>>;
|
|
65
|
+
}, z.core.$strip>>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Shapes from Signify's CLIP v2, checked against openhue-api's OpenAPI spec (Apache-2.0,
|
|
3
|
+
// github.com/openhue/openhue-api, 2026-10). Signify's own reference is login-gated, so every
|
|
4
|
+
// field is unconfirmed from the primary source. Only the fields the snapshot reads are parsed;
|
|
5
|
+
// all are optional and unknown fields pass through, so a schema change degrades, never throws.
|
|
6
|
+
const ref = z.object({ rid: z.string(), rtype: z.string() });
|
|
7
|
+
/** The resource kinds this library reads. */
|
|
8
|
+
export const KINDS = [
|
|
9
|
+
"light",
|
|
10
|
+
"room",
|
|
11
|
+
"zone",
|
|
12
|
+
"grouped_light",
|
|
13
|
+
"scene",
|
|
14
|
+
"bridge_home",
|
|
15
|
+
"zigbee_connectivity",
|
|
16
|
+
];
|
|
17
|
+
/** One CLIP v2 resource, loosely typed: id and type, plus the optional fields the snapshot uses. */
|
|
18
|
+
export const Resource = z.looseObject({
|
|
19
|
+
id: z.string(),
|
|
20
|
+
type: z.string(),
|
|
21
|
+
owner: ref.optional(),
|
|
22
|
+
metadata: z.looseObject({ name: z.string().optional() }).optional(),
|
|
23
|
+
on: z.looseObject({ on: z.boolean() }).optional(),
|
|
24
|
+
dimming: z.looseObject({ brightness: z.number() }).optional(),
|
|
25
|
+
children: z.array(ref).optional(),
|
|
26
|
+
services: z.array(ref).optional(),
|
|
27
|
+
group: ref.optional(), // scene: the room or zone it belongs to
|
|
28
|
+
status: z.unknown().optional(), // scene: { active }; zigbee_connectivity: "connected" | …
|
|
29
|
+
});
|
|
30
|
+
/** The CLIP v2 response envelope: `{ errors, data }`. */
|
|
31
|
+
export const Envelope = z.object({
|
|
32
|
+
errors: z.array(z.object({ description: z.string() })).default([]),
|
|
33
|
+
data: z.array(z.unknown()).default([]),
|
|
34
|
+
});
|
|
35
|
+
/** One event-stream batch. `data` holds partial resources: only the fields that changed. */
|
|
36
|
+
export const HueEvent = z.object({
|
|
37
|
+
id: z.string(),
|
|
38
|
+
type: z.string(), // "add" | "update" | "delete" | "error"; anything newer is ignored, not fatal
|
|
39
|
+
creationtime: z.string(),
|
|
40
|
+
data: z.array(z.looseObject({ id: z.string(), type: z.string() })),
|
|
41
|
+
});
|
|
42
|
+
/** `POST /api` answers with a one-element array holding `success` or `error` (v1 shape). */
|
|
43
|
+
export const PairResponse = z.array(z.object({
|
|
44
|
+
success: z
|
|
45
|
+
.object({ username: z.string(), clientkey: z.string().optional() })
|
|
46
|
+
.optional(),
|
|
47
|
+
error: z.object({ type: z.number(), description: z.string() }).optional(),
|
|
48
|
+
}));
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { HueEvent, Resource } from "./schemas.ts";
|
|
2
|
+
/** 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
|
+
export type HueSnapshot = {
|
|
4
|
+
resources: Record<string, Resource>;
|
|
5
|
+
asOf: string;
|
|
6
|
+
lastEventId?: string;
|
|
7
|
+
lastError?: HueEvent;
|
|
8
|
+
};
|
|
9
|
+
/** A snapshot from one full `get()`. */
|
|
10
|
+
export declare function snapshotFrom(resources: Resource[], asOf: string): HueSnapshot;
|
|
11
|
+
/** Apply one event batch. Pure: returns a new snapshot. Updates carry only changed fields, so they merge; unknown types are ignored. */
|
|
12
|
+
export declare function reduce(snapshot: HueSnapshot, event: HueEvent): HueSnapshot;
|
|
13
|
+
/** A light as a row shows it. `reachable` is undefined when the bridge has no connectivity record for it. */
|
|
14
|
+
export type LightView = {
|
|
15
|
+
id: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
on?: boolean;
|
|
18
|
+
brightness?: number;
|
|
19
|
+
reachable?: boolean;
|
|
20
|
+
};
|
|
21
|
+
/** Every light, with on, brightness and reachability. */
|
|
22
|
+
export declare function lights(s: HueSnapshot): LightView[];
|
|
23
|
+
/** A room or zone as a row shows it, with its grouped light's state. */
|
|
24
|
+
export type GroupView = {
|
|
25
|
+
id: string;
|
|
26
|
+
type: "room" | "zone";
|
|
27
|
+
name?: string;
|
|
28
|
+
groupedLightId?: string;
|
|
29
|
+
on?: boolean;
|
|
30
|
+
brightness?: number;
|
|
31
|
+
lightIds: string[];
|
|
32
|
+
};
|
|
33
|
+
/** Rooms and zones. A room's children are devices; a zone's are lights. */
|
|
34
|
+
export declare function groups(s: HueSnapshot): GroupView[];
|
|
35
|
+
/** The grouped light for the whole home: one PUT `{ on: { on: false } }` to it is "Lights off". */
|
|
36
|
+
export declare function homeGroupedLightId(s: HueSnapshot): string | undefined;
|
|
37
|
+
/** Scenes with their room or zone and whether one is active (`status.active` other than "inactive"). */
|
|
38
|
+
export declare function scenes(s: HueSnapshot): {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string | undefined;
|
|
41
|
+
groupId: string | undefined;
|
|
42
|
+
active: boolean;
|
|
43
|
+
}[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** A snapshot from one full `get()`. */
|
|
2
|
+
export function snapshotFrom(resources, asOf) {
|
|
3
|
+
return {
|
|
4
|
+
resources: Object.fromEntries(resources.map((r) => [r.id, r])),
|
|
5
|
+
asOf,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** Apply one event batch. Pure: returns a new snapshot. Updates carry only changed fields, so they merge; unknown types are ignored. */
|
|
9
|
+
export function reduce(snapshot, event) {
|
|
10
|
+
if (event.type === "error")
|
|
11
|
+
return { ...snapshot, lastError: event };
|
|
12
|
+
if (!["add", "update", "delete"].includes(event.type))
|
|
13
|
+
return snapshot;
|
|
14
|
+
const resources = { ...snapshot.resources };
|
|
15
|
+
for (const item of event.data) {
|
|
16
|
+
const prev = resources[item.id];
|
|
17
|
+
if (event.type === "delete")
|
|
18
|
+
delete resources[item.id];
|
|
19
|
+
else
|
|
20
|
+
resources[item.id] = (event.type === "update" && prev ? merge(prev, item) : item);
|
|
21
|
+
}
|
|
22
|
+
return { ...snapshot, resources };
|
|
23
|
+
}
|
|
24
|
+
const isPlain = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
25
|
+
function merge(a, b) {
|
|
26
|
+
const out = { ...a };
|
|
27
|
+
for (const [k, v] of Object.entries(b))
|
|
28
|
+
out[k] = isPlain(v) && isPlain(a[k]) ? merge(a[k], v) : v;
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
const ofType = (s, type) => Object.values(s.resources).filter((r) => r.type === type);
|
|
32
|
+
const groupedLightOf = (s, r) => r.services
|
|
33
|
+
?.map((x) => s.resources[x.rid])
|
|
34
|
+
.find((x) => x?.type === "grouped_light");
|
|
35
|
+
/** Every light, with on, brightness and reachability. */
|
|
36
|
+
export function lights(s) {
|
|
37
|
+
const zigbee = ofType(s, "zigbee_connectivity");
|
|
38
|
+
return ofType(s, "light").map((l) => {
|
|
39
|
+
const link = zigbee.find((z) => z.owner?.rid === l.owner?.rid);
|
|
40
|
+
return {
|
|
41
|
+
id: l.id,
|
|
42
|
+
name: l.metadata?.name,
|
|
43
|
+
on: l.on?.on,
|
|
44
|
+
brightness: l.dimming?.brightness,
|
|
45
|
+
reachable: link ? link.status === "connected" : undefined,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** Rooms and zones. A room's children are devices; a zone's are lights. */
|
|
50
|
+
export function groups(s) {
|
|
51
|
+
const all = ofType(s, "light");
|
|
52
|
+
return [...ofType(s, "room"), ...ofType(s, "zone")].map((g) => {
|
|
53
|
+
const kids = new Set(g.children?.map((c) => c.rid));
|
|
54
|
+
const grouped = groupedLightOf(s, g);
|
|
55
|
+
return {
|
|
56
|
+
id: g.id,
|
|
57
|
+
type: g.type,
|
|
58
|
+
name: g.metadata?.name,
|
|
59
|
+
groupedLightId: grouped?.id,
|
|
60
|
+
on: grouped?.on?.on,
|
|
61
|
+
brightness: grouped?.dimming?.brightness,
|
|
62
|
+
lightIds: all
|
|
63
|
+
.filter((l) => kids.has(l.id) || kids.has(l.owner?.rid ?? ""))
|
|
64
|
+
.map((l) => l.id),
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** The grouped light for the whole home: one PUT `{ on: { on: false } }` to it is "Lights off". */
|
|
69
|
+
export function homeGroupedLightId(s) {
|
|
70
|
+
const home = ofType(s, "bridge_home")[0];
|
|
71
|
+
return home && groupedLightOf(s, home)?.id;
|
|
72
|
+
}
|
|
73
|
+
/** Scenes with their room or zone and whether one is active (`status.active` other than "inactive"). */
|
|
74
|
+
export function scenes(s) {
|
|
75
|
+
return ofType(s, "scene").map((sc) => {
|
|
76
|
+
const active = sc.status?.active;
|
|
77
|
+
return {
|
|
78
|
+
id: sc.id,
|
|
79
|
+
name: sc.metadata?.name,
|
|
80
|
+
groupId: sc.group?.rid,
|
|
81
|
+
active: active !== undefined && active !== "inactive",
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type IppMessage } from "./codec.ts";
|
|
2
|
+
/** Everything the integration reads from one printer. */
|
|
3
|
+
export declare const REQUESTED: string[];
|
|
4
|
+
/** Where a printer answers: IPP is HTTP POST to `/<rp>`; port 443 (or a found `_ipps._tcp`) is TLS. */
|
|
5
|
+
export type PrinterAddress = {
|
|
6
|
+
host: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
rp?: string;
|
|
9
|
+
secure?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/** Most printers answer on this path (IPP Everywhere); the TXT `rp` says when theirs differs. */
|
|
12
|
+
export declare const DEFAULT_RP = "ipp/print";
|
|
13
|
+
/** The URL to POST to and the `ipp(s)://` URI the request names. A TXT `rp` may or may not start with a slash. */
|
|
14
|
+
export declare function printerUrl(a: PrinterAddress): {
|
|
15
|
+
http: string;
|
|
16
|
+
ipp: string;
|
|
17
|
+
secure: boolean;
|
|
18
|
+
path: string;
|
|
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?: {
|
|
25
|
+
requestId?: number;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}): Promise<IppMessage>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ConnectorError, fetchTimeout } from "../../connector.js";
|
|
2
|
+
import { attributesOf, decodeMessage, getPrinterAttributesRequest, } from "./codec.js";
|
|
3
|
+
// Pure reads: a `fetch`, an address and a path come in, the printer's attributes go out. No writes here yet.
|
|
4
|
+
// TODO(writes, behind KO's hand): Identify-Printer (0x003C, a beep) first, then Print-Job (0x0002) with application/pdf
|
|
5
|
+
// where `document-format-supported` lists it. Each is one more request builder in codec.ts and one function here.
|
|
6
|
+
/** Everything the integration reads from one printer. */
|
|
7
|
+
export const REQUESTED = [
|
|
8
|
+
"printer-state",
|
|
9
|
+
"printer-state-reasons",
|
|
10
|
+
"printer-state-message",
|
|
11
|
+
"marker-names",
|
|
12
|
+
"marker-levels",
|
|
13
|
+
"marker-types",
|
|
14
|
+
"marker-colors",
|
|
15
|
+
"media-ready",
|
|
16
|
+
"queued-job-count",
|
|
17
|
+
"printer-alert",
|
|
18
|
+
"printer-make-and-model",
|
|
19
|
+
"printer-uuid",
|
|
20
|
+
"document-format-supported",
|
|
21
|
+
"ipp-versions-supported",
|
|
22
|
+
"printer-is-accepting-jobs",
|
|
23
|
+
];
|
|
24
|
+
/** Most printers answer on this path (IPP Everywhere); the TXT `rp` says when theirs differs. */
|
|
25
|
+
export const DEFAULT_RP = "ipp/print";
|
|
26
|
+
/** The URL to POST to and the `ipp(s)://` URI the request names. A TXT `rp` may or may not start with a slash. */
|
|
27
|
+
export function printerUrl(a) {
|
|
28
|
+
const secure = a.secure ?? a.port === 443;
|
|
29
|
+
const port = a.port ?? (secure ? 443 : 631);
|
|
30
|
+
const path = (a.rp || DEFAULT_RP).replace(/^\/+/, "");
|
|
31
|
+
const host = a.host.includes(":") ? `[${a.host}]` : a.host;
|
|
32
|
+
return {
|
|
33
|
+
http: `${secure ? "https" : "http"}://${host}:${port}/${path}`,
|
|
34
|
+
ipp: `${secure ? "ipps" : "ipp"}://${host}:${port}/${path}`,
|
|
35
|
+
secure,
|
|
36
|
+
path,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** A few status codes by name, for the error text; any other shows as hex. */
|
|
40
|
+
const STATUS = {
|
|
41
|
+
0x0400: "client-error-bad-request",
|
|
42
|
+
0x0401: "client-error-forbidden",
|
|
43
|
+
0x0402: "client-error-not-authenticated",
|
|
44
|
+
0x0403: "client-error-not-authorized",
|
|
45
|
+
0x0404: "client-error-not-possible",
|
|
46
|
+
0x0406: "client-error-not-found",
|
|
47
|
+
0x040a: "client-error-document-format-not-supported",
|
|
48
|
+
0x0500: "server-error-internal-error",
|
|
49
|
+
0x0501: "server-error-operation-not-supported",
|
|
50
|
+
0x0503: "server-error-service-unavailable",
|
|
51
|
+
0x0507: "server-error-busy",
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Get-Printer-Attributes over HTTP, through `fetchTimeout`. A non-ok HTTP status or an IPP status of 0x0100 and up throws a
|
|
55
|
+
* ConnectorError saying which. Returns the whole response; `attributesOf` gives the printer's attributes.
|
|
56
|
+
*/
|
|
57
|
+
export async function getPrinterAttributes(fetcher, address, options = {}) {
|
|
58
|
+
const url = printerUrl(address);
|
|
59
|
+
const res = await fetchTimeout(fetcher, url.http, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "content-type": "application/ipp", accept: "application/ipp" },
|
|
62
|
+
body: getPrinterAttributesRequest(url.ipp, REQUESTED, options.requestId),
|
|
63
|
+
}, options);
|
|
64
|
+
if (!res.ok)
|
|
65
|
+
throw new ConnectorError(`HTTP ${res.status} on POST /${url.path}`, {
|
|
66
|
+
status: res.status,
|
|
67
|
+
});
|
|
68
|
+
const message = decodeMessage(new Uint8Array(await res.arrayBuffer()));
|
|
69
|
+
if (message.code >= 0x0100) {
|
|
70
|
+
const why = attributesOf(message, 1)["status-message"]?.[0];
|
|
71
|
+
throw new ConnectorError(`IPP ${STATUS[message.code] ?? `status 0x${message.code.toString(16).padStart(4, "0")}`}${typeof why === "string" ? `: ${why}` : ""}`, { status: message.code, code: "ipp" });
|
|
72
|
+
}
|
|
73
|
+
return message;
|
|
74
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Delimiter tags open a group; value tags (0x10 and up) carry one value. Only the ones this package reads or writes are named. */
|
|
2
|
+
export declare const TAG: {
|
|
3
|
+
readonly operation: 1;
|
|
4
|
+
readonly job: 2;
|
|
5
|
+
readonly end: 3;
|
|
6
|
+
readonly printer: 4;
|
|
7
|
+
readonly unsupported: 5;
|
|
8
|
+
readonly integer: 33;
|
|
9
|
+
readonly boolean: 34;
|
|
10
|
+
readonly enum: 35;
|
|
11
|
+
readonly octetString: 48;
|
|
12
|
+
readonly dateTime: 49;
|
|
13
|
+
readonly resolution: 50;
|
|
14
|
+
readonly rangeOfInteger: 51;
|
|
15
|
+
readonly begCollection: 52;
|
|
16
|
+
readonly endCollection: 55;
|
|
17
|
+
readonly textWithoutLanguage: 65;
|
|
18
|
+
readonly nameWithoutLanguage: 66;
|
|
19
|
+
readonly keyword: 68;
|
|
20
|
+
readonly uri: 69;
|
|
21
|
+
readonly uriScheme: 70;
|
|
22
|
+
readonly charset: 71;
|
|
23
|
+
readonly naturalLanguage: 72;
|
|
24
|
+
readonly mimeMediaType: 73;
|
|
25
|
+
};
|
|
26
|
+
/** The operation ids this package sends. Identify-Printer (0x003C) and Print-Job (0x0002) are the writes that come later. */
|
|
27
|
+
export declare const OPERATION: {
|
|
28
|
+
readonly getPrinterAttributes: 11;
|
|
29
|
+
};
|
|
30
|
+
export type IppRange = {
|
|
31
|
+
lower: number;
|
|
32
|
+
upper: number;
|
|
33
|
+
};
|
|
34
|
+
export type IppResolution = {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
units: "dpi" | "dpcm" | number;
|
|
38
|
+
};
|
|
39
|
+
/** A value whose tag this decoder does not read (a language-tagged string, a collection, an out-of-band marker), or whose length is wrong for its tag: the bytes, unchanged. */
|
|
40
|
+
export type IppUnknown = {
|
|
41
|
+
tag: number;
|
|
42
|
+
bytes: Uint8Array;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* One decoded value. Integers and enums are numbers; every string tag is a string; a dateTime is an ISO 8601 string with
|
|
46
|
+
* its offset; an octetString is bytes. A collection decodes to one `IppUnknown` with no bytes, its members skipped.
|
|
47
|
+
*/
|
|
48
|
+
export type IppValue = number | boolean | string | Uint8Array | IppRange | IppResolution | IppUnknown;
|
|
49
|
+
export type IppAttribute = {
|
|
50
|
+
name: string;
|
|
51
|
+
tag: number;
|
|
52
|
+
values: IppValue[];
|
|
53
|
+
};
|
|
54
|
+
export type IppGroup = {
|
|
55
|
+
tag: number;
|
|
56
|
+
attributes: IppAttribute[];
|
|
57
|
+
};
|
|
58
|
+
/** A request or a response: `code` is the operation id of a request or the status code of a response. */
|
|
59
|
+
export type IppMessage = {
|
|
60
|
+
version: [number, number];
|
|
61
|
+
code: number;
|
|
62
|
+
requestId: number;
|
|
63
|
+
groups: IppGroup[];
|
|
64
|
+
data?: Uint8Array;
|
|
65
|
+
};
|
|
66
|
+
/** A message as bytes. Collections are not encoded (a Get-Printer-Attributes request has none). */
|
|
67
|
+
export declare function encodeMessage(m: IppMessage): Uint8Array<ArrayBuffer>;
|
|
68
|
+
/** The Get-Printer-Attributes request: IPP 2.0, utf-8, English, the printer's `ipp://` URI, and the attributes wanted. */
|
|
69
|
+
export declare function getPrinterAttributesRequest(printerUri: string, requested: string[], requestId?: number): Uint8Array<ArrayBuffer>;
|
|
70
|
+
/**
|
|
71
|
+
* A message's bytes as groups of attributes. An unknown or malformed value is kept as `IppUnknown`, never thrown; only a
|
|
72
|
+
* message that cannot be walked (cut short, a value before any attribute) throws. A collection and its members are skipped.
|
|
73
|
+
*/
|
|
74
|
+
export declare function decodeMessage(bytes: Uint8Array): IppMessage;
|
|
75
|
+
/** Every attribute of the groups with this tag (default: the printer's), by name, repeats merged. */
|
|
76
|
+
export declare function attributesOf(m: IppMessage, groupTag?: number): Record<string, IppValue[]>;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// IPP's binary encoding (RFC 8010) by hand, enough for Get-Printer-Attributes: a request out, a response in. A message is
|
|
2
|
+
// version (2 bytes), operation or status code (2), request id (4), then attribute groups, then 0x03, then any document data.
|
|
3
|
+
// Each attribute is a value tag, a name (length + bytes) and a value (length + bytes); a further value of the same attribute
|
|
4
|
+
// repeats the tag with a zero-length name. Both directions share the one layout, so one encoder and one decoder.
|
|
5
|
+
/** Delimiter tags open a group; value tags (0x10 and up) carry one value. Only the ones this package reads or writes are named. */
|
|
6
|
+
export const TAG = {
|
|
7
|
+
operation: 0x01,
|
|
8
|
+
job: 0x02,
|
|
9
|
+
end: 0x03,
|
|
10
|
+
printer: 0x04,
|
|
11
|
+
unsupported: 0x05,
|
|
12
|
+
integer: 0x21,
|
|
13
|
+
boolean: 0x22,
|
|
14
|
+
enum: 0x23,
|
|
15
|
+
octetString: 0x30,
|
|
16
|
+
dateTime: 0x31,
|
|
17
|
+
resolution: 0x32,
|
|
18
|
+
rangeOfInteger: 0x33,
|
|
19
|
+
begCollection: 0x34,
|
|
20
|
+
endCollection: 0x37,
|
|
21
|
+
textWithoutLanguage: 0x41,
|
|
22
|
+
nameWithoutLanguage: 0x42,
|
|
23
|
+
keyword: 0x44,
|
|
24
|
+
uri: 0x45,
|
|
25
|
+
uriScheme: 0x46,
|
|
26
|
+
charset: 0x47,
|
|
27
|
+
naturalLanguage: 0x48,
|
|
28
|
+
mimeMediaType: 0x49,
|
|
29
|
+
};
|
|
30
|
+
/** The operation ids this package sends. Identify-Printer (0x003C) and Print-Job (0x0002) are the writes that come later. */
|
|
31
|
+
export const OPERATION = { getPrinterAttributes: 0x000b };
|
|
32
|
+
const STRING_TAGS = new Set([
|
|
33
|
+
TAG.textWithoutLanguage,
|
|
34
|
+
TAG.nameWithoutLanguage,
|
|
35
|
+
TAG.keyword,
|
|
36
|
+
TAG.uri,
|
|
37
|
+
TAG.uriScheme,
|
|
38
|
+
TAG.charset,
|
|
39
|
+
TAG.naturalLanguage,
|
|
40
|
+
TAG.mimeMediaType,
|
|
41
|
+
]);
|
|
42
|
+
const utf8 = new TextEncoder();
|
|
43
|
+
const text = new TextDecoder();
|
|
44
|
+
const u16 = (n) => [(n >> 8) & 255, n & 255];
|
|
45
|
+
const u32 = (n) => [
|
|
46
|
+
(n >>> 24) & 255,
|
|
47
|
+
(n >>> 16) & 255,
|
|
48
|
+
(n >>> 8) & 255,
|
|
49
|
+
n & 255,
|
|
50
|
+
];
|
|
51
|
+
function encodeValue(tag, value) {
|
|
52
|
+
if (STRING_TAGS.has(tag))
|
|
53
|
+
return [...utf8.encode(String(value))];
|
|
54
|
+
if (tag === TAG.integer || tag === TAG.enum)
|
|
55
|
+
return u32(Number(value));
|
|
56
|
+
if (tag === TAG.boolean)
|
|
57
|
+
return [value ? 1 : 0];
|
|
58
|
+
if (tag === TAG.rangeOfInteger)
|
|
59
|
+
return [
|
|
60
|
+
...u32(value.lower),
|
|
61
|
+
...u32(value.upper),
|
|
62
|
+
];
|
|
63
|
+
if (tag === TAG.resolution) {
|
|
64
|
+
const r = value;
|
|
65
|
+
return [
|
|
66
|
+
...u32(r.x),
|
|
67
|
+
...u32(r.y),
|
|
68
|
+
r.units === "dpi" ? 3 : r.units === "dpcm" ? 4 : Number(r.units),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
if (tag === TAG.dateTime) {
|
|
72
|
+
const m = String(value).match(/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)\.(\d)([+-])(\d\d):(\d\d)$/);
|
|
73
|
+
if (!m)
|
|
74
|
+
throw new Error(`ipp: a dateTime is YYYY-MM-DDTHH:MM:SS.d+HH:MM, got ${String(value)}`);
|
|
75
|
+
const n = m.slice(1).map((x, i) => (i === 7 ? x.charCodeAt(0) : Number(x)));
|
|
76
|
+
return [...u16(n[0]), ...n.slice(1)];
|
|
77
|
+
}
|
|
78
|
+
if (value instanceof Uint8Array)
|
|
79
|
+
return [...value];
|
|
80
|
+
if (typeof value === "object" && "bytes" in value)
|
|
81
|
+
return [...value.bytes];
|
|
82
|
+
throw new Error(`ipp: cannot encode tag 0x${tag.toString(16)}`);
|
|
83
|
+
}
|
|
84
|
+
/** A message as bytes. Collections are not encoded (a Get-Printer-Attributes request has none). */
|
|
85
|
+
export function encodeMessage(m) {
|
|
86
|
+
const out = [...m.version, ...u16(m.code), ...u32(m.requestId)];
|
|
87
|
+
for (const g of m.groups) {
|
|
88
|
+
out.push(g.tag);
|
|
89
|
+
for (const a of g.attributes)
|
|
90
|
+
a.values.forEach((v, i) => {
|
|
91
|
+
const name = i === 0 ? utf8.encode(a.name) : new Uint8Array();
|
|
92
|
+
const value = encodeValue(a.tag, v);
|
|
93
|
+
out.push(a.tag, ...u16(name.length), ...name, ...u16(value.length), ...value);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
out.push(TAG.end);
|
|
97
|
+
return Uint8Array.from([...out, ...(m.data ?? [])]);
|
|
98
|
+
}
|
|
99
|
+
/** The Get-Printer-Attributes request: IPP 2.0, utf-8, English, the printer's `ipp://` URI, and the attributes wanted. */
|
|
100
|
+
export function getPrinterAttributesRequest(printerUri, requested, requestId = 1) {
|
|
101
|
+
return encodeMessage({
|
|
102
|
+
version: [2, 0],
|
|
103
|
+
code: OPERATION.getPrinterAttributes,
|
|
104
|
+
requestId,
|
|
105
|
+
groups: [
|
|
106
|
+
{
|
|
107
|
+
tag: TAG.operation,
|
|
108
|
+
attributes: [
|
|
109
|
+
{ name: "attributes-charset", tag: TAG.charset, values: ["utf-8"] },
|
|
110
|
+
{
|
|
111
|
+
name: "attributes-natural-language",
|
|
112
|
+
tag: TAG.naturalLanguage,
|
|
113
|
+
values: ["en"],
|
|
114
|
+
},
|
|
115
|
+
{ name: "printer-uri", tag: TAG.uri, values: [printerUri] },
|
|
116
|
+
{ name: "requested-attributes", tag: TAG.keyword, values: requested },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const fail = (why) => {
|
|
123
|
+
throw new Error(`ipp: ${why}`);
|
|
124
|
+
};
|
|
125
|
+
function decodeValue(tag, bytes) {
|
|
126
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
|
|
127
|
+
const unknown = { tag, bytes };
|
|
128
|
+
if (STRING_TAGS.has(tag))
|
|
129
|
+
return text.decode(bytes);
|
|
130
|
+
if (tag === TAG.octetString)
|
|
131
|
+
return bytes;
|
|
132
|
+
if (tag === TAG.integer || tag === TAG.enum)
|
|
133
|
+
return bytes.length === 4 ? view.getInt32(0) : unknown;
|
|
134
|
+
if (tag === TAG.boolean)
|
|
135
|
+
return bytes.length === 1 ? bytes[0] !== 0 : unknown;
|
|
136
|
+
if (tag === TAG.rangeOfInteger)
|
|
137
|
+
return bytes.length === 8
|
|
138
|
+
? { lower: view.getInt32(0), upper: view.getInt32(4) }
|
|
139
|
+
: unknown;
|
|
140
|
+
if (tag === TAG.resolution) {
|
|
141
|
+
if (bytes.length !== 9)
|
|
142
|
+
return unknown;
|
|
143
|
+
const units = bytes[8];
|
|
144
|
+
return {
|
|
145
|
+
x: view.getInt32(0),
|
|
146
|
+
y: view.getInt32(4),
|
|
147
|
+
units: units === 3 ? "dpi" : units === 4 ? "dpcm" : units,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (tag === TAG.dateTime) {
|
|
151
|
+
if (bytes.length !== 11)
|
|
152
|
+
return unknown;
|
|
153
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
154
|
+
const [mo, d, h, mi, s, ds, dir, uh, um] = [2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => bytes[i]);
|
|
155
|
+
return `${view.getUint16(0)}-${p(mo)}-${p(d)}T${p(h)}:${p(mi)}:${p(s)}.${ds}${String.fromCharCode(dir)}${p(uh)}:${p(um)}`;
|
|
156
|
+
}
|
|
157
|
+
return unknown;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* A message's bytes as groups of attributes. An unknown or malformed value is kept as `IppUnknown`, never thrown; only a
|
|
161
|
+
* message that cannot be walked (cut short, a value before any attribute) throws. A collection and its members are skipped.
|
|
162
|
+
*/
|
|
163
|
+
export function decodeMessage(bytes) {
|
|
164
|
+
if (bytes.length < 9)
|
|
165
|
+
fail("a message is at least 9 bytes");
|
|
166
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
|
|
167
|
+
let i = 8;
|
|
168
|
+
const need = (n) => i + n <= bytes.length || fail("the message is cut short");
|
|
169
|
+
/** One attribute element at `i`: tag, name, value bytes. */
|
|
170
|
+
const element = () => {
|
|
171
|
+
need(3);
|
|
172
|
+
const tag = bytes[i++];
|
|
173
|
+
const nameLength = view.getUint16(i);
|
|
174
|
+
i += 2;
|
|
175
|
+
need(nameLength + 2);
|
|
176
|
+
const name = text.decode(bytes.subarray(i, i + nameLength));
|
|
177
|
+
i += nameLength;
|
|
178
|
+
const valueLength = view.getUint16(i);
|
|
179
|
+
i += 2;
|
|
180
|
+
need(valueLength);
|
|
181
|
+
const value = bytes.subarray(i, i + valueLength);
|
|
182
|
+
i += valueLength;
|
|
183
|
+
return { tag, name, value };
|
|
184
|
+
};
|
|
185
|
+
const groups = [];
|
|
186
|
+
let group;
|
|
187
|
+
let last;
|
|
188
|
+
for (;;) {
|
|
189
|
+
need(1);
|
|
190
|
+
const tag = bytes[i];
|
|
191
|
+
if (tag === TAG.end) {
|
|
192
|
+
i++;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
if (tag < 0x10) {
|
|
196
|
+
i++;
|
|
197
|
+
groups.push((group = { tag, attributes: [] }));
|
|
198
|
+
last = undefined;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const e = element();
|
|
202
|
+
let value;
|
|
203
|
+
if (e.tag === TAG.begCollection) {
|
|
204
|
+
for (let depth = 1; depth > 0;) {
|
|
205
|
+
const inner = element();
|
|
206
|
+
if (inner.tag === TAG.begCollection)
|
|
207
|
+
depth++;
|
|
208
|
+
else if (inner.tag === TAG.endCollection)
|
|
209
|
+
depth--;
|
|
210
|
+
}
|
|
211
|
+
value = { tag: e.tag, bytes: new Uint8Array() };
|
|
212
|
+
}
|
|
213
|
+
else
|
|
214
|
+
value = decodeValue(e.tag, e.value.slice());
|
|
215
|
+
if (!group)
|
|
216
|
+
fail("an attribute before any group");
|
|
217
|
+
if (e.name === "") {
|
|
218
|
+
if (!last)
|
|
219
|
+
fail("a further value before any attribute");
|
|
220
|
+
last.values.push(value);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
last = group.attributes.find((a) => a.name === e.name);
|
|
224
|
+
if (last)
|
|
225
|
+
last.values.push(value);
|
|
226
|
+
else
|
|
227
|
+
group.attributes.push((last = { name: e.name, tag: e.tag, values: [value] }));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
version: [bytes[0], bytes[1]],
|
|
232
|
+
code: view.getUint16(2),
|
|
233
|
+
requestId: view.getUint32(4),
|
|
234
|
+
groups,
|
|
235
|
+
...(i < bytes.length ? { data: bytes.slice(i) } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/** Every attribute of the groups with this tag (default: the printer's), by name, repeats merged. */
|
|
239
|
+
export function attributesOf(m, groupTag = TAG.printer) {
|
|
240
|
+
const out = {};
|
|
241
|
+
for (const g of m.groups)
|
|
242
|
+
if (g.tag === groupTag)
|
|
243
|
+
for (const a of g.attributes)
|
|
244
|
+
(out[a.name] ??= []).push(...a.values);
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Found } from "../../discover.ts";
|
|
2
|
+
/** One resolved mDNS instance, as `mdnsResolve` in src/finders.ts returns it. */
|
|
3
|
+
export type Answer = {
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
instance: string;
|
|
7
|
+
txt: Record<string, string>;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* AirPrint printers as found devices: the identity is TXT `UUID` (the instance name when absent), the name TXT `ty` (else
|
|
11
|
+
* the instance name), the model `ty`. `txt` keeps the whole record for the connector (`rp`, `pdl`, `URF`, `product`,
|
|
12
|
+
* `adminurl`, `note`). A printer on both types is one entry, the plain `_ipp._tcp` one (`ipp` listed first), so a
|
|
13
|
+
* self-signed certificate on `_ipps._tcp` never decides; a printer on `_ipps._tcp` alone is `secure`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function printersFound(ipp: Answer[], ipps: Answer[]): Found[];
|