homebridge-homiris 0.3.8 → 0.4.3

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,6 +2,30 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.4.3
6
+
7
+ - [CHANGE] Default `name` in the config schema is now `Homiris` instead of `SepsadSecurity`. This is the log-prefix label shown in Homebridge output (`[Homiris] INFO - …`) — display only. The platform identifier (`"platform": "SepsadSecurity"`) is unchanged for backward compatibility, so existing configs keep working untouched.
8
+ - [DOCS] Readme config example now includes `"name": "Homiris"` and clarifies the `platform` vs `name` distinction.
9
+
10
+ Existing users who want the new prefix in their logs need to add `"name": "Homiris"` to their platform block in `config.json` (or set it via the Homebridge UI). The schema default only takes effect for fresh installs.
11
+
12
+ ## 0.4.2
13
+
14
+ - [CHANGE] Replaced `fakegato-history` runtime dependency with a vendored, single-file implementation at `lib/fakegato.js`. Trimmed to temperature-only (single `0102` signature, advertised as such instead of as a `weather` station with phantom 0% humidity / 0 hPa pressure). Plugin now has zero runtime dependencies again.
15
+ - [FIX] Eve's `S2R1`/`S2R2`/`S2W1`/`S2W2` characteristics are now registered as optional on the history service before being added, so HAP-NodeJS no longer logs "Characteristic not in required or optional characteristic section" warnings at startup.
16
+ - [INTERNAL] Persist file format and path are unchanged — existing v0.4.x history is preserved across the upgrade.
17
+ - [INTERNAL] Vendored code preserves upstream MIT attribution to simont77 (full license header in `lib/fakegato.js`).
18
+
19
+ ## 0.4.1
20
+
21
+ - [FIX] Eve history service was being removed by the platform's service cleanup at startup. The `fakegato-history` constructor itself attaches the history service to the accessory; the plugin was additionally adding the constructed instance under a custom subtype and confirming that one, leaving the library's real service unconfirmed and stripped — which silently broke history recording. The plugin now confirms the service the library actually attached.
22
+
23
+ ## 0.4.0
24
+
25
+ - [NEW] Optional Eve app temperature history (graphs, min/max, weekly summaries) via `fakegato-history`. Disabled by default; enable with `eveHistory: true` in the plugin config
26
+ - [NEW] When `eveHistory` is enabled, temperature is polled every 10 minutes by default — or every `refreshTimer` seconds if that is set — overriding the disabled background polling default. The existing background poll (which previously only refreshed alarm state) now also refreshes temperature when Eve history is on
27
+ - [DEP] Adds `fakegato-history` runtime dependency
28
+
5
29
  ## 0.3.8
6
30
 
7
31
  - [FIX] Transparent re-authentication on `403 SESSION_EXPIREE` — the `idSession` returned by `/connect` expires before the OAuth `access_token`, so requests now detect session expiration, refresh the session, and retry once instead of failing and waiting for the 1-minute retry timer
@@ -11,8 +11,8 @@
11
11
  "name": {
12
12
  "title": "Name",
13
13
  "type": "string",
14
- "default": "SepsadSecurity",
15
- "description": "The name that appears in the Homebridge log"
14
+ "default": "Homiris",
15
+ "description": "The name that appears in the Homebridge log prefix. Display only — does not affect the platform identifier."
16
16
  },
17
17
  "login": {
18
18
  "title": "Sepsad/Homiris account login",
@@ -27,9 +27,9 @@
27
27
  "type": "string",
28
28
  "default": "HOMIRIS",
29
29
  "oneOf": [
30
- { "title": "Homiris", "enum": ["HOMIRIS"] },
31
- { "title": "Sepsad", "enum": ["SEPSAD"] },
32
- { "title": "EPS", "enum": ["EPS"] }
30
+ {"title": "Homiris", "enum": ["HOMIRIS"]},
31
+ {"title": "Sepsad", "enum": ["SEPSAD"]},
32
+ {"title": "EPS", "enum": ["EPS"]}
33
33
  ]
34
34
  },
35
35
  "allowActivation": {
@@ -44,6 +44,12 @@
44
44
  "maximum": 3600,
45
45
  "description": "Refresh alarm state every X seconds. Leave empty to disable. Minimum 120s to avoid rate limiting by Homiris servers."
46
46
  },
47
+ "eveHistory": {
48
+ "title": "Enable Eve app temperature history",
49
+ "type": "boolean",
50
+ "default": false,
51
+ "description": "Expose temperature sensors with Eve-compatible history (graphs, min/max). When enabled, temperature is polled every 10 minutes (or every 'Refresh timer' seconds if that is set), overriding the disabled background polling default."
52
+ },
47
53
  "maxWaitTimeForOperation": {
48
54
  "title": "Maximum wait time during operation",
49
55
  "type": "integer",
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- var Service, Characteristic, Accessory, UUIDGen;
1
+ var Service, Characteristic, Accessory, UUIDGen, FakeGatoHistoryService;
2
2
 
3
3
  var HomirisAPI = require('./homirisAPI.js').HomirisAPI;
4
4
  const HomirisTools = require('./homirisTools.js');
@@ -19,6 +19,14 @@ function myHomirisPlatform(log, config, api) {
19
19
  ? HomirisTools.checkParameter(config['refreshTimer'], 120, 3600, 300)
20
20
  : 0;
21
21
 
22
+ this.eveHistory = HomirisTools.checkBoolParameter(config['eveHistory'], false);
23
+
24
+ // Eve history needs periodic temperature samples. If the user hasn't set a
25
+ // refreshTimer, fall back to a 10-minute cadence — the interval Eve expects.
26
+ if (this.eveHistory && this.refreshTimer === 0) {
27
+ this.refreshTimer = 600;
28
+ }
29
+
22
30
  this.refreshTimerDuringOperation = HomirisTools.checkParameter(
23
31
  config['refreshTimerDuringOperation'],
24
32
  2,
@@ -77,6 +85,7 @@ module.exports = function (homebridge) {
77
85
  Characteristic = homebridge.hap.Characteristic;
78
86
  Accessory = homebridge.platformAccessory;
79
87
  UUIDGen = homebridge.hap.uuid;
88
+ FakeGatoHistoryService = require('./lib/fakegato')(homebridge);
80
89
  homebridge.registerPlatform('SepsadSecurity', myHomirisPlatform);
81
90
  };
82
91
 
@@ -113,11 +122,7 @@ myHomirisPlatform.prototype = {
113
122
  }
114
123
 
115
124
  if (accstoRemove.length > 0)
116
- this.api.unregisterPlatformAccessories(
117
- 'homebridge-homiris',
118
- 'SepsadSecurity',
119
- accstoRemove
120
- );
125
+ this.api.unregisterPlatformAccessories('homebridge-homiris', 'SepsadSecurity', accstoRemove);
121
126
  },
122
127
 
123
128
  cleanServices: function () {
@@ -158,9 +163,7 @@ myHomirisPlatform.prototype = {
158
163
 
159
164
  this.homirisAPI.on('securitySystemRefreshed', () => {
160
165
  this.log.debug('INFO - securitySystemRefreshed event');
161
- this.log.debug(
162
- 'INFO - SecuritySystem : ' + JSON.stringify(this.homirisAPI.securitySystem)
163
- );
166
+ this.log.debug('INFO - SecuritySystem : ' + JSON.stringify(this.homirisAPI.securitySystem));
164
167
 
165
168
  if (!this.loaded) {
166
169
  this.loadSecuritySystem();
@@ -269,8 +272,7 @@ myHomirisPlatform.prototype = {
269
272
  for (let a = 0; a < smokeDetectors.length; a++) {
270
273
  let smokeSensorName = smokeDetectors[a].label;
271
274
  let smokeSensorModel = this.homirisAPI.securitySystem.model;
272
- let smokeSensorSeriaNumber =
273
- this.homirisAPI.securitySystem.id + '/' + smokeDetectors[a].id;
275
+ let smokeSensorSeriaNumber = this.homirisAPI.securitySystem.id + '/' + smokeDetectors[a].id;
274
276
 
275
277
  this.log('INFO - Discovered SmokeSensor : ' + smokeSensorName);
276
278
 
@@ -344,8 +346,7 @@ myHomirisPlatform.prototype = {
344
346
  for (let a = 0; a < tempSensors.length; a++) {
345
347
  let tempSensorName = tempSensors[a].label;
346
348
  let tempSensorModel = this.homirisAPI.securitySystem.model;
347
- let tempSensorSeriaNumber =
348
- this.homirisAPI.securitySystem.id + '/' + tempSensors[a].id;
349
+ let tempSensorSeriaNumber = this.homirisAPI.securitySystem.id + '/' + tempSensors[a].id;
349
350
 
350
351
  this.log('INFO - Discovered TemperatureSensor : ' + tempSensorName);
351
352
 
@@ -394,6 +395,25 @@ myHomirisPlatform.prototype = {
394
395
 
395
396
  this.bindCurrentTemperatureCharacteristic(HKTempSensorService);
396
397
 
398
+ if (this.eveHistory) {
399
+ if (!myTempSensorAccessory.loggingService) {
400
+ this.log('INFO - Creating Eve history Service ' + tempSensorName);
401
+ // The factory attaches its own HistoryService to the accessory and
402
+ // exposes it as `.service`. Don't call addService manually.
403
+ myTempSensorAccessory.loggingService = new FakeGatoHistoryService(
404
+ 'temperature',
405
+ myTempSensorAccessory,
406
+ {
407
+ path: this.api.user.persistPath(),
408
+ log: this.log,
409
+ }
410
+ );
411
+ }
412
+ if (myTempSensorAccessory.loggingService.service) {
413
+ this._confirmedServices.push(myTempSensorAccessory.loggingService.service);
414
+ }
415
+ }
416
+
397
417
  this._confirmedAccessories.push(myTempSensorAccessory);
398
418
  this._confirmedServices.push(HKTempSensorService);
399
419
 
@@ -438,9 +458,7 @@ myHomirisPlatform.prototype = {
438
458
  //timer for background refresh
439
459
  this.refreshBackground();
440
460
  } else {
441
- this.log(
442
- 'ERROR - discoverSecuritySystem - no security system found, will retry in 1 minute'
443
- );
461
+ this.log('ERROR - discoverSecuritySystem - no security system found, will retry in 1 minute');
444
462
 
445
463
  setTimeout(() => {
446
464
  this.homirisAPI.getSecuritySystem();
@@ -490,10 +508,7 @@ myHomirisPlatform.prototype = {
490
508
 
491
509
  updateTemperature() {
492
510
  for (let a = 0; a < this.foundAccessories.length; a++) {
493
- if (
494
- this.foundAccessories[a].tempSensorID &&
495
- this.homirisAPI.securitySystem.temperatureInfo
496
- ) {
511
+ if (this.foundAccessories[a].tempSensorID && this.homirisAPI.securitySystem.temperatureInfo) {
497
512
  this.log.debug('INFO - refreshing temp Sensor - ' + this.foundAccessories[a].name);
498
513
 
499
514
  let tempResults = this.homirisAPI.securitySystem.temperatureInfo;
@@ -759,10 +774,12 @@ myHomirisPlatform.prototype = {
759
774
  this.log.debug(
760
775
  'INFO - Setting Timer for background refresh every : ' + this.refreshTimer + 's'
761
776
  );
762
- this.timerID = setInterval(
763
- () => this.homirisAPI.getSecuritySystem(),
764
- this.refreshTimer * 1000
765
- );
777
+ this.timerID = setInterval(() => {
778
+ this.homirisAPI.getSecuritySystem();
779
+ if (this.eveHistory && this.tempLoaded) {
780
+ this.homirisAPI.getTemperature();
781
+ }
782
+ }, this.refreshTimer * 1000);
766
783
  }
767
784
  },
768
785
 
@@ -882,6 +899,13 @@ myHomirisPlatform.prototype = {
882
899
 
883
900
  if (newTemp != currentTemp)
884
901
  HKTempSensorService.getCharacteristic(Characteristic.CurrentTemperature).updateValue(newTemp);
902
+
903
+ if (this.eveHistory && myTempAccessory.loggingService && newTemp !== undefined) {
904
+ myTempAccessory.loggingService.addEntry({
905
+ time: Math.floor(Date.now() / 1000),
906
+ temp: newTemp,
907
+ });
908
+ }
885
909
  },
886
910
 
887
911
  refreshSmokeSensor: function (mySmokeAccessory, result) {
@@ -921,7 +945,8 @@ myHomirisPlatform.prototype = {
921
945
  );
922
946
  return;
923
947
  } else {
924
- let currentSmokeStatus = HKSmokeSensorService.getCharacteristic(Characteristic.SmokeDetected).value;
948
+ let currentSmokeStatus = HKSmokeSensorService.getCharacteristic(Characteristic.SmokeDetected)
949
+ .value;
925
950
  if (currentSmokeStatus != newSmokeStatus)
926
951
  HKSmokeSensorService.getCharacteristic(Characteristic.SmokeDetected).updateValue(
927
952
  newSmokeStatus
@@ -0,0 +1,368 @@
1
+ /*
2
+ * Trimmed Eve-history service for homebridge-homiris.
3
+ *
4
+ * Derived from fakegato-history (https://github.com/simont77/fakegato-history),
5
+ * MIT-licensed:
6
+ *
7
+ * Copyright (c) 2017 simont77
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a
10
+ * copy of this software and associated documentation files (the "Software"),
11
+ * to deal in the Software without restriction, including without limitation
12
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13
+ * and/or sell copies of the Software, and to permit persons to whom the
14
+ * Software is furnished to do so, subject to the following conditions:
15
+ *
16
+ * The above copyright notice and this permission notice shall be included
17
+ * in all copies or substantial portions of the Software.
18
+ *
19
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
22
+ * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
23
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
24
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
25
+ * USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+ *
27
+ * Modifications for homebridge-homiris (2026, Nicolas Roughol):
28
+ * - Stripped to temperature-only (single-field 0102 signature) — no humidity,
29
+ * pressure, motion, door, switch, energy, custom, googleDrive, or
30
+ * averaging-timer code paths.
31
+ * - Synchronous fs load and queued async writes, no retry-loop / global
32
+ * storage manager.
33
+ * - Eve characteristics registered as optional on the service so HAP-NodeJS
34
+ * does not log "characteristic not in required or optional" warnings.
35
+ * - Single file, Homebridge v2 / HAP-NodeJS v2 compatible.
36
+ */
37
+
38
+ 'use strict';
39
+
40
+ const fs = require('fs');
41
+ const os = require('os');
42
+ const path = require('path');
43
+ const {format: sprintf} = require('util');
44
+
45
+ const EPOCH_OFFSET = 978307200; // Eve uses 2001-01-01 epoch
46
+ const HISTORY_UUID = 'E863F007-079E-48FF-8F27-9C2605A29F52';
47
+ const S2R1_UUID = 'E863F116-079E-48FF-8F27-9C2605A29F52';
48
+ const S2R2_UUID = 'E863F117-079E-48FF-8F27-9C2605A29F52';
49
+ const S2W1_UUID = 'E863F11C-079E-48FF-8F27-9C2605A29F52';
50
+ const S2W2_UUID = 'E863F121-079E-48FF-8F27-9C2605A29F52';
51
+
52
+ const HISTORY_SIZE = 4032; // ring-buffer capacity (matches Eve client expectation)
53
+ const FILE_SUFFIX = '_persist.json';
54
+ const HOSTNAME = os.hostname().split('.')[0];
55
+
56
+ // Single-field "temperature only" signature.
57
+ // 01 0102 -> 1 field, signature 0102 (CurrentTemperature, 2 bytes, factor 100)
58
+ // 01 -> bitmap with bit 0 set
59
+ const TYPE116_TEMP = '01 0102';
60
+ const TYPE117_TEMP = '01';
61
+
62
+ const hexToBase64 = (val) =>
63
+ Buffer.from(String(val).replace(/[^0-9A-F]/gi, ''), 'hex').toString('base64');
64
+ const base64ToHex = (val) => (val ? Buffer.from(val, 'base64').toString('hex') : val);
65
+ const swap16 = (val) => ((val & 0xff) << 8) | ((val >>> 8) & 0xff);
66
+ const swap32 = (val) =>
67
+ ((val & 0xff) << 24) | ((val & 0xff00) << 8) | ((val >>> 8) & 0xff00) | ((val >>> 24) & 0xff);
68
+ const numToHex = (val, len) => {
69
+ let s = Number(val >>> 0).toString(16);
70
+ if (s.length % 2 !== 0) s = '0' + s;
71
+ return len ? ('0000000000000' + s).slice(-len) : s;
72
+ };
73
+
74
+ module.exports = function (homebridge) {
75
+ const Service = homebridge.hap.Service;
76
+ const Characteristic = homebridge.hap.Characteristic;
77
+ const Formats = homebridge.hap.Formats;
78
+ const Perms = homebridge.hap.Perms;
79
+
80
+ class S2R1Characteristic extends Characteristic {
81
+ constructor() {
82
+ super('S2R1', S2R1_UUID);
83
+ this.setProps({
84
+ format: Formats.DATA,
85
+ perms: [Perms.PAIRED_READ, Perms.NOTIFY, Perms.HIDDEN],
86
+ });
87
+ }
88
+ }
89
+ S2R1Characteristic.UUID = S2R1_UUID;
90
+
91
+ class S2R2Characteristic extends Characteristic {
92
+ constructor() {
93
+ super('S2R2', S2R2_UUID);
94
+ this.setProps({
95
+ format: Formats.DATA,
96
+ perms: [Perms.PAIRED_READ, Perms.NOTIFY, Perms.HIDDEN],
97
+ });
98
+ }
99
+ }
100
+ S2R2Characteristic.UUID = S2R2_UUID;
101
+
102
+ class S2W1Characteristic extends Characteristic {
103
+ constructor() {
104
+ super('S2W1', S2W1_UUID);
105
+ this.setProps({
106
+ format: Formats.DATA,
107
+ perms: [Perms.PAIRED_WRITE, Perms.HIDDEN],
108
+ });
109
+ }
110
+ }
111
+ S2W1Characteristic.UUID = S2W1_UUID;
112
+
113
+ class S2W2Characteristic extends Characteristic {
114
+ constructor() {
115
+ super('S2W2', S2W2_UUID);
116
+ this.setProps({
117
+ format: Formats.DATA,
118
+ perms: [Perms.PAIRED_WRITE, Perms.HIDDEN],
119
+ });
120
+ }
121
+ }
122
+ S2W2Characteristic.UUID = S2W2_UUID;
123
+
124
+ class HistoryService extends Service {
125
+ constructor(displayName, subtype) {
126
+ super(displayName, HISTORY_UUID, subtype);
127
+ // Register before adding so HAP-NodeJS doesn't warn about the
128
+ // characteristics being absent from the service definition.
129
+ this.addOptionalCharacteristic(S2R1Characteristic);
130
+ this.addOptionalCharacteristic(S2R2Characteristic);
131
+ this.addOptionalCharacteristic(S2W1Characteristic);
132
+ this.addOptionalCharacteristic(S2W2Characteristic);
133
+ this.addCharacteristic(S2R1Characteristic);
134
+ this.addCharacteristic(S2R2Characteristic);
135
+ this.addCharacteristic(S2W1Characteristic);
136
+ this.addCharacteristic(S2W2Characteristic);
137
+ }
138
+ }
139
+ HistoryService.UUID = HISTORY_UUID;
140
+
141
+ class TemperatureHistory {
142
+ constructor(accessory, options) {
143
+ options = options || {};
144
+ this.accessory = accessory;
145
+ this.accessoryName = accessory.displayName;
146
+ this.log = options.log || accessory.log || {debug: () => {}};
147
+ if (!this.log.debug) this.log.debug = () => {};
148
+
149
+ this.size = options.size || HISTORY_SIZE;
150
+ this.path = options.path || path.join(os.homedir(), '.homebridge');
151
+ this.filename = options.filename || HOSTNAME + '_' + this.accessoryName + FILE_SUFFIX;
152
+
153
+ this.history = ['noValue'];
154
+ this.firstEntry = 0;
155
+ this.lastEntry = 0;
156
+ this.usedMemory = 0;
157
+ this.refTime = 0;
158
+ this.initialTime = 0;
159
+ this.currentEntry = 1;
160
+ this.transfer = false;
161
+ this.setTime = true;
162
+ this.restarted = true;
163
+ this.memoryAddress = 0;
164
+ this.dataStream = '';
165
+ this.extra = undefined;
166
+
167
+ this._writePending = null;
168
+ this._writing = false;
169
+
170
+ this._load();
171
+
172
+ // Reuse the existing service if cached, otherwise add a fresh one.
173
+ this.service = accessory.getService(HistoryService);
174
+ if (!this.service) {
175
+ this.service = accessory.addService(
176
+ HistoryService,
177
+ this.accessoryName + ' History',
178
+ 'temperature'
179
+ );
180
+ }
181
+
182
+ this.service.getCharacteristic(S2R2Characteristic).on('get', this._getS2R2.bind(this));
183
+ this.service.getCharacteristic(S2W1Characteristic).on('set', this._setS2W1.bind(this));
184
+ this.service.getCharacteristic(S2W2Characteristic).on('set', this._setS2W2.bind(this));
185
+ }
186
+
187
+ addEntry(entry) {
188
+ if (entry == null || typeof entry.time !== 'number' || typeof entry.temp !== 'number') {
189
+ return;
190
+ }
191
+ this._addEntry({time: entry.time, temp: entry.temp});
192
+ }
193
+
194
+ _addEntry(entry) {
195
+ const entry2address = (val) => val % this.size;
196
+
197
+ if (this.usedMemory < this.size) {
198
+ this.usedMemory++;
199
+ this.firstEntry = 0;
200
+ this.lastEntry = this.usedMemory;
201
+ } else {
202
+ this.firstEntry++;
203
+ this.lastEntry = this.firstEntry + this.usedMemory;
204
+ if (this.restarted) {
205
+ this.history[entry2address(this.lastEntry)] = {
206
+ time: entry.time,
207
+ setRefTime: 1,
208
+ };
209
+ this.firstEntry++;
210
+ this.lastEntry = this.firstEntry + this.usedMemory;
211
+ this.restarted = false;
212
+ }
213
+ }
214
+
215
+ if (this.refTime === 0) {
216
+ this.refTime = entry.time - EPOCH_OFFSET;
217
+ this.history[this.lastEntry] = {time: entry.time, setRefTime: 1};
218
+ this.initialTime = entry.time;
219
+ this.lastEntry++;
220
+ this.usedMemory++;
221
+ }
222
+
223
+ this.history[entry2address(this.lastEntry)] = entry;
224
+
225
+ // S2R1 manifest: tells Eve the current ring-buffer state.
226
+ const usedMem = this.usedMemory < this.size ? this.usedMemory + 1 : this.usedMemory;
227
+ const firstIdx = this.usedMemory < this.size ? this.firstEntry : this.firstEntry + 1;
228
+ const val = sprintf(
229
+ '%s00000000%s%s%s%s%s000000000101',
230
+ numToHex(swap32(entry.time - this.refTime - EPOCH_OFFSET), 8),
231
+ numToHex(swap32(this.refTime), 8),
232
+ TYPE116_TEMP,
233
+ numToHex(swap16(usedMem), 4),
234
+ numToHex(swap16(this.size), 4),
235
+ numToHex(swap32(firstIdx), 8)
236
+ );
237
+
238
+ this.service.getCharacteristic(S2R1Characteristic).setValue(hexToBase64(val));
239
+ this.log.debug(
240
+ '**fakegato %s: lastEntry=%d usedMemory=%d',
241
+ this.accessoryName,
242
+ this.lastEntry,
243
+ this.usedMemory
244
+ );
245
+
246
+ this._save();
247
+ }
248
+
249
+ _getS2R2(callback) {
250
+ const entry2address = (val) => val % this.size;
251
+
252
+ if (this.currentEntry <= this.lastEntry && this.transfer) {
253
+ this.memoryAddress = entry2address(this.currentEntry);
254
+ for (let i = 0; i < 11; i++) {
255
+ const slot = this.history[this.memoryAddress];
256
+ if (
257
+ (slot && slot.setRefTime === 1) ||
258
+ this.setTime ||
259
+ this.currentEntry === this.firstEntry + 1
260
+ ) {
261
+ this.dataStream += sprintf(
262
+ ',15%s 0100 0000 81%s0000 0000 00 0000',
263
+ numToHex(swap32(this.currentEntry), 8),
264
+ numToHex(swap32(this.refTime), 8)
265
+ );
266
+ this.setTime = false;
267
+ } else {
268
+ // Temp-only entry: 0x0c (12) bytes.
269
+ // 1B length + 4B entry idx + 4B time delta + 1B bitmap + 2B temp = 12B
270
+ this.dataStream += sprintf(
271
+ ',0c %s%s-%s:%s',
272
+ numToHex(swap32(this.currentEntry), 8),
273
+ numToHex(swap32(slot.time - this.refTime - EPOCH_OFFSET), 8),
274
+ TYPE117_TEMP,
275
+ numToHex(swap16(Math.round(slot.temp * 100)), 4)
276
+ );
277
+ }
278
+ this.currentEntry++;
279
+ this.memoryAddress = entry2address(this.currentEntry);
280
+ if (this.currentEntry > this.lastEntry) break;
281
+ }
282
+ this.log.debug('**fakegato S2R2 %s: %s', this.accessoryName, this.dataStream);
283
+ callback(null, hexToBase64(this.dataStream));
284
+ this.dataStream = '';
285
+ } else {
286
+ this.transfer = false;
287
+ callback(null, hexToBase64('00'));
288
+ }
289
+ }
290
+
291
+ _setS2W1(val, callback) {
292
+ callback(null);
293
+ const valHex = base64ToHex(val);
294
+ const substring = valHex.substring(4, 12);
295
+ const valInt = parseInt(substring, 16);
296
+ const address = swap32(valInt);
297
+ this.log.debug('**fakegato S2W1 %s: address=%s', this.accessoryName, address.toString(16));
298
+ this.currentEntry = address !== 0 ? address : 1;
299
+ this.transfer = true;
300
+ }
301
+
302
+ _setS2W2(val, callback) {
303
+ this.log.debug('**fakegato S2W2 %s: clock adjust %s', this.accessoryName, base64ToHex(val));
304
+ callback(null);
305
+ }
306
+
307
+ _filePath() {
308
+ return path.join(this.path, this.filename);
309
+ }
310
+
311
+ _load() {
312
+ try {
313
+ const raw = fs.readFileSync(this._filePath(), 'utf8');
314
+ const data = JSON.parse(raw);
315
+ this.firstEntry = data.firstEntry || 0;
316
+ this.lastEntry = data.lastEntry || 0;
317
+ this.usedMemory = data.usedMemory || 0;
318
+ this.refTime = data.refTime || 0;
319
+ this.initialTime = data.initialTime || 0;
320
+ this.history = Array.isArray(data.history) ? data.history : ['noValue'];
321
+ this.extra = data.extra;
322
+ this.log.debug('**fakegato loaded %s: %d entries', this.accessoryName, this.usedMemory);
323
+ } catch (err) {
324
+ if (err.code !== 'ENOENT') {
325
+ this.log.debug('**fakegato load error %s: %s', this.accessoryName, err.message);
326
+ }
327
+ }
328
+ }
329
+
330
+ _save() {
331
+ const data = JSON.stringify({
332
+ firstEntry: this.firstEntry,
333
+ lastEntry: this.lastEntry,
334
+ usedMemory: this.usedMemory,
335
+ refTime: this.refTime,
336
+ initialTime: this.initialTime,
337
+ history: this.history,
338
+ extra: this.extra,
339
+ });
340
+
341
+ // Coalesce concurrent writes — keep only the latest pending payload.
342
+ this._writePending = data;
343
+ if (this._writing) return;
344
+ this._writing = true;
345
+
346
+ const flush = () => {
347
+ const payload = this._writePending;
348
+ this._writePending = null;
349
+ fs.writeFile(this._filePath(), payload, 'utf8', (err) => {
350
+ if (err) this.log.debug('**fakegato save error %s: %s', this.accessoryName, err.message);
351
+ if (this._writePending) {
352
+ flush();
353
+ } else {
354
+ this._writing = false;
355
+ }
356
+ });
357
+ };
358
+ flush();
359
+ }
360
+ }
361
+
362
+ // Public factory: keep the same call signature the plugin already uses.
363
+ function FakegatoFactory(_accessoryType, accessory, options) {
364
+ return new TemperatureHistory(accessory, options);
365
+ }
366
+ FakegatoFactory.UUID = HISTORY_UUID;
367
+ return FakegatoFactory;
368
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-homiris",
3
- "version": "0.3.8",
3
+ "version": "0.4.3",
4
4
  "author": "Nicolas Roughol",
5
5
  "description": "Homebridge plugin for Homiris / Sepsad / EPS alarm systems",
6
6
  "main": "index.js",
package/readme.md CHANGED
@@ -6,16 +6,32 @@ Forked from [homebridge-sepsadsecurity](https://github.com/nicoduj/homebridge-se
6
6
 
7
7
  ## What's new
8
8
 
9
+ ### 0.4.3
10
+
11
+ - Default `name` (the log-prefix label) is now `Homiris` instead of `SepsadSecurity`. Display only — `platform` stays `SepsadSecurity` for backward compatibility. To get the new prefix in your existing install's logs, add `"name": "Homiris"` to your `config.json` platform block.
12
+
13
+ ### 0.4.2
14
+
15
+ - Replaced the `fakegato-history` dependency with a vendored, temperature-only implementation in `lib/fakegato.js`. Cleans up the startup `S2R2/S2W1/S2W2` characteristic warnings, advertises a temp-only signature to Eve (no more phantom 0% humidity / 0 hPa pressure), and restores zero runtime dependencies. Existing persist files are preserved across the upgrade.
16
+
17
+ ### 0.4.0
18
+
19
+ - Optional Eve app temperature history (graphs, min/max). Disabled by default; enable with `eveHistory: true`. When enabled, temperature is polled every 10 minutes (or every `refreshTimer` seconds if set), overriding the disabled background polling default.
20
+
9
21
  ### 0.3.4
22
+
10
23
  - Reverted disarm support — Homiris API requires biometric device auth for sensitive actions
11
24
 
12
25
  ### 0.3.2
26
+
13
27
  - Fixed activation endpoint: was missing `smartphone/production/1.0.0/` prefix
14
28
 
15
29
  ### 0.3.1
30
+
16
31
  - Fixed accessory registration using the old plugin name, which caused "no loaded plugin could be found" warnings
17
32
 
18
33
  ### 0.3.0
34
+
19
35
  - **Fixed API compatibility** -- the Homiris/EPS API started requiring `User-Agent` and other headers on all requests; the original plugin only sent them on login
20
36
  - **Homebridge v2.0 + v1.11 support** -- replaced removed `getServiceByUUIDAndSubType()` API
21
37
  - **Zero runtime dependencies** -- replaced deprecated `request` library with native `fetch()`, removed `locks`
@@ -42,6 +58,7 @@ Or search for `homebridge-homiris` in the Homebridge UI plugins tab.
42
58
  "platforms": [
43
59
  {
44
60
  "platform": "SepsadSecurity",
61
+ "name": "Homiris",
45
62
  "login": "123456",
46
63
  "password": "your-password",
47
64
  "originSession": "HOMIRIS"
@@ -49,19 +66,22 @@ Or search for `homebridge-homiris` in the Homebridge UI plugins tab.
49
66
  ]
50
67
  ```
51
68
 
69
+ `platform` must remain `"SepsadSecurity"` (the plugin's stable identifier — kept for backward compatibility with existing installs); `name` is just the log-prefix label and can be set freely.
70
+
52
71
  ### Fields
53
72
 
54
- | Field | Required | Default | Description |
55
- |-------|----------|---------|-------------|
56
- | `platform` | Yes | | Must be `"SepsadSecurity"` |
57
- | `login` | Yes | | Your Homiris/Sepsad account login |
58
- | `password` | Yes | | Your Homiris/Sepsad account password |
59
- | `originSession` | No | `"HOMIRIS"` | `"HOMIRIS"`, `"SEPSAD"`, or `"EPS"` depending on your system brand |
60
- | `allowActivation` | No | `false` | Set to `true` to allow arming the system via HomeKit |
61
- | `refreshTimer` | No | disabled | Refresh alarm state every X seconds (120--3600). Leave empty to disable |
62
- | `maxWaitTimeForOperation` | No | `30` | Max seconds to wait for an arm operation to complete (30--90) |
63
- | `refreshTimerDuringOperation` | No | `10` | Polling interval in seconds while an arm operation is in progress (2--15) |
64
- | `cleanCache` | No | `false` | Set to `true` to remove cached accessories on next restart, then remove the option |
73
+ | Field | Required | Default | Description |
74
+ | ----------------------------- | -------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |
75
+ | `platform` | Yes | | Must be `"SepsadSecurity"` |
76
+ | `login` | Yes | | Your Homiris/Sepsad account login |
77
+ | `password` | Yes | | Your Homiris/Sepsad account password |
78
+ | `originSession` | No | `"HOMIRIS"` | `"HOMIRIS"`, `"SEPSAD"`, or `"EPS"` depending on your system brand |
79
+ | `allowActivation` | No | `false` | Set to `true` to allow arming the system via HomeKit |
80
+ | `refreshTimer` | No | disabled | Refresh alarm state every X seconds (120--3600). Leave empty to disable |
81
+ | `eveHistory` | No | `false` | Set to `true` to expose temperature sensors with Eve-compatible history. Forces a 10-minute temperature poll if `refreshTimer` is unset |
82
+ | `maxWaitTimeForOperation` | No | `30` | Max seconds to wait for an arm operation to complete (30--90) |
83
+ | `refreshTimerDuringOperation` | No | `10` | Polling interval in seconds while an arm operation is in progress (2--15) |
84
+ | `cleanCache` | No | `false` | Set to `true` to remove cached accessories on next restart, then remove the option |
65
85
 
66
86
  ### Migrating from homebridge-sepsadsecurity
67
87
 
@@ -74,7 +94,7 @@ Or search for `homebridge-homiris` in the Homebridge UI plugins tab.
74
94
 
75
95
  - **Security System** -- arm state (Away/Home/Night/Off), mapped from Homiris TOTAL/PARTIAL/OFF modes
76
96
  - **Smoke Sensors** -- one per smoke detector reported by your system
77
- - **Temperature Sensors** -- one per temperature probe (if your system has any)
97
+ - **Temperature Sensors** -- one per temperature probe (if your system has any). With `eveHistory` enabled, history is also exposed to the Eve app (graphs, min/max, weekly summaries)
78
98
 
79
99
  ## Requirements
80
100