@homebridge-plugins/homebridge-printer 0.0.0-stage → 1.0.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/lib/poller.js ADDED
@@ -0,0 +1,187 @@
1
+ import { EventEmitter } from 'node:events'
2
+
3
+ import { IppPrinter, PRINTER_STATES } from './ipp.js'
4
+
5
+ const REQUESTED = [
6
+ 'printer-state',
7
+ 'printer-state-reasons',
8
+ 'printer-state-message',
9
+ 'printer-make-and-model',
10
+ 'printer-info',
11
+ 'printer-is-accepting-jobs',
12
+ 'queued-job-count',
13
+ 'marker-names',
14
+ 'marker-levels',
15
+ 'marker-low-levels',
16
+ 'marker-high-levels',
17
+ 'marker-colors',
18
+ 'marker-types',
19
+ ]
20
+
21
+ const NETWORK_CODES = new Set([
22
+ 'ECONNREFUSED',
23
+ 'ETIMEDOUT',
24
+ 'EHOSTUNREACH',
25
+ 'ENETUNREACH',
26
+ 'ECONNRESET',
27
+ 'ECONNABORTED',
28
+ 'ENOTFOUND',
29
+ 'EAI_AGAIN',
30
+ 'EBUSY',
31
+ 'EPIPE',
32
+ ])
33
+
34
+ function arr(v) {
35
+ return v === undefined || v === null
36
+ ? []
37
+ : Array.isArray(v)
38
+ ? v
39
+ : [v]
40
+ }
41
+
42
+ function parseColors(c) {
43
+ // HP tri-color cartridges report e.g. "#00FFFF#FF00FF#FFFF00"
44
+ const m = String(c || '').match(/#[0-9a-f]{6}/gi)
45
+ return m
46
+ ? m.map(x => x.toUpperCase())
47
+ : []
48
+ }
49
+
50
+ function parseMarkers(attrs) {
51
+ const names = arr(attrs['marker-names'])
52
+ const levels = arr(attrs['marker-levels'])
53
+ const lows = arr(attrs['marker-low-levels'])
54
+ const highs = arr(attrs['marker-high-levels'])
55
+ const colors = arr(attrs['marker-colors'])
56
+ const types = arr(attrs['marker-types'])
57
+ const count = Math.max(names.length, levels.length)
58
+ const markers = []
59
+ for (let i = 0; i < count; i++) {
60
+ const raw = typeof levels[i] === 'number'
61
+ ? levels[i]
62
+ : -1
63
+ const high = typeof highs[i] === 'number' && highs[i] > 0
64
+ ? highs[i]
65
+ : 100
66
+ // -1 = unavailable, -2 = unknown, -3 = "some remaining" (RFC 3805 / PWG 5100.13)
67
+ const level = raw >= 0
68
+ ? Math.max(0, Math.min(100, Math.round((raw / high) * 100)))
69
+ : null
70
+ markers.push({
71
+ index: i,
72
+ name: names[i] || `Supply ${i + 1}`,
73
+ level,
74
+ someRemaining: raw === -3,
75
+ low: typeof lows[i] === 'number' && lows[i] >= 0
76
+ ? lows[i]
77
+ : null,
78
+ colors: parseColors(colors[i]),
79
+ type: types[i] || null,
80
+ })
81
+ }
82
+ return markers
83
+ }
84
+
85
+ class PrinterPoller extends EventEmitter {
86
+ constructor(config, log) {
87
+ super()
88
+ this.config = config
89
+ this.log = log
90
+ this.client = new IppPrinter(config.address, {
91
+ timeout: Math.min(15000, Math.max(3000, config.polling * 800)),
92
+ })
93
+ this.timer = null
94
+ this.stopped = false
95
+ this.state = {
96
+ id: config.id,
97
+ name: config.name,
98
+ address: config.address,
99
+ online: false,
100
+ status: 'unknown',
101
+ reasons: [],
102
+ message: null,
103
+ model: config.model || null,
104
+ queuedJobs: 0,
105
+ printing: false,
106
+ markers: [],
107
+ lowThreshold: config.lowInkThreshold,
108
+ lastUpdated: null,
109
+ lastSeen: null,
110
+ error: null,
111
+ }
112
+ }
113
+
114
+ start() {
115
+ this.stopped = false
116
+ this.poll()
117
+ }
118
+
119
+ stop() {
120
+ this.stopped = true
121
+ clearTimeout(this.timer)
122
+ }
123
+
124
+ async poll() {
125
+ clearTimeout(this.timer)
126
+ const prev = JSON.stringify(this.state)
127
+ try {
128
+ const a = await this.client.getAttributes(REQUESTED)
129
+ const reasons = arr(a['printer-state-reasons']).filter(r => r && r !== 'none')
130
+ const markers = parseMarkers(a)
131
+ Object.assign(this.state, {
132
+ online: true,
133
+ status: PRINTER_STATES[a['printer-state']] || 'unknown',
134
+ reasons,
135
+ message: a['printer-state-message'] || null,
136
+ model: this.config.model || a['printer-make-and-model'] || this.state.model,
137
+ queuedJobs: typeof a['queued-job-count'] === 'number'
138
+ ? a['queued-job-count']
139
+ : 0,
140
+ // Printing = the printer is busy AND has a job. Printers also report "processing"
141
+ // while warming up or cleaning heads; those have no job, so they don't count.
142
+ printing: a['printer-state'] === 4 && (typeof a['queued-job-count'] !== 'number' || a['queued-job-count'] > 0),
143
+ markers: markers.length ? markers : this.state.markers,
144
+ lastUpdated: new Date().toISOString(),
145
+ lastSeen: new Date().toISOString(),
146
+ error: null,
147
+ })
148
+ this.log.debug(`${this.config.name}: ${this.state.status} ${JSON.stringify(markers.map(m => [m.name, m.level]))}`)
149
+ } catch (err) {
150
+ const network = err && NETWORK_CODES.has(err.code)
151
+ if (network) {
152
+ this.log.debug(`${this.config.name}: printer not reachable (${err.code})`)
153
+ } else if (this.state.error !== err.message) {
154
+ this.log.warn(`${this.config.name}: ${err.message}`)
155
+ }
156
+ Object.assign(this.state, {
157
+ online: false,
158
+ status: 'offline',
159
+ printing: false,
160
+ lastUpdated: new Date().toISOString(),
161
+ error: network ? 'Printer not reachable' : err.message,
162
+ })
163
+ }
164
+ if (JSON.stringify(this.state) !== prev) {
165
+ try {
166
+ this.emit('update', this.state)
167
+ } catch (err) {
168
+ this.log.warn(`${this.config.name}: ${err.message}`)
169
+ }
170
+ }
171
+ if (!this.stopped) {
172
+ // a refresh from the dashboard can run while a poll is waiting: keep to one timer
173
+ clearTimeout(this.timer)
174
+ // Check more often while a job is printing, so the switch turns off promptly when it finishes
175
+ const secs
176
+ = this.state.status === 'processing' || this.state.printing
177
+ ? Math.min(this.config.polling, 2)
178
+ : this.config.polling
179
+ this.timer = setTimeout(() => this.poll(), secs * 1000)
180
+ if (this.timer.unref) {
181
+ this.timer.unref()
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ export { parseMarkers, PrinterPoller, REQUESTED }
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@homebridge-plugins/homebridge-printer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "alias": "Printer",
4
+ "displayName": "Homebridge Printer",
5
+ "type": "module",
6
+ "version": "1.0.0",
7
+ "description": "Homebridge plugin to show HP and other AirPrint (IPP) printers in Apple Home, with ink levels and a live dashboard.",
8
+ "author": "homebridge-plugins",
9
+ "license": "MIT",
10
+ "homepage": "https://github.com/homebridge-plugins/homebridge-printer",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/homebridge-plugins/homebridge-printer.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/homebridge-plugins/homebridge-printer/issues"
17
+ },
18
+ "keywords": [
19
+ "homebridge",
20
+ "homebridge-plugin",
21
+ "supports-hap",
22
+ "supports-matter",
23
+ "homebridge-printer",
24
+ "homekit",
25
+ "printer",
26
+ "ipp",
27
+ "airprint",
28
+ "hp",
29
+ "ink",
30
+ "energy"
31
+ ],
32
+ "main": "lib/index.js",
33
+ "engines": {
34
+ "homebridge": "^1.11.4 || ^2.0.0",
35
+ "node": "^22.12.0 || ^24.0.0 || ^26.0.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "lint": "eslint . --max-warnings=0",
42
+ "lint:fix": "npm run lint -- --fix",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest watch",
45
+ "test-coverage": "npm run test -- --coverage",
46
+ "changelog:sync": "node .github/scripts/sync-changelog.mjs"
47
+ },
48
+ "dependencies": {
49
+ "@homebridge/plugin-ui-utils": "^2.2.7"
50
+ },
51
+ "devDependencies": {
52
+ "@antfu/eslint-config": "^10.0.0",
53
+ "@vitest/coverage-v8": "^5.1.0",
54
+ "ajv": "^8.20.0",
55
+ "homebridge": "^2.5.0",
56
+ "vitest": "^5.1.0"
57
+ }
58
+ }