sharp-echonet 0.2.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/LICENSE +21 -0
- package/README.md +167 -0
- package/data/ki-ux75.json +354 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +42 -0
- package/dist/decode-only.d.ts +11 -0
- package/dist/decode-only.js +9 -0
- package/dist/decode.d.ts +40 -0
- package/dist/decode.js +87 -0
- package/dist/echonet.d.ts +51 -0
- package/dist/echonet.js +154 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/read.d.ts +28 -0
- package/dist/read.js +63 -0
- package/dist/table.d.ts +78 -0
- package/dist/table.data.d.ts +2 -0
- package/dist/table.data.js +354 -0
- package/dist/table.js +27 -0
- package/package.json +56 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
export const tableData = {
|
|
2
|
+
"model": "KIUX75",
|
|
3
|
+
"productCode": "4b4955583735",
|
|
4
|
+
"firmware": [
|
|
5
|
+
"SHARP_B02"
|
|
6
|
+
],
|
|
7
|
+
"eoj": "0x013501",
|
|
8
|
+
"standardVersion": "I",
|
|
9
|
+
"note": "Field offsets for the vendor-specific properties of the Sharp KI-UX75 humidifying air purifier. Confirmed against two units. Offsets are byte positions inside the EDT of the given EPC.",
|
|
10
|
+
"spec": {
|
|
11
|
+
"hasDustSensor": true,
|
|
12
|
+
"hasPM25Sensor": false,
|
|
13
|
+
"hasCO2Sensor": false,
|
|
14
|
+
"hasLightSensor": true,
|
|
15
|
+
"hasHumidFunc": true,
|
|
16
|
+
"dustFilterLimit": 3000
|
|
17
|
+
},
|
|
18
|
+
"standardProperties": {
|
|
19
|
+
"0x80": {
|
|
20
|
+
"name": "operationStatus",
|
|
21
|
+
"note": "0x30 on, 0x31 off. Readable and writable."
|
|
22
|
+
},
|
|
23
|
+
"0x84": {
|
|
24
|
+
"name": "instantaneousPower",
|
|
25
|
+
"unit": "W",
|
|
26
|
+
"size": 2
|
|
27
|
+
},
|
|
28
|
+
"0x88": {
|
|
29
|
+
"name": "faultStatus",
|
|
30
|
+
"note": "0x41 fault, 0x42 normal"
|
|
31
|
+
},
|
|
32
|
+
"0xA0": {
|
|
33
|
+
"name": "airFlowRate",
|
|
34
|
+
"note": "Writes are acknowledged with ESV 0x71 and then ignored. Power draw does not change."
|
|
35
|
+
},
|
|
36
|
+
"0xC0": {
|
|
37
|
+
"name": "airPollutionDetected",
|
|
38
|
+
"note": "0x41 detected, 0x42 not detected"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"properties": {
|
|
42
|
+
"0xF1": {
|
|
43
|
+
"length": 43,
|
|
44
|
+
"fields": [
|
|
45
|
+
{
|
|
46
|
+
"offset": 2,
|
|
47
|
+
"size": 1,
|
|
48
|
+
"name": "brightnessRaw",
|
|
49
|
+
"type": "uint",
|
|
50
|
+
"confidence": "confirmed",
|
|
51
|
+
"note": "Not lux. A non-linear 0-255 scale: 199-202 fully lit, 145 at dusk with no lights, 0 in the dark. The vendor app does not read this byte; it uses the three-level flag at offset 8 instead."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"offset": 3,
|
|
55
|
+
"size": 1,
|
|
56
|
+
"name": "roomTemperature",
|
|
57
|
+
"type": "uint",
|
|
58
|
+
"unit": "degC",
|
|
59
|
+
"vendorSlot": "k1.s1",
|
|
60
|
+
"confidence": "confirmed"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"offset": 4,
|
|
64
|
+
"size": 1,
|
|
65
|
+
"name": "roomHumidity",
|
|
66
|
+
"type": "uint",
|
|
67
|
+
"unit": "%",
|
|
68
|
+
"vendorSlot": "k1.s2",
|
|
69
|
+
"confidence": "confirmed"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"offset": 8,
|
|
73
|
+
"size": 1,
|
|
74
|
+
"name": "lightingLevel",
|
|
75
|
+
"type": "bits",
|
|
76
|
+
"bits": "high 4",
|
|
77
|
+
"vendorSlot": "k1.s15",
|
|
78
|
+
"confidence": "confirmed",
|
|
79
|
+
"note": "The vendor app reads the top four bits and maps them to off / dark / bright."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"offset": 11,
|
|
83
|
+
"size": 4,
|
|
84
|
+
"name": "totalOperatingTime",
|
|
85
|
+
"type": "uint",
|
|
86
|
+
"unit": "minutes",
|
|
87
|
+
"vendorSlot": "k1.s3",
|
|
88
|
+
"confidence": "confirmed",
|
|
89
|
+
"note": "Advanced by exactly 30 across a 30 minute gap."
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"offset": 15,
|
|
93
|
+
"size": 2,
|
|
94
|
+
"name": "pciUnit1Used",
|
|
95
|
+
"type": "uint",
|
|
96
|
+
"vendorSlot": "k1.s4",
|
|
97
|
+
"confidence": "confirmed"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"offset": 17,
|
|
101
|
+
"size": 2,
|
|
102
|
+
"name": "pciUnit2Used",
|
|
103
|
+
"type": "uint",
|
|
104
|
+
"vendorSlot": "k1.s5",
|
|
105
|
+
"confidence": "confirmed"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"offset": 21,
|
|
109
|
+
"size": 4,
|
|
110
|
+
"name": "cadrUsed",
|
|
111
|
+
"type": "uint",
|
|
112
|
+
"vendorSlot": "k1.s6",
|
|
113
|
+
"confidence": "confirmed",
|
|
114
|
+
"note": "Cumulative volume of air cleaned. Rises monotonically."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"offset": 27,
|
|
118
|
+
"size": 2,
|
|
119
|
+
"name": "pm25Value",
|
|
120
|
+
"type": "uint",
|
|
121
|
+
"vendorSlot": "k1.s7",
|
|
122
|
+
"confidence": "offset-confirmed",
|
|
123
|
+
"usable": false,
|
|
124
|
+
"note": "The vendor app calls this pm25Value but gates it on hasPM25Sensor, which is false for this model. The bytes move, but do not publish them as a PM2.5 reading."
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"offset": 29,
|
|
128
|
+
"size": 2,
|
|
129
|
+
"name": "dustFilterUsed",
|
|
130
|
+
"type": "uint",
|
|
131
|
+
"vendorSlot": "k1.s8",
|
|
132
|
+
"confidence": "confirmed",
|
|
133
|
+
"note": "Compare against spec.dustFilterLimit for remaining life."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"offset": 31,
|
|
137
|
+
"size": 2,
|
|
138
|
+
"name": "smellFilterUsed",
|
|
139
|
+
"type": "uint",
|
|
140
|
+
"vendorSlot": "k1.s9",
|
|
141
|
+
"confidence": "confirmed"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"offset": 35,
|
|
145
|
+
"size": 2,
|
|
146
|
+
"name": "humidFilterUsed",
|
|
147
|
+
"type": "uint",
|
|
148
|
+
"vendorSlot": "k1.s10",
|
|
149
|
+
"confidence": "confirmed"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"offset": 37,
|
|
153
|
+
"size": 1,
|
|
154
|
+
"name": "agIonFilterUsed",
|
|
155
|
+
"type": "uint",
|
|
156
|
+
"vendorSlot": "k1.s11",
|
|
157
|
+
"confidence": "confirmed"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"offset": 38,
|
|
161
|
+
"size": 2,
|
|
162
|
+
"name": "totalHumidificationAmount",
|
|
163
|
+
"type": "uint",
|
|
164
|
+
"vendorSlot": "k1.s12",
|
|
165
|
+
"confidence": "confirmed"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"offset": 40,
|
|
169
|
+
"size": 3,
|
|
170
|
+
"name": "numberOfParticles",
|
|
171
|
+
"type": "uint",
|
|
172
|
+
"unit": "particles/L",
|
|
173
|
+
"vendorSlot": "k1.s14",
|
|
174
|
+
"confidence": "confirmed",
|
|
175
|
+
"note": "Matched the vendor's own figure at an aligned timestamp, then again on the second unit."
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
"unresolved": [
|
|
179
|
+
{
|
|
180
|
+
"vendorSlot": "k1.s13",
|
|
181
|
+
"name": "co2Concentration",
|
|
182
|
+
"note": "This model has no CO2 sensor, so the field is always zero and its offset cannot be pinned."
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
"0xF2": {
|
|
187
|
+
"length": 41,
|
|
188
|
+
"fields": [
|
|
189
|
+
{
|
|
190
|
+
"offset": 16,
|
|
191
|
+
"size": 1,
|
|
192
|
+
"name": "pm25Level",
|
|
193
|
+
"type": "uint",
|
|
194
|
+
"vendorSlot": "k2.s3",
|
|
195
|
+
"confidence": "confirmed",
|
|
196
|
+
"usable": false,
|
|
197
|
+
"note": "Tracked the vendor value across four timestamps. Still gated on hasPM25Sensor for this model."
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"offset": 17,
|
|
201
|
+
"size": 1,
|
|
202
|
+
"name": "airSummaryLevel",
|
|
203
|
+
"type": "uint",
|
|
204
|
+
"vendorSlot": "k2.s4",
|
|
205
|
+
"confidence": "confirmed"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"offset": 18,
|
|
209
|
+
"size": 1,
|
|
210
|
+
"name": "humidIconStatus",
|
|
211
|
+
"type": "uint",
|
|
212
|
+
"vendorSlot": "k2.s5",
|
|
213
|
+
"confidence": "confirmed"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"offset": 19,
|
|
217
|
+
"size": 1,
|
|
218
|
+
"name": "waterPresent",
|
|
219
|
+
"type": "flag",
|
|
220
|
+
"trueValue": "0xff",
|
|
221
|
+
"vendorSlot": "k2.s6",
|
|
222
|
+
"confidence": "confirmed",
|
|
223
|
+
"note": "0xff while the machine has water, 0x00 once it wants a refill. This is the only slot in the group that the vendor app never reads, and no vendor screen shows a refill state. Treat anything other than 0xff as empty so that an unseen third value does not fail silent."
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"offset": 20,
|
|
227
|
+
"size": 1,
|
|
228
|
+
"name": "lightSensorFlag",
|
|
229
|
+
"type": "flag",
|
|
230
|
+
"trueValue": "0xff",
|
|
231
|
+
"vendorSlot": "k2.s7",
|
|
232
|
+
"confidence": "confirmed",
|
|
233
|
+
"note": "0xff in a lit room, 0x00 in the dark."
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"offset": 24,
|
|
237
|
+
"size": 1,
|
|
238
|
+
"name": "humidButtonStatus",
|
|
239
|
+
"type": "uint",
|
|
240
|
+
"vendorSlot": "k2.s11",
|
|
241
|
+
"confidence": "confirmed",
|
|
242
|
+
"note": "Changed from 0x00 to 0x81 at the moment humidification was switched back on."
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"offset": 25,
|
|
246
|
+
"size": 1,
|
|
247
|
+
"name": "pciVisible",
|
|
248
|
+
"type": "uint",
|
|
249
|
+
"vendorSlot": "k2.s12",
|
|
250
|
+
"confidence": "confirmed"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"offset": 14,
|
|
254
|
+
"size": 1,
|
|
255
|
+
"name": "smellLevel",
|
|
256
|
+
"type": "uint",
|
|
257
|
+
"vendorSlot": "k2.s1",
|
|
258
|
+
"confidence": "probable",
|
|
259
|
+
"note": "Placed by the ordering that holds for slots 3 through 12. Not observed changing."
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"offset": 15,
|
|
263
|
+
"size": 1,
|
|
264
|
+
"name": "dustLevel",
|
|
265
|
+
"type": "uint",
|
|
266
|
+
"vendorSlot": "k2.s2",
|
|
267
|
+
"confidence": "probable",
|
|
268
|
+
"note": "Same reasoning as smellLevel."
|
|
269
|
+
}
|
|
270
|
+
],
|
|
271
|
+
"unresolved": [
|
|
272
|
+
{
|
|
273
|
+
"vendorSlot": "k2.s13 - k2.s17",
|
|
274
|
+
"note": "Slots 1 through 12 sit at offsets 14 through 25 in order, but slot 16 (volumeSetting) does not land where that run predicts, so the tail is laid out differently. Left unresolved rather than guessed."
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
},
|
|
278
|
+
"0xF3": {
|
|
279
|
+
"length": 27,
|
|
280
|
+
"fields": [
|
|
281
|
+
{
|
|
282
|
+
"offset": 15,
|
|
283
|
+
"size": 1,
|
|
284
|
+
"name": "humidificationEnabled",
|
|
285
|
+
"type": "flag",
|
|
286
|
+
"trueValue": "0xff",
|
|
287
|
+
"confidence": "confirmed",
|
|
288
|
+
"note": "The setting, not whether the machine is humidifying right now. A unit with an empty tank still reports 0xff while the setting is on. Writes are not reflected."
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"offset": 4,
|
|
292
|
+
"size": 1,
|
|
293
|
+
"name": "driveState",
|
|
294
|
+
"type": "uint",
|
|
295
|
+
"vendorSlot": "k3.s1",
|
|
296
|
+
"confidence": "probable",
|
|
297
|
+
"note": "The vendor app reads this slot for drive mode, running state and dust filter life."
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
"unresolved": [
|
|
301
|
+
{
|
|
302
|
+
"vendorSlot": "k3.s2",
|
|
303
|
+
"offset": 8,
|
|
304
|
+
"note": "The vendor app reads this slot, but what it means is not established."
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"vendorSlot": "k3.s6",
|
|
308
|
+
"offset": 13,
|
|
309
|
+
"note": "Position matched on a single snapshot. Meaning unknown."
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"vendorSlot": "rest of 0xF3",
|
|
313
|
+
"note": "Matched from a single snapshot where most bytes were zero."
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
},
|
|
317
|
+
"0xF4": {
|
|
318
|
+
"note": "Every byte was zero on both units across every reading."
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
"traps": [
|
|
322
|
+
{
|
|
323
|
+
"where": "0xF3[5]",
|
|
324
|
+
"claim": "Looks like a water or mode flag: 0x00 on one unit and 0x01 on the other.",
|
|
325
|
+
"reality": "A per-unit constant. It did not move when the tank was refilled or when humidification was switched on and off."
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"where": "0xF2[39]",
|
|
329
|
+
"claim": "Same shape, 0x00 against 0x01 between units.",
|
|
330
|
+
"reality": "Also a per-unit constant."
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"where": "0xF1[42]",
|
|
334
|
+
"claim": "Drifts constantly and never returns, so it reads as noise.",
|
|
335
|
+
"reality": "The low byte of the three byte particle count at offset 40. Read the whole field."
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"where": "0xF1[14]",
|
|
339
|
+
"reality": "Increments once a minute."
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"where": "0xF1[24]",
|
|
343
|
+
"reality": "Increments roughly every 43 seconds."
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"where": "0xA0",
|
|
347
|
+
"reality": "The device answers a write with ESV 0x71 and then ignores it. Instantaneous power does not budge."
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"where": "humidifier class 0x0139",
|
|
351
|
+
"reality": "The standard defines a six step water level at EPC 0xC5, but this unit lists only 0x013501 in its instance list and does not answer that object at all."
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
};
|
package/dist/table.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { tableData } from "./table.data.js";
|
|
2
|
+
/**
|
|
3
|
+
* data/ki-ux75.json is the source of truth. This reads the TypeScript generated
|
|
4
|
+
* from it rather than the file itself, so that decoding works where there is no
|
|
5
|
+
* filesystem.
|
|
6
|
+
*/
|
|
7
|
+
export const table = tableData;
|
|
8
|
+
/** Every field of one property, in byte order. */
|
|
9
|
+
export function fieldsOf(epc) {
|
|
10
|
+
const spec = table.properties[hexEpc(epc)];
|
|
11
|
+
return [...(spec?.fields ?? [])].sort((a, b) => a.offset - b.offset);
|
|
12
|
+
}
|
|
13
|
+
export function hexEpc(epc) {
|
|
14
|
+
return `0x${epc.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether a product code belongs to the hardware this table describes.
|
|
18
|
+
*
|
|
19
|
+
* EPC 0x8C carries the product code as ASCII padded with zero bytes, so
|
|
20
|
+
* `KIUX75` arrives as `4b4955583735000000000000`. Compare only the prefix.
|
|
21
|
+
*/
|
|
22
|
+
export function matchesProductCode(productCode) {
|
|
23
|
+
const hex = typeof productCode === "string"
|
|
24
|
+
? productCode.toLowerCase()
|
|
25
|
+
: [...productCode].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
26
|
+
return hex.startsWith(table.productCode);
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sharp-echonet",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Decode the vendor-specific ECHONET Lite properties of Sharp humidifying air purifiers over the local network",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"echonet",
|
|
7
|
+
"echonet-lite",
|
|
8
|
+
"sharp",
|
|
9
|
+
"air-purifier",
|
|
10
|
+
"ki-ux75",
|
|
11
|
+
"smart-home"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./decode": {
|
|
21
|
+
"types": "./dist/decode-only.d.ts",
|
|
22
|
+
"default": "./dist/decode-only.js"
|
|
23
|
+
},
|
|
24
|
+
"./table": "./data/ki-ux75.json"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"sharp-echonet": "./dist/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"data",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p tsconfig.json",
|
|
40
|
+
"test": "node --test --experimental-strip-types test/*.test.ts",
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
42
|
+
"lint:text": "textlint '**/*.md'",
|
|
43
|
+
"check": "npm run typecheck && npm run test && npm run lint:text",
|
|
44
|
+
"gen": "node scripts/gen-table.mjs",
|
|
45
|
+
"prebuild": "npm run gen",
|
|
46
|
+
"pretest": "npm run gen",
|
|
47
|
+
"prepublishOnly": "npm run check",
|
|
48
|
+
"prepack": "npm run build"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^26.4.0",
|
|
52
|
+
"textlint": "^15.8.0",
|
|
53
|
+
"textlint-rule-preset-ai-writing": "^1.1.0",
|
|
54
|
+
"typescript": "^5.7.2"
|
|
55
|
+
}
|
|
56
|
+
}
|