@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,421 @@
|
|
|
1
|
+
/* Printer dashboard – served by the plugin at http://<homebridge>:8587/ */
|
|
2
|
+
;(function (global) {
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
5
|
+
const REASONS = {
|
|
6
|
+
'media-empty': 'Out of paper',
|
|
7
|
+
'media-needed': 'Load paper',
|
|
8
|
+
'media-jam': 'Paper jam',
|
|
9
|
+
'door-open': 'A door is open',
|
|
10
|
+
'cover-open': 'Cover is open',
|
|
11
|
+
'input-tray-missing': 'Paper tray missing',
|
|
12
|
+
'output-tray-missing': 'Output tray missing',
|
|
13
|
+
'marker-supply-empty': 'Cartridge empty',
|
|
14
|
+
'marker-supply-low': 'Ink low',
|
|
15
|
+
'toner-empty': 'Toner empty',
|
|
16
|
+
'toner-low': 'Toner low',
|
|
17
|
+
'paused': 'Paused',
|
|
18
|
+
'offline': 'Printer reports offline',
|
|
19
|
+
'other': 'Needs attention',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function el(tag, cls, text) {
|
|
23
|
+
const n = document.createElement(tag)
|
|
24
|
+
if (cls) {
|
|
25
|
+
n.className = cls
|
|
26
|
+
}
|
|
27
|
+
if (text !== undefined && text !== null) {
|
|
28
|
+
n.textContent = text
|
|
29
|
+
}
|
|
30
|
+
return n
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function reasonText(r) {
|
|
34
|
+
const base = String(r).replace(/-(?:error|warning|report)$/, '')
|
|
35
|
+
return (
|
|
36
|
+
REASONS[base]
|
|
37
|
+
|| base.replace(/-/g, ' ').replace(/^\w/, (c) => {
|
|
38
|
+
return c.toUpperCase()
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fmtTime(iso) {
|
|
44
|
+
if (!iso) {
|
|
45
|
+
return '—'
|
|
46
|
+
}
|
|
47
|
+
const d = new Date(iso)
|
|
48
|
+
return `${d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })} ${d.toLocaleDateString([], {
|
|
49
|
+
month: 'short',
|
|
50
|
+
day: 'numeric',
|
|
51
|
+
year: 'numeric',
|
|
52
|
+
})}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Normalise HP's pure RGB marker colours into friendlier ink tones (like HP Smart does)
|
|
56
|
+
const INK = {
|
|
57
|
+
'#00FFFF': '#2d9fd8',
|
|
58
|
+
'#FF00FF': '#d6337e',
|
|
59
|
+
'#FFFF00': '#f7cf3b',
|
|
60
|
+
'#000000': '#1d1d22',
|
|
61
|
+
}
|
|
62
|
+
function inkColour(c) {
|
|
63
|
+
return INK[c] || c
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isLow(m, threshold) {
|
|
67
|
+
if (m.level === null || m.level === undefined) {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
const t = m.low !== null && m.low > 0
|
|
71
|
+
? Math.max(m.low, threshold)
|
|
72
|
+
: threshold
|
|
73
|
+
return m.level <= t
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderInk(markers, threshold) {
|
|
77
|
+
const wrap = el('div', 'hp-inks')
|
|
78
|
+
markers.forEach((m) => {
|
|
79
|
+
const colours = (m.colors && m.colors.length
|
|
80
|
+
? m.colors
|
|
81
|
+
: ['#8a8a96']).map(inkColour)
|
|
82
|
+
const low = isLow(m, threshold)
|
|
83
|
+
const unknown = m.level === null || m.level === undefined
|
|
84
|
+
const col = el('div', 'hp-ink')
|
|
85
|
+
col.title = `${m.name}: ${unknown
|
|
86
|
+
? (m.someRemaining ? 'some ink remaining' : 'level unknown')
|
|
87
|
+
: `${m.level}%`}`
|
|
88
|
+
|
|
89
|
+
const tube = el('div', `hp-tube${low ? ' is-low' : ''}${unknown ? ' is-unknown' : ''}`)
|
|
90
|
+
tube.setAttribute('role', 'meter')
|
|
91
|
+
tube.setAttribute('aria-label', m.name)
|
|
92
|
+
tube.setAttribute('aria-valuemin', '0')
|
|
93
|
+
tube.setAttribute('aria-valuemax', '100')
|
|
94
|
+
if (!unknown) {
|
|
95
|
+
tube.setAttribute('aria-valuenow', String(m.level))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const level = unknown
|
|
99
|
+
? (m.someRemaining ? 50 : 0)
|
|
100
|
+
: m.level
|
|
101
|
+
colours.forEach((c) => {
|
|
102
|
+
const stripe = el('div', 'hp-stripe')
|
|
103
|
+
const fill = el('div', 'hp-fill')
|
|
104
|
+
fill.style.setProperty('--ink', c)
|
|
105
|
+
fill.style.height = `${level}%`
|
|
106
|
+
stripe.appendChild(fill)
|
|
107
|
+
tube.appendChild(stripe)
|
|
108
|
+
})
|
|
109
|
+
col.appendChild(tube)
|
|
110
|
+
|
|
111
|
+
const dots = el('div', `hp-dots hp-dots-${Math.min(colours.length, 3)}`)
|
|
112
|
+
colours.slice(0, 3).forEach((c) => {
|
|
113
|
+
const d = el('span', 'hp-dot')
|
|
114
|
+
d.style.background = c
|
|
115
|
+
dots.appendChild(d)
|
|
116
|
+
})
|
|
117
|
+
col.appendChild(dots)
|
|
118
|
+
col.appendChild(el('div', `hp-pct${low ? ' is-low' : ''}`, unknown
|
|
119
|
+
? '?'
|
|
120
|
+
: `${m.level}%`))
|
|
121
|
+
wrap.appendChild(col)
|
|
122
|
+
})
|
|
123
|
+
return wrap
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const PRINTER_SVG
|
|
127
|
+
= '<svg viewBox="0 0 520 300" class="hp-printer-svg" aria-hidden="true">'
|
|
128
|
+
+ '<defs>'
|
|
129
|
+
+ '<linearGradient id="hpBody" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="var(--body-hi)"/><stop offset="1" stop-color="var(--body-lo)"/></linearGradient>'
|
|
130
|
+
+ '<linearGradient id="hpLid" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="var(--body-hi)"/><stop offset="1" stop-color="var(--body-mid)"/></linearGradient>'
|
|
131
|
+
+ '<clipPath id="hpSlot"><rect x="150" y="196" width="230" height="120"/></clipPath>'
|
|
132
|
+
+ '</defs>'
|
|
133
|
+
+ '<ellipse cx="262" cy="282" rx="230" ry="12" fill="var(--shadow)"/>'
|
|
134
|
+
// paper sticking out of the top feeder
|
|
135
|
+
+ '<rect x="170" y="38" width="190" height="22" rx="2" fill="var(--paper)" stroke="var(--line)"/>'
|
|
136
|
+
// main body
|
|
137
|
+
+ '<path d="M42 92 Q42 74 60 72 L470 72 Q488 74 488 92 L494 250 Q494 268 476 268 L54 268 Q36 268 36 250 Z" fill="url(#hpBody)" stroke="var(--line)"/>'
|
|
138
|
+
// accent band on the left edge (blue-grey like the DeskJet in the reference)
|
|
139
|
+
+ '<path d="M42 92 Q42 74 60 72 L120 72 L110 128 L40 128 Z" fill="var(--accent-band)"/>'
|
|
140
|
+
+ '<path d="M470 72 Q488 74 488 92 L490 128 L430 128 L420 72 Z" fill="var(--accent-band)"/>'
|
|
141
|
+
// scanner lid
|
|
142
|
+
+ '<path d="M100 56 Q100 46 112 46 L420 46 Q432 46 432 56 L436 122 Q436 130 426 130 L106 130 Q96 130 96 122 Z" fill="url(#hpLid)" stroke="var(--line)"/>'
|
|
143
|
+
+ '<ellipse cx="300" cy="74" rx="22" ry="5" fill="var(--logo)"/>'
|
|
144
|
+
// control panel
|
|
145
|
+
+ '<rect x="56" y="96" width="34" height="58" rx="5" fill="var(--panel)" transform="skewX(-6)"/>'
|
|
146
|
+
+ '<circle cx="64" cy="108" r="3" class="hp-led"/>'
|
|
147
|
+
+ '<rect x="62" y="118" width="14" height="10" rx="1.5" fill="var(--panel-screen)"/>'
|
|
148
|
+
// label plate
|
|
149
|
+
+ '<rect x="58" y="176" width="66" height="14" rx="3" fill="var(--plate)" stroke="var(--line)"/>'
|
|
150
|
+
// output opening
|
|
151
|
+
+ '<path d="M152 150 L378 150 Q390 150 390 162 L390 238 L140 238 L140 162 Q140 150 152 150 Z" fill="var(--opening)" stroke="var(--line)"/>'
|
|
152
|
+
+ '<rect x="152" y="190" width="226" height="14" rx="3" fill="var(--slot)"/>'
|
|
153
|
+
+ '<rect x="248" y="146" width="34" height="8" rx="2" fill="var(--body-mid)" stroke="var(--line)"/>'
|
|
154
|
+
+ '<g fill="var(--rollers)"><rect x="170" y="194" width="30" height="6" rx="1"/><rect x="222" y="194" width="30" height="6" rx="1"/><rect x="278" y="194" width="30" height="6" rx="1"/><rect x="330" y="194" width="30" height="6" rx="1"/></g>'
|
|
155
|
+
// paper coming out (animated while printing)
|
|
156
|
+
+ '<g clip-path="url(#hpSlot)"><g class="hp-sheet"><rect x="176" y="190" width="178" height="70" fill="var(--paper)" stroke="var(--line)"/>'
|
|
157
|
+
+ '<g stroke="var(--ink-lines)" stroke-width="3" stroke-linecap="round"><line x1="192" y1="206" x2="300" y2="206"/><line x1="192" y1="218" x2="330" y2="218"/><line x1="192" y1="230" x2="280" y2="230"/></g></g></g>'
|
|
158
|
+
// output tray
|
|
159
|
+
+ '<path d="M126 238 L404 238 L412 258 L118 258 Z" fill="var(--tray)" stroke="var(--line)"/>'
|
|
160
|
+
+ '<rect x="230" y="242" width="70" height="5" rx="2.5" fill="var(--body-mid)"/>'
|
|
161
|
+
+ '</svg>'
|
|
162
|
+
|
|
163
|
+
function statusInfo(p) {
|
|
164
|
+
if (!p.online || p.status === 'offline' || p.status === 'unknown') {
|
|
165
|
+
return {
|
|
166
|
+
cls: 'is-offline',
|
|
167
|
+
title: p.status === 'unknown'
|
|
168
|
+
? 'Connecting…'
|
|
169
|
+
: 'Not connected.',
|
|
170
|
+
sub:
|
|
171
|
+
p.status === 'unknown'
|
|
172
|
+
? 'Waiting for the first response from the printer'
|
|
173
|
+
: 'Check the printer is on and connected to the network',
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const issues = (p.reasons || []).filter((r) => {
|
|
177
|
+
return /-(?:error|warning)$/.test(r) || r === 'media-empty' || r === 'media-jam' || r === 'door-open'
|
|
178
|
+
})
|
|
179
|
+
if (p.printing === true || (p.printing === undefined && p.status === 'processing')) {
|
|
180
|
+
return {
|
|
181
|
+
cls: 'is-printing',
|
|
182
|
+
title: 'Printing…',
|
|
183
|
+
sub: p.queuedJobs > 1
|
|
184
|
+
? `${p.queuedJobs} jobs in the queue`
|
|
185
|
+
: 'Your document is on its way',
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
p.status === 'stopped'
|
|
190
|
+
|| issues.some((r) => {
|
|
191
|
+
return r.endsWith('-error')
|
|
192
|
+
})
|
|
193
|
+
) {
|
|
194
|
+
return {
|
|
195
|
+
cls: 'is-error',
|
|
196
|
+
title: issues.length
|
|
197
|
+
? reasonText(issues[0])
|
|
198
|
+
: 'Printer stopped',
|
|
199
|
+
sub: p.message || 'Check the printer',
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (issues.length) {
|
|
203
|
+
return {
|
|
204
|
+
cls: 'is-warn',
|
|
205
|
+
title: reasonText(issues[0]),
|
|
206
|
+
sub: issues.length > 1
|
|
207
|
+
? `and ${issues.length - 1} more`
|
|
208
|
+
: 'Printer needs attention',
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { cls: 'is-ready', title: 'Ready', sub: 'Idle and waiting for jobs' }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const ICONS = {
|
|
215
|
+
'is-printing':
|
|
216
|
+
'<svg viewBox="0 0 24 24"><path d="M7 3h10v5H7z" fill="none" stroke="currentColor" stroke-width="1.7"/><rect x="3" y="8" width="18" height="9" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M7 14h10v7H7z" fill="none" stroke="currentColor" stroke-width="1.7"/></svg>',
|
|
217
|
+
'is-ready':
|
|
218
|
+
'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M8 12.5l2.6 2.6L16 9.7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
219
|
+
'is-offline':
|
|
220
|
+
'<svg viewBox="0 0 24 24"><rect x="3" y="8" width="13" height="9" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M6 3h7v5H6zM6 14h7v6H6z" fill="none" stroke="currentColor" stroke-width="1.7"/><circle cx="18" cy="17" r="4.5" fill="currentColor"/><path d="M16.8 15.8a1.3 1.3 0 1 1 1.8 1.2c-.4.2-.6.5-.6.9M18 19.6v.1" fill="none" stroke="var(--pill-bg)" stroke-width="1.2" stroke-linecap="round"/></svg>',
|
|
221
|
+
'is-warn':
|
|
222
|
+
'<svg viewBox="0 0 24 24"><path d="M12 3l10 18H2z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M12 10v5M12 17.6v.1" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>',
|
|
223
|
+
'is-error':
|
|
224
|
+
'<svg viewBox="0 0 24 24"><path d="M12 3l10 18H2z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M12 10v5M12 17.6v.1" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>',
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function renderPrinter(p, threshold) {
|
|
228
|
+
const s = statusInfo(p)
|
|
229
|
+
const sec = el('section', `hp-card ${s.cls}`)
|
|
230
|
+
|
|
231
|
+
const left = el('div', 'hp-left')
|
|
232
|
+
left.appendChild(el('h1', 'hp-name', p.name))
|
|
233
|
+
if (p.model) {
|
|
234
|
+
left.appendChild(el('div', 'hp-model', p.model.replace(/^HP\s+/i, '')))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (p.markers && p.markers.length) {
|
|
238
|
+
const box = el('div', 'hp-supplies')
|
|
239
|
+
box.appendChild(renderInk(p.markers, p.lowThreshold ?? threshold))
|
|
240
|
+
box.appendChild(el('div', 'hp-meta', 'Estimated supply levels'))
|
|
241
|
+
box.appendChild(el('div', 'hp-meta', `Last updated ${fmtTime(p.lastSeen || p.lastUpdated)}`))
|
|
242
|
+
const low = p.markers.filter((m) => {
|
|
243
|
+
return isLow(m, p.lowThreshold ?? threshold)
|
|
244
|
+
})
|
|
245
|
+
if (low.length) {
|
|
246
|
+
box.appendChild(
|
|
247
|
+
el(
|
|
248
|
+
'div',
|
|
249
|
+
'hp-warn',
|
|
250
|
+
`${
|
|
251
|
+
low.length === 1
|
|
252
|
+
? `${low[0].name} is`
|
|
253
|
+
: `${low
|
|
254
|
+
.map((m) => {
|
|
255
|
+
return m.name
|
|
256
|
+
})
|
|
257
|
+
.join(' and ')} are`
|
|
258
|
+
} running low`,
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
left.appendChild(box)
|
|
263
|
+
} else {
|
|
264
|
+
const empty = el('div', 'hp-supplies hp-supplies-empty')
|
|
265
|
+
empty.appendChild(
|
|
266
|
+
el(
|
|
267
|
+
'div',
|
|
268
|
+
'hp-meta',
|
|
269
|
+
p.online ? 'This printer did not report supply levels' : 'Supply levels appear once the printer is reachable',
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
empty.appendChild(el('div', 'hp-meta', `Last updated ${fmtTime(p.lastUpdated)}`))
|
|
273
|
+
left.appendChild(empty)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (p.energy) {
|
|
277
|
+
const e = p.energy
|
|
278
|
+
const kwh = e.kWh < 10
|
|
279
|
+
? e.kWh.toFixed(2)
|
|
280
|
+
: e.kWh.toFixed(1)
|
|
281
|
+
const energy = el('div', 'hp-energy')
|
|
282
|
+
energy.title = 'Worked out from the printer\'s state (printing, ready, sleep, off) and the wattages in Settings'
|
|
283
|
+
energy.appendChild(el('strong', null, `≈ ${Math.round(e.watts * 10) / 10} W`))
|
|
284
|
+
energy.appendChild(
|
|
285
|
+
el(
|
|
286
|
+
'span',
|
|
287
|
+
null,
|
|
288
|
+
` now · ${kwh} kWh since ${new Date(e.since).toLocaleDateString([], { day: 'numeric', month: 'short' })}`,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
left.appendChild(energy)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const pill = el('div', 'hp-pill')
|
|
295
|
+
pill.setAttribute('role', 'status')
|
|
296
|
+
const icon = el('span', 'hp-pill-icon')
|
|
297
|
+
icon.innerHTML = ICONS[s.cls]
|
|
298
|
+
const txt = el('span', 'hp-pill-text')
|
|
299
|
+
txt.appendChild(el('strong', null, s.title))
|
|
300
|
+
txt.appendChild(el('span', null, ` ${s.sub}`))
|
|
301
|
+
pill.appendChild(icon)
|
|
302
|
+
pill.appendChild(txt)
|
|
303
|
+
left.appendChild(pill)
|
|
304
|
+
|
|
305
|
+
const right = el('div', 'hp-right')
|
|
306
|
+
right.innerHTML = PRINTER_SVG
|
|
307
|
+
|
|
308
|
+
sec.appendChild(left)
|
|
309
|
+
sec.appendChild(right)
|
|
310
|
+
return sec
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function render(root, data) {
|
|
314
|
+
const printers = (data && data.printers) || []
|
|
315
|
+
root.innerHTML = ''
|
|
316
|
+
if (!printers.length) {
|
|
317
|
+
const e = el('section', 'hp-card hp-empty')
|
|
318
|
+
e.appendChild(el('h1', 'hp-name', 'No printers yet'))
|
|
319
|
+
e.appendChild(el('div', 'hp-meta', 'Add a printer in the plugin settings and restart Homebridge.'))
|
|
320
|
+
root.appendChild(e)
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
printers.forEach((p) => {
|
|
324
|
+
root.appendChild(renderPrinter(p, 10))
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* source: { load(): Promise<status>, subscribe?(cb): void }
|
|
330
|
+
*/
|
|
331
|
+
function mount(root, source) {
|
|
332
|
+
root.classList.add('hp-dashboard')
|
|
333
|
+
root.innerHTML = '<div class="hp-loading">Loading printers…</div>'
|
|
334
|
+
let lastSig = null
|
|
335
|
+
const draw = function (d) {
|
|
336
|
+
// Only rebuild when something visible changed, so animations don't restart every poll
|
|
337
|
+
const sig = JSON.stringify(
|
|
338
|
+
((d && d.printers) || []).map((p) => {
|
|
339
|
+
return [
|
|
340
|
+
p.name,
|
|
341
|
+
p.model,
|
|
342
|
+
p.online,
|
|
343
|
+
p.status,
|
|
344
|
+
p.printing,
|
|
345
|
+
p.energy && [
|
|
346
|
+
p.energy.watts,
|
|
347
|
+
p.energy.kWh.toFixed(2),
|
|
348
|
+
],
|
|
349
|
+
p.reasons,
|
|
350
|
+
p.queuedJobs,
|
|
351
|
+
p.markers,
|
|
352
|
+
p.message,
|
|
353
|
+
fmtTime(p.lastSeen || p.lastUpdated),
|
|
354
|
+
]
|
|
355
|
+
}),
|
|
356
|
+
)
|
|
357
|
+
if (sig === lastSig) {
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
lastSig = sig
|
|
361
|
+
render(root, d)
|
|
362
|
+
}
|
|
363
|
+
const fail = function (err) {
|
|
364
|
+
root.innerHTML = ''
|
|
365
|
+
const e = el('section', 'hp-card hp-empty')
|
|
366
|
+
e.appendChild(el('h1', 'hp-name', 'Dashboard unavailable'))
|
|
367
|
+
e.appendChild(el('div', 'hp-meta', (err && err.message) || String(err)))
|
|
368
|
+
root.appendChild(e)
|
|
369
|
+
}
|
|
370
|
+
source.load().then(draw, fail)
|
|
371
|
+
if (source.subscribe) {
|
|
372
|
+
source.subscribe(draw, fail)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Data source for the standalone page: Server-Sent Events, falling back to polling. */
|
|
377
|
+
function httpSource(base) {
|
|
378
|
+
base = base || ''
|
|
379
|
+
return {
|
|
380
|
+
load() {
|
|
381
|
+
return fetch(`${base}/api/status`, { cache: 'no-store' }).then((r) => {
|
|
382
|
+
if (!r.ok) {
|
|
383
|
+
throw new Error(`HTTP ${r.status}`)
|
|
384
|
+
}
|
|
385
|
+
return r.json()
|
|
386
|
+
})
|
|
387
|
+
},
|
|
388
|
+
subscribe(cb) {
|
|
389
|
+
const self = this
|
|
390
|
+
const poll = function () {
|
|
391
|
+
setInterval(() => {
|
|
392
|
+
self.load().then(cb, () => {})
|
|
393
|
+
}, 5000)
|
|
394
|
+
}
|
|
395
|
+
if (!global.EventSource) {
|
|
396
|
+
return poll()
|
|
397
|
+
}
|
|
398
|
+
const es = new EventSource(`${base}/api/events`)
|
|
399
|
+
let opened = false
|
|
400
|
+
es.onopen = function () {
|
|
401
|
+
opened = true
|
|
402
|
+
}
|
|
403
|
+
es.onmessage = function (e) {
|
|
404
|
+
try {
|
|
405
|
+
cb(JSON.parse(e.data))
|
|
406
|
+
} catch (_) {
|
|
407
|
+
/* ignore */
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
es.onerror = function () {
|
|
411
|
+
if (!opened) {
|
|
412
|
+
es.close()
|
|
413
|
+
poll()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
global.HPDashboard = { mount, render, httpSource }
|
|
421
|
+
})(window)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<link rel="stylesheet" href="dashboard.css">
|
|
2
|
+
<link rel="stylesheet" href="settings.css">
|
|
3
|
+
|
|
4
|
+
<div id="hp-ui">
|
|
5
|
+
<div id="hp-dash"></div>
|
|
6
|
+
|
|
7
|
+
<div class="hp-tools">
|
|
8
|
+
<div class="hp-tools-row">
|
|
9
|
+
<button type="button" class="btn btn-primary" id="hp-find">Find printers on my network</button>
|
|
10
|
+
<a id="hp-full" class="btn btn-outline-secondary" target="_blank" rel="noopener" style="display:none">Open full-screen dashboard</a>
|
|
11
|
+
</div>
|
|
12
|
+
<div id="hp-found"></div>
|
|
13
|
+
<div id="hp-tests"></div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<p class="hp-hint text-muted small" id="hp-hint">Enter your printer's IP address or .local name below, click <strong>Save</strong>, then restart Homebridge.</p>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<script src="dashboard.js"></script>
|
|
20
|
+
<script src="settings.js"></script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* Printer page in the Homebridge UI – uses the Bootstrap 5 styles the Homebridge UI injects */
|
|
2
|
+
#hp-ui { padding-bottom: 6px; }
|
|
3
|
+
#hp-dash { margin-bottom: 14px; }
|
|
4
|
+
#hp-dash .hp-card { padding: 22px 26px; }
|
|
5
|
+
#hp-dash .hp-name { font-size: clamp(28px, 5vw, 44px); }
|
|
6
|
+
#hp-dash .hp-model { font-size: clamp(15px, 2vw, 21px); margin-bottom: 16px; }
|
|
7
|
+
#hp-dash .hp-printer-svg { max-width: 460px; }
|
|
8
|
+
.hp-tools { margin-bottom: 8px; }
|
|
9
|
+
.hp-tools-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
10
|
+
#hp-found { margin-top: 10px; }
|
|
11
|
+
.hp-found-row {
|
|
12
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
13
|
+
padding: 10px 12px; margin-bottom: 8px;
|
|
14
|
+
border: 1px solid var(--bs-border-color, #dee2e6); border-radius: 8px;
|
|
15
|
+
}
|
|
16
|
+
#hp-tests { margin-top: 12px; }
|
|
17
|
+
.hp-test-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 0 2px; flex-wrap: wrap; }
|
|
18
|
+
.hp-test-label { overflow-wrap: anywhere; }
|
|
19
|
+
.hp-test-result { font-size: 14px; min-height: 2px; padding-bottom: 6px; }
|
|
20
|
+
.hp-test-result.is-good { color: var(--bs-success, #198754); }
|
|
21
|
+
.hp-test-result.is-bad { color: var(--bs-danger, #dc3545); }
|
|
22
|
+
.hp-test-result.is-busy { color: var(--bs-secondary-color, #6c757d); }
|
|
23
|
+
.hp-hint { margin: 10px 0 0; }
|
|
24
|
+
|
|
25
|
+
/* Readable in the Homebridge UI's dark mode, whatever theme is active */
|
|
26
|
+
body.dark-mode #hp-ui { color: #e8e8ef; }
|
|
27
|
+
body.dark-mode #hp-ui .text-muted { color: #a8a8b8 !important; }
|
|
28
|
+
body.dark-mode #hp-ui .btn-outline-secondary { color: #d6d6e0; border-color: #8a8a9a; }
|
|
29
|
+
body.dark-mode #hp-ui .btn-outline-secondary:hover { color: #111; background: #d6d6e0; }
|
|
30
|
+
body.dark-mode .hp-found-row { border-color: #4a4a58; }
|