@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.
@@ -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
+ }