@homebridge-plugins/homebridge-ewelink 12.18.2 → 12.18.3-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.
- package/CHANGELOG.md +8 -0
- package/lib/fakegato/fakegato-history.js +37 -24
- package/lib/fakegato/fakegato-storage.js +47 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@homebridge-plugins/homebridge-ewelink` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v12.18.3 (Pending Release)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- fix: keep saving eve history for a device with a slash in its name
|
|
10
|
+
- fix: write eve history atomically, so an interrupted write cannot corrupt it
|
|
11
|
+
- fix: recover from an unreadable eve history instead of retrying forever
|
|
12
|
+
|
|
5
13
|
## v12.18.2 (2026-08-09)
|
|
6
14
|
|
|
7
15
|
### Changed
|
|
@@ -139,12 +139,15 @@ export default function (homebridge) {
|
|
|
139
139
|
(err, loaded) => {
|
|
140
140
|
if (err) {
|
|
141
141
|
this.log('FGH load error: %s.', err)
|
|
142
|
-
} else {
|
|
143
|
-
|
|
144
|
-
this.log('FGH loaded from storage.')
|
|
145
|
-
}
|
|
146
|
-
this.loaded = true
|
|
142
|
+
} else if (loaded) {
|
|
143
|
+
this.log('FGH loaded from storage.')
|
|
147
144
|
}
|
|
145
|
+
// Ready either way. A history that could not be read starts
|
|
146
|
+
// empty, which is the only thing left to do with it - leaving
|
|
147
|
+
// this unset instead would park save() in a 100ms retry that
|
|
148
|
+
// never ends, so a single unreadable file cost a busy loop for
|
|
149
|
+
// the lifetime of the plugin
|
|
150
|
+
this.loaded = true
|
|
148
151
|
},
|
|
149
152
|
)
|
|
150
153
|
}.bind(this),
|
|
@@ -593,27 +596,37 @@ export default function (homebridge) {
|
|
|
593
596
|
homebridge.globalFakeGatoStorage.read({
|
|
594
597
|
service: this,
|
|
595
598
|
callback: function (err, data) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
this.lastEntry = jsonFile.lastEntry
|
|
603
|
-
this.usedMemory = jsonFile.usedMemory
|
|
604
|
-
this.refTime = jsonFile.refTime
|
|
605
|
-
this.initialTime = jsonFile.initialTime
|
|
606
|
-
this.history = jsonFile.history
|
|
607
|
-
this.extra = jsonFile.extra
|
|
608
|
-
} catch (e) {
|
|
609
|
-
this.log('FGH error fetching data - invalid JSON [%s].', e)
|
|
610
|
-
cb(e, false)
|
|
611
|
-
}
|
|
612
|
-
cb(null, true)
|
|
613
|
-
}
|
|
614
|
-
} else {
|
|
599
|
+
// No history stored yet, or a file with nothing in it. There is
|
|
600
|
+
// nothing to restore either way, and the caller still has to be told
|
|
601
|
+
// so: an empty file used to answer nothing at all, which left the
|
|
602
|
+
// service waiting to be loaded while save() retried every 100ms for
|
|
603
|
+
// as long as the plugin ran
|
|
604
|
+
if (err || !data) {
|
|
615
605
|
cb(null, false)
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
// this.log('[%s] read data [%s].', this.accessoryName, data)
|
|
611
|
+
const jsonFile = typeof data === 'object' ? data : JSON.parse(data)
|
|
612
|
+
this.firstEntry = jsonFile.firstEntry
|
|
613
|
+
this.lastEntry = jsonFile.lastEntry
|
|
614
|
+
this.usedMemory = jsonFile.usedMemory
|
|
615
|
+
this.refTime = jsonFile.refTime
|
|
616
|
+
this.initialTime = jsonFile.initialTime
|
|
617
|
+
this.history = jsonFile.history
|
|
618
|
+
this.extra = jsonFile.extra
|
|
619
|
+
} catch (e) {
|
|
620
|
+
// A file that will not parse was cut short. The entries are gone
|
|
621
|
+
// whatever happens next, so the caller is told once and the service
|
|
622
|
+
// starts empty - this used to report the failure and then
|
|
623
|
+
// immediately report success as well, over the top of it
|
|
624
|
+
this.log('FGH error fetching data - invalid JSON [%s].', e)
|
|
625
|
+
cb(e, false)
|
|
626
|
+
return
|
|
616
627
|
}
|
|
628
|
+
|
|
629
|
+
cb(null, true)
|
|
617
630
|
}.bind(this),
|
|
618
631
|
})
|
|
619
632
|
}
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
4
5
|
|
|
5
6
|
const hostname = os.hostname().split('.')[0]
|
|
6
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' ? /[\\/]/g : /\//g
|
|
24
|
+
|
|
25
|
+
function toFileNamePart(name) {
|
|
26
|
+
return String(name ?? '').replace(PATH_SEPARATORS, '-')
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
export default class {
|
|
8
30
|
constructor(params) {
|
|
9
31
|
if (!params) {
|
|
@@ -27,7 +49,7 @@ export default class {
|
|
|
27
49
|
const newWriter = {
|
|
28
50
|
service,
|
|
29
51
|
callback: params.callback,
|
|
30
|
-
fileName: `${hostname}_${service.accessoryName}_persist.json`,
|
|
52
|
+
fileName: `${hostname}_${toFileNamePart(service.accessoryName)}_persist.json`,
|
|
31
53
|
}
|
|
32
54
|
const onReady = typeof params.onReady === 'function' ? params.onReady : () => {}
|
|
33
55
|
newWriter.storageHandler = fs
|
|
@@ -67,15 +89,36 @@ export default class {
|
|
|
67
89
|
? writer.callback
|
|
68
90
|
: () => {}
|
|
69
91
|
const fileLoc = path.join(writer.path, writer.fileName)
|
|
92
|
+
// Written to a temporary file and renamed over the target, so a write cut
|
|
93
|
+
// short by a restart or a power cut leaves the previous history intact
|
|
94
|
+
// rather than a half-written file that will not parse.
|
|
95
|
+
//
|
|
96
|
+
// The leading dot matters: this is HAP-NodeJS's persist directory, and it
|
|
97
|
+
// reads every file that does not start with one into memory at startup -
|
|
98
|
+
// so a temporary file left behind by a crash would be loaded, and kept,
|
|
99
|
+
// for the life of every run after it. The pid keeps child bridges sharing
|
|
100
|
+
// the directory from racing on the same temporary name.
|
|
101
|
+
const tempLoc = path.join(writer.path, `.${writer.fileName}.${process.pid}.tmp`)
|
|
70
102
|
this.log(
|
|
71
103
|
'[%s] FGS write file [%s] [%s].',
|
|
72
104
|
params.service.accessoryName,
|
|
73
105
|
fileLoc,
|
|
74
106
|
params.data.substring(1, 81),
|
|
75
107
|
)
|
|
76
|
-
writer.storageHandler.writeFile(
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
writer.storageHandler.writeFile(tempLoc, params.data, 'utf8', (writeErr) => {
|
|
109
|
+
if (writeErr) {
|
|
110
|
+
// the temporary file may never have been created, so a failure to
|
|
111
|
+
// remove it is not worth reporting over the write error itself
|
|
112
|
+
writer.storageHandler.unlink(tempLoc, () => {
|
|
113
|
+
this.writing = false
|
|
114
|
+
callBack(writeErr)
|
|
115
|
+
})
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
writer.storageHandler.rename(tempLoc, fileLoc, (renameErr) => {
|
|
119
|
+
this.writing = false
|
|
120
|
+
callBack(renameErr || null)
|
|
121
|
+
})
|
|
79
122
|
})
|
|
80
123
|
} else {
|
|
81
124
|
setTimeout(() => this.write(params), 100)
|
package/package.json
CHANGED