@homebridge-plugins/homebridge-govee 11.35.1-beta.1 → 11.36.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,13 +2,16 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-govee` will be documented in this file.
4
4
 
5
- ## v11.35.1 (Pending Release)
5
+ ## v11.36.0 (2026-08-11)
6
6
 
7
7
  ### Changed
8
8
 
9
9
  - fix: stop reading an h7173 startup frame as a kettle preset temperature (#1351)
10
10
  - fix: call the kettle's first temperature slot the target, not preset 1 (#1337)
11
11
  - feat: show how long the kettle will keep the water warm for (#1337)
12
+ - fix: keep saving eve history for a device with a slash in its name
13
+ - fix: write eve history atomically, so an interrupted write cannot corrupt it
14
+ - fix: recover from an unreadable eve history instead of retrying forever
12
15
 
13
16
  ## v11.35.0 (2026-08-09)
14
17
 
@@ -141,12 +141,15 @@ export default function (homebridge) {
141
141
  (err, loaded) => {
142
142
  if (err) {
143
143
  this.log('FGH load error: %s.', err)
144
- } else {
145
- if (loaded) {
146
- this.log('FGH loaded from storage.')
147
- }
148
- this.loaded = true
144
+ } else if (loaded) {
145
+ this.log('FGH loaded from storage.')
149
146
  }
147
+ // Ready either way. A history that could not be read starts
148
+ // empty, which is the only thing left to do with it - leaving
149
+ // this unset instead would park save() in a 100ms retry that
150
+ // never ends, so a single unreadable file cost a busy loop for
151
+ // the lifetime of the plugin
152
+ this.loaded = true
150
153
  },
151
154
  )
152
155
  }.bind(this),
@@ -595,27 +598,37 @@ export default function (homebridge) {
595
598
  homebridge.globalFakeGatoStorage.read({
596
599
  service: this,
597
600
  callback: function (err, data) {
598
- if (!err) {
599
- if (data) {
600
- try {
601
- // this.log('[%s] read data [%s].', this.accessoryName, data)
602
- const jsonFile = typeof data === 'object' ? data : JSON.parse(data)
603
- this.firstEntry = jsonFile.firstEntry
604
- this.lastEntry = jsonFile.lastEntry
605
- this.usedMemory = jsonFile.usedMemory
606
- this.refTime = jsonFile.refTime
607
- this.initialTime = jsonFile.initialTime
608
- this.history = jsonFile.history
609
- this.extra = jsonFile.extra
610
- } catch (e) {
611
- this.log('FGH error fetching data - invalid JSON [%s].', e)
612
- cb(e, false)
613
- }
614
- cb(null, true)
615
- }
616
- } else {
601
+ // No history stored yet, or a file with nothing in it. There is
602
+ // nothing to restore either way, and the caller still has to be told
603
+ // so: an empty file used to answer nothing at all, which left the
604
+ // service waiting to be loaded while save() retried every 100ms for
605
+ // as long as the plugin ran
606
+ if (err || !data) {
617
607
  cb(null, false)
608
+ return
609
+ }
610
+
611
+ try {
612
+ // this.log('[%s] read data [%s].', this.accessoryName, data)
613
+ const jsonFile = typeof data === 'object' ? data : JSON.parse(data)
614
+ this.firstEntry = jsonFile.firstEntry
615
+ this.lastEntry = jsonFile.lastEntry
616
+ this.usedMemory = jsonFile.usedMemory
617
+ this.refTime = jsonFile.refTime
618
+ this.initialTime = jsonFile.initialTime
619
+ this.history = jsonFile.history
620
+ this.extra = jsonFile.extra
621
+ } catch (e) {
622
+ // A file that will not parse was cut short. The entries are gone
623
+ // whatever happens next, so the caller is told once and the service
624
+ // starts empty - this used to report the failure and then
625
+ // immediately report success as well, over the top of it
626
+ this.log('FGH error fetching data - invalid JSON [%s].', e)
627
+ cb(e, false)
628
+ return
618
629
  }
630
+
631
+ cb(null, true)
619
632
  }.bind(this),
620
633
  })
621
634
  }
@@ -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-govee",
3
3
  "alias": "Govee",
4
4
  "type": "module",
5
- "version": "11.35.1-beta.1",
5
+ "version": "11.36.0",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",