@homebridge-plugins/homebridge-wemo 7.7.1-beta.2 → 7.7.2-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 CHANGED
@@ -2,7 +2,15 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-wemo` will be documented in this file.
4
4
 
5
- ## v7.7.1 (Pending Release)
5
+ ## v7.7.2 (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
+
13
+ ## v7.7.1 (2026-08-10)
6
14
 
7
15
  ### Changed
8
16
 
@@ -16,6 +24,7 @@ All notable changes to `@homebridge-plugins/homebridge-wemo` will be documented
16
24
  - fix: cancel a garage door's settle timer when homebridge shuts down
17
25
  - fix: wait for the manually configured devices before reporting which ones failed to connect
18
26
  - fix: reject an interval so large it would make the plugin poll every millisecond
27
+ - chore(deps): dependency updates
19
28
 
20
29
  ## v7.7.0 (2026-08-02)
21
30
 
@@ -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
- if (loaded) {
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
- if (!err) {
597
- if (data) {
598
- try {
599
- // this.log('[%s] read data [%s].', this.accessoryName, data)
600
- const jsonFile = typeof data === 'object' ? data : JSON.parse(data)
601
- this.firstEntry = jsonFile.firstEntry
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(fileLoc, params.data, 'utf8', (...args) => {
77
- this.writing = false
78
- callBack(args)
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
@@ -2,7 +2,7 @@
2
2
  "name": "@homebridge-plugins/homebridge-wemo",
3
3
  "alias": "Wemo",
4
4
  "type": "module",
5
- "version": "7.7.1-beta.2",
5
+ "version": "7.7.2-beta.0",
6
6
  "description": "Homebridge plugin to integrate Wemo devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",
@@ -61,14 +61,14 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@homebridge/plugin-ui-utils": "^2.2.5",
64
- "axios": "^1.18.1",
64
+ "axios": "^1.19.0",
65
65
  "node-ssdp": "^4.0.1",
66
66
  "p-queue": "^9.3.3",
67
67
  "xml2js": "^0.6.2",
68
68
  "xmlbuilder": "^15.1.1"
69
69
  },
70
70
  "devDependencies": {
71
- "@antfu/eslint-config": "^9.2.0",
71
+ "@antfu/eslint-config": "^9.3.0",
72
72
  "@vitest/coverage-v8": "^4.1.10",
73
73
  "vitest": "^4.1.10"
74
74
  }