@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/CHANGELOG.md +13 -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
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/* Printer – the plugin's page in the Homebridge UI (Plugins → Printer → Settings).
|
|
2
|
+
*
|
|
3
|
+
* Top: live dashboard, "Find printers on my network", "Test connection".
|
|
4
|
+
* Below: the regular settings form, drawn by the Homebridge UI itself (homebridge.showSchemaForm()).
|
|
5
|
+
*/
|
|
6
|
+
;(function () {
|
|
7
|
+
'use strict'
|
|
8
|
+
|
|
9
|
+
const hb = window.homebridge
|
|
10
|
+
const ALIAS = 'Printer'
|
|
11
|
+
let config = null // the plugin's config block (kept in sync with the form below)
|
|
12
|
+
let started = false
|
|
13
|
+
|
|
14
|
+
function el(tag, cls, text) {
|
|
15
|
+
const n = document.createElement(tag)
|
|
16
|
+
if (cls) {
|
|
17
|
+
n.className = cls
|
|
18
|
+
}
|
|
19
|
+
if (text !== undefined && text !== null) {
|
|
20
|
+
n.textContent = text
|
|
21
|
+
}
|
|
22
|
+
return n
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function printers() {
|
|
26
|
+
return config && Array.isArray(config.printers)
|
|
27
|
+
? config.printers
|
|
28
|
+
: []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function withAddress() {
|
|
32
|
+
return printers().filter((p) => {
|
|
33
|
+
return p && String(p.address || '').trim()
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function err(e) {
|
|
38
|
+
return (e && (e.message || (e.data && e.data.message))) || String(e)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------- config
|
|
42
|
+
function starterBlock() {
|
|
43
|
+
return {
|
|
44
|
+
platform: ALIAS,
|
|
45
|
+
name: 'Printer',
|
|
46
|
+
printers: [
|
|
47
|
+
{ address: '' },
|
|
48
|
+
],
|
|
49
|
+
dashboard: { enabled: true, port: 8587 },
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pushConfig() {
|
|
54
|
+
return hb.updatePluginConfig([config]).then((blocks) => {
|
|
55
|
+
if (blocks && blocks[0]) {
|
|
56
|
+
config = blocks[0]
|
|
57
|
+
}
|
|
58
|
+
refreshAll()
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function addAddress(address, name) {
|
|
63
|
+
const list = printers()
|
|
64
|
+
const lower = String(address).toLowerCase()
|
|
65
|
+
if (
|
|
66
|
+
list.some((p) => {
|
|
67
|
+
return String(p.address || '').toLowerCase() === lower
|
|
68
|
+
})
|
|
69
|
+
) {
|
|
70
|
+
hb.toast.info('That printer is already in your settings.')
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const empty = list.filter((p) => {
|
|
74
|
+
return !String(p.address || '').trim()
|
|
75
|
+
})[0]
|
|
76
|
+
if (empty) {
|
|
77
|
+
empty.address = address
|
|
78
|
+
if (name && !empty.name) {
|
|
79
|
+
empty.name = name
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
list.push(name && list.length
|
|
83
|
+
? { address, name }
|
|
84
|
+
: { address })
|
|
85
|
+
}
|
|
86
|
+
config.printers = list
|
|
87
|
+
pushConfig().then(
|
|
88
|
+
() => {
|
|
89
|
+
hb.toast.success(`Added ${address}. Click Save, then restart Homebridge.`)
|
|
90
|
+
},
|
|
91
|
+
(e) => {
|
|
92
|
+
hb.toast.error(err(e))
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------- dashboard link
|
|
98
|
+
function showFullLink() {
|
|
99
|
+
const a = document.getElementById('hp-full')
|
|
100
|
+
const d = (config && config.dashboard) || {}
|
|
101
|
+
if (d.enabled === false) {
|
|
102
|
+
a.style.display = 'none'
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
a.href = `${location.protocol.replace('https:', 'http:')}//${location.hostname}:${Number(d.port) || 8587}/`
|
|
106
|
+
a.style.display = ''
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------- test connection
|
|
110
|
+
function renderTests() {
|
|
111
|
+
const box = document.getElementById('hp-tests')
|
|
112
|
+
box.innerHTML = ''
|
|
113
|
+
withAddress().forEach((p) => {
|
|
114
|
+
const row = el('div', 'hp-test-row')
|
|
115
|
+
const label = el('div', 'hp-test-label')
|
|
116
|
+
label.appendChild(el('strong', null, String(p.name || '').trim() || 'Printer'))
|
|
117
|
+
label.appendChild(el('span', 'text-muted', ` · ${p.address}`))
|
|
118
|
+
const result = el('div', 'hp-test-result')
|
|
119
|
+
const btn = el('button', 'btn btn-sm btn-outline-primary', 'Test connection')
|
|
120
|
+
btn.type = 'button'
|
|
121
|
+
btn.addEventListener('click', () => {
|
|
122
|
+
btn.disabled = true
|
|
123
|
+
result.className = 'hp-test-result is-busy'
|
|
124
|
+
result.textContent = 'Contacting printer…'
|
|
125
|
+
hb.request('/test', { address: p.address })
|
|
126
|
+
.then(
|
|
127
|
+
(r) => {
|
|
128
|
+
if (r && r.ok) {
|
|
129
|
+
const inks = (r.markers || [])
|
|
130
|
+
.map((m) => {
|
|
131
|
+
return `${m.name} ${m.level === null
|
|
132
|
+
? '?'
|
|
133
|
+
: `${m.level}%`}`
|
|
134
|
+
})
|
|
135
|
+
.join(' · ')
|
|
136
|
+
result.className = 'hp-test-result is-good'
|
|
137
|
+
result.textContent = `✓ Connected – ${r.model || 'printer'}${inks
|
|
138
|
+
? ` – ${inks}`
|
|
139
|
+
: ''}`
|
|
140
|
+
} else {
|
|
141
|
+
result.className = 'hp-test-result is-bad'
|
|
142
|
+
result.textContent = `✕ ${(r && r.message) || 'Could not reach the printer.'}`
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
(e) => {
|
|
146
|
+
result.className = 'hp-test-result is-bad'
|
|
147
|
+
result.textContent = `✕ ${err(e)}`
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
.then(() => {
|
|
151
|
+
btn.disabled = false
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
row.appendChild(label)
|
|
155
|
+
row.appendChild(btn)
|
|
156
|
+
box.appendChild(row)
|
|
157
|
+
box.appendChild(result)
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------- find printers
|
|
162
|
+
function setupFind() {
|
|
163
|
+
const btn = document.getElementById('hp-find')
|
|
164
|
+
const box = document.getElementById('hp-found')
|
|
165
|
+
btn.addEventListener('click', () => {
|
|
166
|
+
btn.disabled = true
|
|
167
|
+
box.innerHTML = ''
|
|
168
|
+
const wait = el('div', 'text-muted py-2')
|
|
169
|
+
wait.appendChild(el('span', 'spinner-border spinner-border-sm me-2'))
|
|
170
|
+
wait.appendChild(document.createTextNode('Searching for about 3 seconds…'))
|
|
171
|
+
box.appendChild(wait)
|
|
172
|
+
hb.request('/discover', {})
|
|
173
|
+
.then(
|
|
174
|
+
(res) => {
|
|
175
|
+
box.innerHTML = ''
|
|
176
|
+
const found = (res && res.printers) || []
|
|
177
|
+
if (!found.length) {
|
|
178
|
+
box.appendChild(
|
|
179
|
+
el(
|
|
180
|
+
'div',
|
|
181
|
+
'text-muted py-2',
|
|
182
|
+
'No printers answered. Make sure the printer is on and on the same network as Homebridge, or type its IP address in the form below.',
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
found.forEach((f) => {
|
|
188
|
+
const address = f.port === 631 && !f.secure
|
|
189
|
+
? f.host
|
|
190
|
+
: f.address
|
|
191
|
+
const already = printers().some((p) => {
|
|
192
|
+
const a = String(p.address || '').toLowerCase()
|
|
193
|
+
return (
|
|
194
|
+
a
|
|
195
|
+
&& (a === String(address).toLowerCase()
|
|
196
|
+
|| a.includes(String(f.host).toLowerCase())
|
|
197
|
+
|| a.includes(String(f.hostname).toLowerCase()))
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
const row = el('div', 'hp-found-row')
|
|
201
|
+
const info = el('div')
|
|
202
|
+
info.appendChild(el('div', 'fw-semibold', f.name.replace(/\s*\[[0-9A-F]{6}\]\s*$/i, '')))
|
|
203
|
+
info.appendChild(el('div', 'small text-muted', [f.model, f.host].filter(Boolean).join(' · ')))
|
|
204
|
+
row.appendChild(info)
|
|
205
|
+
if (already) {
|
|
206
|
+
row.appendChild(el('span', 'badge bg-secondary', 'Added'))
|
|
207
|
+
} else {
|
|
208
|
+
const add = el('button', 'btn btn-sm btn-primary', 'Add')
|
|
209
|
+
add.type = 'button'
|
|
210
|
+
add.addEventListener('click', () => {
|
|
211
|
+
add.disabled = true
|
|
212
|
+
addAddress(address)
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
btn.click()
|
|
215
|
+
}, 400)
|
|
216
|
+
})
|
|
217
|
+
row.appendChild(add)
|
|
218
|
+
}
|
|
219
|
+
box.appendChild(row)
|
|
220
|
+
})
|
|
221
|
+
},
|
|
222
|
+
(e) => {
|
|
223
|
+
box.innerHTML = ''
|
|
224
|
+
box.appendChild(el('div', 'text-danger py-2', `Search failed: ${err(e)}`))
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
.then(() => {
|
|
228
|
+
btn.disabled = false
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------- live dashboard
|
|
234
|
+
let dashCb = null
|
|
235
|
+
const dashSource = {
|
|
236
|
+
load() {
|
|
237
|
+
const list = withAddress()
|
|
238
|
+
if (!list.length) {
|
|
239
|
+
return Promise.resolve({ printers: [] })
|
|
240
|
+
}
|
|
241
|
+
return hb.request('/status', { printers: list, dashboard: config.dashboard || {} })
|
|
242
|
+
},
|
|
243
|
+
subscribe(cb) {
|
|
244
|
+
dashCb = cb
|
|
245
|
+
const self = this
|
|
246
|
+
setInterval(() => {
|
|
247
|
+
self.load().then(cb, () => {})
|
|
248
|
+
}, 6000)
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function refreshAll() {
|
|
253
|
+
showFullLink()
|
|
254
|
+
renderTests()
|
|
255
|
+
const hint = document.getElementById('hp-hint')
|
|
256
|
+
hint.style.display = withAddress().length
|
|
257
|
+
? 'none'
|
|
258
|
+
: ''
|
|
259
|
+
document.getElementById('hp-dash').style.display = withAddress().length
|
|
260
|
+
? ''
|
|
261
|
+
: 'none'
|
|
262
|
+
if (dashCb) {
|
|
263
|
+
dashSource.load().then(dashCb, () => {})
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------- start
|
|
268
|
+
function start() {
|
|
269
|
+
if (started) {
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
started = true
|
|
273
|
+
setupFind()
|
|
274
|
+
|
|
275
|
+
hb.getPluginConfig()
|
|
276
|
+
.then((blocks) => {
|
|
277
|
+
// The Homebridge UI only draws its settings form once a config block exists,
|
|
278
|
+
// so on a fresh install create one with an empty printer to fill in.
|
|
279
|
+
const fresh = !blocks || !blocks.length
|
|
280
|
+
config = fresh
|
|
281
|
+
? starterBlock()
|
|
282
|
+
: blocks[0]
|
|
283
|
+
if (!Array.isArray(config.printers)) {
|
|
284
|
+
config.printers = Array.isArray(config.printer)
|
|
285
|
+
? config.printer
|
|
286
|
+
: []
|
|
287
|
+
}
|
|
288
|
+
if (!config.printers.length) {
|
|
289
|
+
config.printers.push({ address: '' })
|
|
290
|
+
}
|
|
291
|
+
const ready = fresh || !blocks[0].printers
|
|
292
|
+
? hb.updatePluginConfig([config])
|
|
293
|
+
: Promise.resolve()
|
|
294
|
+
return ready
|
|
295
|
+
})
|
|
296
|
+
.then(() => {
|
|
297
|
+
hb.showSchemaForm()
|
|
298
|
+
const dash = document.getElementById('hp-dash')
|
|
299
|
+
if (document.body.classList.contains('dark-mode')) {
|
|
300
|
+
dash.classList.add('hp-dark')
|
|
301
|
+
}
|
|
302
|
+
window.HPDashboard.mount(dash, dashSource)
|
|
303
|
+
refreshAll()
|
|
304
|
+
})
|
|
305
|
+
.catch((e) => {
|
|
306
|
+
hb.toast.error(`Could not load settings: ${err(e)}`)
|
|
307
|
+
hb.showSchemaForm()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// Keep in step with edits made in the form below
|
|
311
|
+
hb.addEventListener('configChanged', (event) => {
|
|
312
|
+
const blocks = event.data
|
|
313
|
+
if (blocks && blocks[0]) {
|
|
314
|
+
config = blocks[0]
|
|
315
|
+
refreshAll()
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
hb.addEventListener('ready', start)
|
|
321
|
+
if (document.body && document.body.style.display === 'block') {
|
|
322
|
+
start()
|
|
323
|
+
}
|
|
324
|
+
})()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server side of the plugin's page in the Homebridge UI (Plugins → Printer → Settings).
|
|
3
|
+
*
|
|
4
|
+
* Requests: /status, /test, /discover
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import http from 'node:http'
|
|
8
|
+
|
|
9
|
+
import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils'
|
|
10
|
+
|
|
11
|
+
import { discoverPrinters } from '../discovery.js'
|
|
12
|
+
import { PrinterPoller } from '../poller.js'
|
|
13
|
+
|
|
14
|
+
const quiet = {
|
|
15
|
+
info() {},
|
|
16
|
+
warn() {},
|
|
17
|
+
error() {},
|
|
18
|
+
debug() {},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Ask the running plugin's dashboard server for its live status (fast, and matches the Home app). */
|
|
22
|
+
function fromRunningPlugin(port) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const req = http.get(
|
|
25
|
+
{
|
|
26
|
+
host: '127.0.0.1',
|
|
27
|
+
port,
|
|
28
|
+
path: '/api/status',
|
|
29
|
+
timeout: 1500,
|
|
30
|
+
},
|
|
31
|
+
(res) => {
|
|
32
|
+
let body = ''
|
|
33
|
+
res.on('data', c => (body += c))
|
|
34
|
+
res.on('end', () => {
|
|
35
|
+
try {
|
|
36
|
+
const data = JSON.parse(body)
|
|
37
|
+
resolve(data && Array.isArray(data.printers)
|
|
38
|
+
? data
|
|
39
|
+
: null)
|
|
40
|
+
} catch (_) {
|
|
41
|
+
resolve(null)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
req.on('timeout', () => req.destroy())
|
|
47
|
+
req.on('error', () => resolve(null))
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function pollOnce(p, i) {
|
|
52
|
+
const poller = new PrinterPoller(
|
|
53
|
+
{
|
|
54
|
+
...p,
|
|
55
|
+
id: String(i),
|
|
56
|
+
name: String(p.name || '').trim() || (i
|
|
57
|
+
? `Printer ${i + 1}`
|
|
58
|
+
: 'Printer'),
|
|
59
|
+
address: String(p.address).trim(),
|
|
60
|
+
polling: 10,
|
|
61
|
+
lowInkThreshold: Number(p.lowInkThreshold) || 10,
|
|
62
|
+
},
|
|
63
|
+
quiet,
|
|
64
|
+
)
|
|
65
|
+
poller.stopped = true // single check
|
|
66
|
+
await poller.poll()
|
|
67
|
+
return poller.state
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- live status for the dashboard at the top of the page --------------------------------------
|
|
71
|
+
async function status({ printers, dashboard }) {
|
|
72
|
+
const list = (Array.isArray(printers)
|
|
73
|
+
? printers
|
|
74
|
+
: []).filter(p => p && String(p.address || '').trim())
|
|
75
|
+
if (!list.length) {
|
|
76
|
+
return { printers: [] }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const port = Number(dashboard && dashboard.port) || 8587
|
|
80
|
+
if (!dashboard || dashboard.enabled !== false) {
|
|
81
|
+
const live = await fromRunningPlugin(port)
|
|
82
|
+
// Use the plugin's data only if it is watching exactly the printers in these settings
|
|
83
|
+
if (live) {
|
|
84
|
+
const want = list
|
|
85
|
+
.map(p => String(p.address).trim().toLowerCase())
|
|
86
|
+
.sort()
|
|
87
|
+
.join('|')
|
|
88
|
+
const have = live.printers
|
|
89
|
+
.map(p => String(p.address).trim().toLowerCase())
|
|
90
|
+
.sort()
|
|
91
|
+
.join('|')
|
|
92
|
+
if (want === have) {
|
|
93
|
+
return { ...live, source: 'plugin' }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const states = await Promise.all(list.map(pollOnce))
|
|
98
|
+
return {
|
|
99
|
+
generated: new Date().toISOString(),
|
|
100
|
+
source: 'settings',
|
|
101
|
+
printers: states,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- "Test connection" ---------------------------------------------------------------------------
|
|
106
|
+
async function test({ address }) {
|
|
107
|
+
if (!address || !String(address).trim()) {
|
|
108
|
+
return { ok: false, message: 'Enter an address first.' }
|
|
109
|
+
}
|
|
110
|
+
const state = await pollOnce({ address }, 0)
|
|
111
|
+
if (!state.online) {
|
|
112
|
+
return { ok: false, message: state.error || 'No answer from that address.' }
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
model: state.model,
|
|
117
|
+
status: state.status,
|
|
118
|
+
markers: state.markers.map(m => ({ name: m.name, level: m.level })),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- "Find printers on my network" -------------------------------------------------------------
|
|
123
|
+
async function discover() {
|
|
124
|
+
return { printers: await discoverPrinters({ timeout: 3000 }) }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class PluginUiServer extends HomebridgePluginUiServer {
|
|
128
|
+
constructor() {
|
|
129
|
+
super()
|
|
130
|
+
this.onRequest('/status', body => status(body || {}))
|
|
131
|
+
this.onRequest('/test', body => test(body || {}))
|
|
132
|
+
this.onRequest('/discover', () => discover())
|
|
133
|
+
this.ready()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
;(() => new PluginUiServer())()
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PLATFORM_NAME, PLUGIN_NAME, PrinterPlatform } from './platform.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @homebridge-plugins/homebridge-printer
|
|
5
|
+
* Based on homebridge-printer by SeydX (https://github.com/seydx/homebridge-printer), MIT.
|
|
6
|
+
* Rewritten for Homebridge 1.8+ and 2.x.
|
|
7
|
+
*/
|
|
8
|
+
export default (api) => {
|
|
9
|
+
api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, PrinterPlatform)
|
|
10
|
+
}
|