@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,155 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
|
|
6
|
+
const hostname = os.hostname().split('.')[0]
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Path separators, as the platform actually understands them.
|
|
10
|
+
*
|
|
11
|
+
* A history file is named after the accessory, and an accessory is named by
|
|
12
|
+
* whoever set the device up. A name carrying a separator stops being a name
|
|
13
|
+
* once it reaches `path.join`: it reads as a directory that does not exist, so
|
|
14
|
+
* the write fails with ENOENT and - because nothing here passes a callback -
|
|
15
|
+
* the error goes nowhere. A device called "Kitchen/Diner" simply never saved
|
|
16
|
+
* its history, with nothing in the log to say so. HAP-NodeJS guards its own
|
|
17
|
+
* storage keys against the same thing.
|
|
18
|
+
*
|
|
19
|
+
* Only genuine separators are replaced, and only where they are one. A
|
|
20
|
+
* backslash is an ordinary filename character on Linux, so replacing it there
|
|
21
|
+
* would rename the file of anyone already using one and lose their history.
|
|
22
|
+
*/
|
|
23
|
+
const PATH_SEPARATORS = process.platform === 'win32'
|
|
24
|
+
? /[\\/]/g
|
|
25
|
+
: /\//g
|
|
26
|
+
|
|
27
|
+
function toFileNamePart(name) {
|
|
28
|
+
return String(name ?? '').replace(PATH_SEPARATORS, '-')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default class {
|
|
32
|
+
constructor(params) {
|
|
33
|
+
if (!params) {
|
|
34
|
+
params = {}
|
|
35
|
+
}
|
|
36
|
+
this.writers = []
|
|
37
|
+
this.log = params.log || {}
|
|
38
|
+
if (!this.log) {
|
|
39
|
+
this.log = () => {}
|
|
40
|
+
}
|
|
41
|
+
this.addingWriter = false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
addWriter(service, params) {
|
|
45
|
+
if (!this.addingWriter) {
|
|
46
|
+
this.addingWriter = true
|
|
47
|
+
if (!params) {
|
|
48
|
+
params = {}
|
|
49
|
+
}
|
|
50
|
+
this.log('[%s] FGS addWriter().', service.accessoryName)
|
|
51
|
+
const newWriter = {
|
|
52
|
+
service,
|
|
53
|
+
callback: params.callback,
|
|
54
|
+
fileName: `${hostname}_${toFileNamePart(service.accessoryName)}_persist.json`,
|
|
55
|
+
}
|
|
56
|
+
const onReady = typeof params.onReady === 'function'
|
|
57
|
+
? params.onReady
|
|
58
|
+
: () => {}
|
|
59
|
+
newWriter.storageHandler = fs
|
|
60
|
+
newWriter.path = params.path || path.join(os.homedir(), '.homebridge')
|
|
61
|
+
this.writers.push(newWriter)
|
|
62
|
+
this.addingWriter = false
|
|
63
|
+
onReady()
|
|
64
|
+
} else {
|
|
65
|
+
setTimeout(() => this.addWriter(service, params), 100)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getWriter(service) {
|
|
70
|
+
return this.writers.find(ele => ele.service === service)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_getWriterIndex(service) {
|
|
74
|
+
return this.writers.findIndex(ele => ele.service === service)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getWriters() {
|
|
78
|
+
return this.writers
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
delWriter(service) {
|
|
82
|
+
const index = this._getWriterIndex(service)
|
|
83
|
+
this.writers.splice(index, 1)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
write(params) {
|
|
87
|
+
if (!this.writing) {
|
|
88
|
+
this.writing = true
|
|
89
|
+
const writer = this.getWriter(params.service)
|
|
90
|
+
const callBack = typeof params.callback === 'function'
|
|
91
|
+
? params.callback
|
|
92
|
+
: typeof writer.callback === 'function'
|
|
93
|
+
? writer.callback
|
|
94
|
+
: () => {}
|
|
95
|
+
const fileLoc = path.join(writer.path, writer.fileName)
|
|
96
|
+
// Written to a temporary file and renamed over the target, so a write cut
|
|
97
|
+
// short by a restart or a power cut leaves the previous history intact
|
|
98
|
+
// rather than a half-written file that will not parse.
|
|
99
|
+
//
|
|
100
|
+
// The leading dot matters: this is HAP-NodeJS's persist directory, and it
|
|
101
|
+
// reads every file that does not start with one into memory at startup -
|
|
102
|
+
// so a temporary file left behind by a crash would be loaded, and kept,
|
|
103
|
+
// for the life of every run after it. The pid keeps child bridges sharing
|
|
104
|
+
// the directory from racing on the same temporary name.
|
|
105
|
+
const tempLoc = path.join(writer.path, `.${writer.fileName}.${process.pid}.tmp`)
|
|
106
|
+
this.log(
|
|
107
|
+
'[%s] FGS write file [%s] [%s].',
|
|
108
|
+
params.service.accessoryName,
|
|
109
|
+
fileLoc,
|
|
110
|
+
params.data.substring(1, 81),
|
|
111
|
+
)
|
|
112
|
+
writer.storageHandler.writeFile(tempLoc, params.data, 'utf8', (writeErr) => {
|
|
113
|
+
if (writeErr) {
|
|
114
|
+
// the temporary file may never have been created, so a failure to
|
|
115
|
+
// remove it is not worth reporting over the write error itself
|
|
116
|
+
writer.storageHandler.unlink(tempLoc, () => {
|
|
117
|
+
this.writing = false
|
|
118
|
+
callBack(writeErr)
|
|
119
|
+
})
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
writer.storageHandler.rename(tempLoc, fileLoc, (renameErr) => {
|
|
123
|
+
this.writing = false
|
|
124
|
+
callBack(renameErr || null)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
} else {
|
|
128
|
+
setTimeout(() => this.write(params), 100)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
read(params) {
|
|
133
|
+
const writer = this.getWriter(params.service)
|
|
134
|
+
const callBack = typeof params.callback === 'function'
|
|
135
|
+
? params.callback
|
|
136
|
+
: typeof writer.callback === 'function'
|
|
137
|
+
? writer.callback
|
|
138
|
+
: () => {}
|
|
139
|
+
const fileLoc = path.join(writer.path, writer.fileName)
|
|
140
|
+
this.log('[%s] FGS read file [%s].', params.service.accessoryName, fileLoc)
|
|
141
|
+
writer.storageHandler.readFile(fileLoc, 'utf8', callBack)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
remove(params) {
|
|
145
|
+
const writer = this.getWriter(params.service)
|
|
146
|
+
const callBack = typeof params.callback === 'function'
|
|
147
|
+
? params.callback
|
|
148
|
+
: typeof writer.callback === 'function'
|
|
149
|
+
? writer.callback
|
|
150
|
+
: () => {}
|
|
151
|
+
const fileLoc = path.join(writer.path, writer.fileName)
|
|
152
|
+
this.log('[%s] FGS delete file [%s].', params.service.accessoryName, fileLoc)
|
|
153
|
+
writer.storageHandler.unlink(fileLoc, callBack)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export default class {
|
|
2
|
+
constructor(params) {
|
|
3
|
+
if (!params) {
|
|
4
|
+
params = {}
|
|
5
|
+
}
|
|
6
|
+
this.subscribedServices = []
|
|
7
|
+
this.minutes = params.minutes || 10
|
|
8
|
+
this.intervalID = null
|
|
9
|
+
this.running = false
|
|
10
|
+
this.log = params.log || {}
|
|
11
|
+
if (!this.log) {
|
|
12
|
+
this.log = () => {}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
subscribe(service, callback) {
|
|
17
|
+
this.log('[%s] FGT new subscription.', service.accessoryName)
|
|
18
|
+
const newService = {
|
|
19
|
+
service,
|
|
20
|
+
callback,
|
|
21
|
+
backLog: [],
|
|
22
|
+
previousBackLog: [],
|
|
23
|
+
previousAvrg: {},
|
|
24
|
+
}
|
|
25
|
+
this.subscribedServices.push(newService)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getSubscriber(service) {
|
|
29
|
+
return this.subscribedServices.find(el => el.service === service)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_getSubscriberIndex(service) {
|
|
33
|
+
return this.subscribedServices.findIndex(el => el.service === service)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getSubscribers() {
|
|
37
|
+
return this.subscribedServices
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
unsubscribe(service) {
|
|
41
|
+
const index = this._getSubscriberIndex(service)
|
|
42
|
+
if (index !== -1) {
|
|
43
|
+
this.subscribedServices.splice(index, 1)
|
|
44
|
+
}
|
|
45
|
+
if (this.subscribedServices.length === 0 && this.running) {
|
|
46
|
+
this.stop()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
start() {
|
|
51
|
+
this.log('Starting global FGT [%s minutes].', this.minutes)
|
|
52
|
+
if (this.running) {
|
|
53
|
+
this.stop()
|
|
54
|
+
}
|
|
55
|
+
this.running = true
|
|
56
|
+
this.intervalID = setInterval(this.executeCallbacks.bind(this), this.minutes * 60 * 1000)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
stop() {
|
|
60
|
+
this.log('Stopping global FGT.')
|
|
61
|
+
clearInterval(this.intervalID)
|
|
62
|
+
this.running = false
|
|
63
|
+
this.intervalID = null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
executeCallbacks() {
|
|
67
|
+
this.log('FGT executeCallbacks().')
|
|
68
|
+
if (this.subscribedServices.length !== 0) {
|
|
69
|
+
for (const s in this.subscribedServices) {
|
|
70
|
+
if (Object.hasOwn(this.subscribedServices, s)) {
|
|
71
|
+
const service = this.subscribedServices[s]
|
|
72
|
+
if (typeof service.callback === 'function') {
|
|
73
|
+
service.previousAvrg = service.callback({
|
|
74
|
+
backLog: service.backLog,
|
|
75
|
+
previousAvrg: service.previousAvrg,
|
|
76
|
+
timer: this,
|
|
77
|
+
immediate: false,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
executeImmediateCallback(service) {
|
|
86
|
+
this.log('[%s] FGT executeImmediateCallback().', service.accessoryName)
|
|
87
|
+
if (typeof service.callback === 'function' && service.backLog.length) {
|
|
88
|
+
service.previousAvrg = service.callback({
|
|
89
|
+
backLog: service.backLog,
|
|
90
|
+
previousAvrg: service.previousAvrg,
|
|
91
|
+
timer: this,
|
|
92
|
+
immediate: true,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
addData(params) {
|
|
98
|
+
const data = params.entry
|
|
99
|
+
const { service } = params
|
|
100
|
+
const immediateCallback = params.immediateCallback || false
|
|
101
|
+
this.log(
|
|
102
|
+
'[%s] FGT addData() [%s] immediate [%s].',
|
|
103
|
+
service.accessoryName,
|
|
104
|
+
data,
|
|
105
|
+
immediateCallback,
|
|
106
|
+
)
|
|
107
|
+
if (immediateCallback) {
|
|
108
|
+
this.getSubscriber(service).backLog[0] = data
|
|
109
|
+
} else {
|
|
110
|
+
this.getSubscriber(service).backLog.push(data)
|
|
111
|
+
}
|
|
112
|
+
if (immediateCallback) {
|
|
113
|
+
this.executeImmediateCallback(this.getSubscriber(service))
|
|
114
|
+
}
|
|
115
|
+
if (!this.running) {
|
|
116
|
+
this.start()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
emptyData(service) {
|
|
121
|
+
this.log('[%s] FGT emptyData().', service.accessoryName)
|
|
122
|
+
const source = this.getSubscriber(service)
|
|
123
|
+
if (source.backLog.length) {
|
|
124
|
+
source.previousBackLog = source.backLog
|
|
125
|
+
}
|
|
126
|
+
source.backLog = []
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// https://github.com/homebridge/HAP-NodeJS/blob/master/src/lib/util/uuid.ts
|
|
2
|
+
|
|
3
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
4
|
+
const shortRegex = /^[0-9a-f]{1,8}$/i
|
|
5
|
+
|
|
6
|
+
function isValid(UUID) {
|
|
7
|
+
return uuidRegex.test(UUID)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toLongFormUUID(uuid, base = '-0000-1000-8000-0026BB765291') {
|
|
11
|
+
if (isValid(uuid)) {
|
|
12
|
+
return uuid.toUpperCase()
|
|
13
|
+
}
|
|
14
|
+
if (!shortRegex.test(uuid)) {
|
|
15
|
+
throw new TypeError('uuid was not a valid UUID or short form UUID')
|
|
16
|
+
}
|
|
17
|
+
if (!isValid(`00000000${base}`)) {
|
|
18
|
+
throw new TypeError('base was not a valid base UUID')
|
|
19
|
+
}
|
|
20
|
+
return ((`00000000${uuid}`).slice(-8) + base).toUpperCase()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toShortFormUUID(uuid, base = '-0000-1000-8000-0026BB765291') {
|
|
24
|
+
uuid = toLongFormUUID(uuid, base)
|
|
25
|
+
return uuid.substring(0, 8)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { toLongFormUUID, toShortFormUUID }
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/* Printer dashboard */
|
|
2
|
+
.hp-dashboard {
|
|
3
|
+
--bg: #e3e3ee;
|
|
4
|
+
--card: transparent;
|
|
5
|
+
--text: #1f1f2b;
|
|
6
|
+
--model: #4b55e6;
|
|
7
|
+
--muted: #6f6f80;
|
|
8
|
+
--box: #f3f3f8;
|
|
9
|
+
--tube-bg: #e1e1e8;
|
|
10
|
+
--tube-edge: #d2d2dc;
|
|
11
|
+
--low: #f08a00;
|
|
12
|
+
--pill-bg: #f7f7fb;
|
|
13
|
+
--pill-border: #2a2a36;
|
|
14
|
+
--ok: #1f8a4c;
|
|
15
|
+
--printing: #4b55e6;
|
|
16
|
+
--error: #c62f2f;
|
|
17
|
+
|
|
18
|
+
/* printer illustration */
|
|
19
|
+
--body-hi: #ffffff;
|
|
20
|
+
--body-mid: #eef0f4;
|
|
21
|
+
--body-lo: #dfe2e8;
|
|
22
|
+
--accent-band: #7f97c6;
|
|
23
|
+
--line: #c3c7d1;
|
|
24
|
+
--shadow: rgba(40, 40, 70, 0.10);
|
|
25
|
+
--paper: #ffffff;
|
|
26
|
+
--panel: #9aa9c9;
|
|
27
|
+
--panel-screen: #53607b;
|
|
28
|
+
--plate: #f7f8fa;
|
|
29
|
+
--opening: #f4f5f8;
|
|
30
|
+
--rollers: #3a3d46;
|
|
31
|
+
--slot: #5b5f6b;
|
|
32
|
+
--tray: #f2f3f6;
|
|
33
|
+
--logo: #8ea1cb;
|
|
34
|
+
--ink-lines: #a9b1c8;
|
|
35
|
+
|
|
36
|
+
font-family: "Inter", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
|
|
37
|
+
color: var(--text);
|
|
38
|
+
background: var(--bg);
|
|
39
|
+
border-radius: 14px;
|
|
40
|
+
padding: 8px 0;
|
|
41
|
+
-webkit-font-smoothing: antialiased;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.hp-dashboard.hp-dark,
|
|
45
|
+
body.dark-mode .hp-dashboard {
|
|
46
|
+
--bg: #1d1d27;
|
|
47
|
+
--text: #ececf4;
|
|
48
|
+
--model: #8d95ff;
|
|
49
|
+
--muted: #a3a3b6;
|
|
50
|
+
--box: #282836;
|
|
51
|
+
--tube-bg: #3a3a4a;
|
|
52
|
+
--tube-edge: #4a4a5c;
|
|
53
|
+
--pill-bg: #262634;
|
|
54
|
+
--pill-border: #9a9ab0;
|
|
55
|
+
--body-hi: #f4f5f8;
|
|
56
|
+
--body-mid: #e2e5ea;
|
|
57
|
+
--body-lo: #cdd1d9;
|
|
58
|
+
--shadow: rgba(0, 0, 0, 0.35);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.hp-dashboard * { box-sizing: border-box; }
|
|
62
|
+
|
|
63
|
+
.hp-card {
|
|
64
|
+
display: grid;
|
|
65
|
+
grid-template-columns: minmax(260px, 1fr) minmax(260px, 1.35fr);
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 24px;
|
|
68
|
+
padding: 32px 40px;
|
|
69
|
+
}
|
|
70
|
+
.hp-card + .hp-card { border-top: 1px solid var(--tube-edge); }
|
|
71
|
+
|
|
72
|
+
.hp-name {
|
|
73
|
+
font-size: clamp(36px, 6vw, 64px);
|
|
74
|
+
font-weight: 500;
|
|
75
|
+
letter-spacing: -0.02em;
|
|
76
|
+
line-height: 1.05;
|
|
77
|
+
margin: 0 0 8px;
|
|
78
|
+
}
|
|
79
|
+
.hp-model {
|
|
80
|
+
color: var(--model);
|
|
81
|
+
font-size: clamp(18px, 2.4vw, 28px);
|
|
82
|
+
line-height: 1.3;
|
|
83
|
+
margin-bottom: 26px;
|
|
84
|
+
max-width: 18em;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.hp-supplies {
|
|
88
|
+
display: inline-block;
|
|
89
|
+
background: var(--box);
|
|
90
|
+
border-radius: 14px;
|
|
91
|
+
padding: 18px 20px 16px;
|
|
92
|
+
min-width: 240px;
|
|
93
|
+
}
|
|
94
|
+
.hp-inks { display: flex; gap: 18px; margin-bottom: 12px; align-items: flex-start; }
|
|
95
|
+
.hp-ink { display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
96
|
+
|
|
97
|
+
.hp-tube {
|
|
98
|
+
display: flex;
|
|
99
|
+
gap: 2px;
|
|
100
|
+
height: 124px;
|
|
101
|
+
border-radius: 999px;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
padding: 0;
|
|
104
|
+
background: var(--tube-bg);
|
|
105
|
+
box-shadow: inset 0 0 0 1px var(--tube-edge);
|
|
106
|
+
}
|
|
107
|
+
.hp-tube.is-low { box-shadow: 0 0 0 2px var(--low); }
|
|
108
|
+
.hp-tube.is-unknown .hp-fill { opacity: .45; }
|
|
109
|
+
|
|
110
|
+
.hp-stripe {
|
|
111
|
+
width: 11px;
|
|
112
|
+
height: 100%;
|
|
113
|
+
position: relative;
|
|
114
|
+
background: linear-gradient(90deg, rgba(255,255,255,.55), rgba(255,255,255,.15));
|
|
115
|
+
}
|
|
116
|
+
.hp-tube > .hp-stripe:only-child { width: 34px; }
|
|
117
|
+
.hp-fill {
|
|
118
|
+
position: absolute;
|
|
119
|
+
left: 0; right: 0; bottom: 0;
|
|
120
|
+
background: linear-gradient(180deg, color-mix(in srgb, var(--ink) 15%, transparent) 0%, var(--ink) 45%, var(--ink) 100%);
|
|
121
|
+
transition: height .8s cubic-bezier(.2,.7,.2,1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.hp-dots { position: relative; width: 22px; height: 18px; }
|
|
125
|
+
.hp-dot { position: absolute; width: 9px; height: 9px; border-radius: 50%; }
|
|
126
|
+
.hp-dots-1 .hp-dot { left: 6px; top: 4px; }
|
|
127
|
+
.hp-dots-2 .hp-dot:nth-child(1) { left: 1px; top: 4px; }
|
|
128
|
+
.hp-dots-2 .hp-dot:nth-child(2) { left: 12px; top: 4px; }
|
|
129
|
+
.hp-dots-3 .hp-dot:nth-child(1) { left: 6px; top: 0; }
|
|
130
|
+
.hp-dots-3 .hp-dot:nth-child(2) { left: 0; top: 9px; }
|
|
131
|
+
.hp-dots-3 .hp-dot:nth-child(3) { left: 11px; top: 9px; }
|
|
132
|
+
|
|
133
|
+
.hp-pct { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
134
|
+
.hp-pct.is-low { color: var(--low); font-weight: 600; }
|
|
135
|
+
|
|
136
|
+
.hp-meta { color: var(--muted); font-size: 14px; line-height: 1.7; }
|
|
137
|
+
.hp-warn { color: var(--low); font-size: 14px; font-weight: 600; margin-top: 4px; }
|
|
138
|
+
|
|
139
|
+
.hp-pill {
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 12px;
|
|
143
|
+
margin-top: 16px;
|
|
144
|
+
padding: 10px 18px;
|
|
145
|
+
border: 1.5px solid var(--pill-border);
|
|
146
|
+
border-radius: 22px;
|
|
147
|
+
background: var(--pill-bg);
|
|
148
|
+
width: fit-content;
|
|
149
|
+
max-width: 340px;
|
|
150
|
+
line-height: 1.25;
|
|
151
|
+
font-size: 15px;
|
|
152
|
+
}
|
|
153
|
+
.hp-pill-icon { width: 28px; height: 28px; flex: none; color: var(--muted); }
|
|
154
|
+
.hp-pill-icon svg { width: 100%; height: 100%; display: block; }
|
|
155
|
+
.hp-pill-text span { color: var(--muted); }
|
|
156
|
+
.is-ready .hp-pill-icon { color: var(--ok); }
|
|
157
|
+
.is-printing .hp-pill-icon { color: var(--printing); animation: hp-pulse 1.4s ease-in-out infinite; }
|
|
158
|
+
.is-printing .hp-pill { border-color: var(--printing); }
|
|
159
|
+
.is-error .hp-pill-icon { color: var(--error); }
|
|
160
|
+
.is-warn .hp-pill-icon { color: var(--low); }
|
|
161
|
+
.is-warn .hp-pill { border-color: var(--low); }
|
|
162
|
+
.is-warn .hp-led { fill: #ffb43d; animation: hp-blink 1.2s steps(2) infinite; }
|
|
163
|
+
.is-error .hp-pill { border-color: var(--error); }
|
|
164
|
+
.is-offline .hp-pill-icon { color: var(--text); }
|
|
165
|
+
|
|
166
|
+
.hp-right { display: flex; justify-content: center; }
|
|
167
|
+
.hp-printer-svg { width: 100%; max-width: 620px; height: auto; overflow: visible; }
|
|
168
|
+
.is-offline .hp-printer-svg { filter: grayscale(.6); opacity: .75; }
|
|
169
|
+
|
|
170
|
+
.hp-sheet { transform: translateY(-60px); }
|
|
171
|
+
.is-printing .hp-sheet { animation: hp-feed 3.2s cubic-bezier(.45,.05,.4,1) infinite; }
|
|
172
|
+
.hp-led { fill: #9aa3b5; }
|
|
173
|
+
.is-ready .hp-led { fill: #46d17d; }
|
|
174
|
+
.is-printing .hp-led { fill: #7c86ff; animation: hp-blink 1s steps(2) infinite; }
|
|
175
|
+
.is-error .hp-led { fill: #ff5a4f; animation: hp-blink .8s steps(2) infinite; }
|
|
176
|
+
|
|
177
|
+
@keyframes hp-feed {
|
|
178
|
+
0% { transform: translateY(-62px); }
|
|
179
|
+
70% { transform: translateY(14px); }
|
|
180
|
+
100% { transform: translateY(14px); opacity: 0; }
|
|
181
|
+
}
|
|
182
|
+
@keyframes hp-blink { 50% { opacity: .25; } }
|
|
183
|
+
@keyframes hp-pulse { 50% { transform: scale(.9); opacity: .7; } }
|
|
184
|
+
|
|
185
|
+
@media (prefers-reduced-motion: reduce) {
|
|
186
|
+
.is-printing .hp-sheet { animation: none; transform: translateY(0); }
|
|
187
|
+
.hp-led, .hp-pill-icon { animation: none !important; }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.hp-loading { padding: 40px; color: var(--muted); }
|
|
191
|
+
.hp-empty { grid-template-columns: 1fr; }
|
|
192
|
+
|
|
193
|
+
@media (max-width: 760px) {
|
|
194
|
+
.hp-card { grid-template-columns: 1fr; padding: 24px 16px; }
|
|
195
|
+
.hp-right { order: -1; }
|
|
196
|
+
.hp-printer-svg { max-width: 420px; }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.hp-dashboard.hp-dark .hp-dot,
|
|
200
|
+
body.dark-mode .hp-dashboard .hp-dot { box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.35); }
|
|
201
|
+
|
|
202
|
+
/* Keep host themes (Homebridge UI / Bootstrap) from restyling the dashboard */
|
|
203
|
+
.hp-dashboard .hp-name { color: var(--text); font-family: inherit; }
|
|
204
|
+
.hp-dashboard .hp-model { color: var(--model); }
|
|
205
|
+
|
|
206
|
+
.hp-energy { margin-top: 12px; font-size: 14px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
207
|
+
.hp-energy strong { color: var(--text); font-weight: 600; }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Printers</title>
|
|
7
|
+
<link rel="stylesheet" href="dashboard.css">
|
|
8
|
+
<style>
|
|
9
|
+
html, body { margin: 0; min-height: 100%; background: #e3e3ee; }
|
|
10
|
+
html.dark, html.dark body { background: #1d1d27; }
|
|
11
|
+
.page { max-width: 1280px; margin: 0 auto; padding: 24px 16px; }
|
|
12
|
+
.page .hp-dashboard { background: transparent; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<main class="page"><div id="app"></div></main>
|
|
17
|
+
<script src="dashboard.js"></script>
|
|
18
|
+
<script>
|
|
19
|
+
(function () {
|
|
20
|
+
var app = document.getElementById('app');
|
|
21
|
+
var dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
|
|
22
|
+
function theme() {
|
|
23
|
+
var on = dark && dark.matches;
|
|
24
|
+
app.classList.toggle('hp-dark', !!on);
|
|
25
|
+
document.documentElement.classList.toggle('dark', !!on);
|
|
26
|
+
}
|
|
27
|
+
theme();
|
|
28
|
+
if (dark && dark.addEventListener) dark.addEventListener('change', theme);
|
|
29
|
+
HPDashboard.mount(app, HPDashboard.httpSource(''));
|
|
30
|
+
})();
|
|
31
|
+
</script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|