@homebridge-plugins/homebridge-printer 0.0.0-stage → 1.0.1-beta.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/CHANGELOG.md +20 -0
- package/LICENSE +22 -0
- package/README.md +62 -2
- package/branding/Homebridge_x_Printer.png +0 -0
- package/branding/Homebridge_x_Printer.svg +1 -0
- package/branding/icon.png +0 -0
- package/config.schema.json +250 -0
- package/eslint.config.js +58 -0
- package/lib/accessories.js +333 -0
- package/lib/characteristics.js +95 -0
- package/lib/dashboard-server.js +134 -0
- package/lib/discovery.js +302 -0
- package/lib/energy.js +485 -0
- package/lib/fakegato/LICENSE +21 -0
- package/lib/fakegato/fakegato-history.js +833 -0
- package/lib/fakegato/fakegato-storage.js +155 -0
- package/lib/fakegato/fakegato-timer.js +128 -0
- package/lib/fakegato/uuid.js +28 -0
- package/lib/homebridge-ui/public/dashboard.css +207 -0
- package/lib/homebridge-ui/public/dashboard.html +33 -0
- package/lib/homebridge-ui/public/dashboard.js +421 -0
- package/lib/homebridge-ui/public/index.html +20 -0
- package/lib/homebridge-ui/public/settings.css +30 -0
- package/lib/homebridge-ui/public/settings.js +324 -0
- package/lib/homebridge-ui/server.js +137 -0
- package/lib/index.js +10 -0
- package/lib/ipp.js +352 -0
- package/lib/platform.js +335 -0
- package/lib/poller.js +187 -0
- package/package.json +56 -4
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
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"alias": "Printer",
|
|
4
|
+
"displayName": "Homebridge Printer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"version": "1.0.1-beta.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.3.0"
|
|
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
|
+
}
|