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 +24 -0
- package/config.schema.json +11 -5
- package/index.js +50 -25
- package/lib/fakegato.js +368 -0
- package/package.json +1 -1
- package/readme.md +32 -12
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
|
package/config.schema.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"name": {
|
|
12
12
|
"title": "Name",
|
|
13
13
|
"type": "string",
|
|
14
|
-
"default": "
|
|
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
|
-
{
|
|
31
|
-
{
|
|
32
|
-
{
|
|
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
|
-
|
|
764
|
-
this.
|
|
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)
|
|
948
|
+
let currentSmokeStatus = HKSmokeSensorService.getCharacteristic(Characteristic.SmokeDetected)
|
|
949
|
+
.value;
|
|
925
950
|
if (currentSmokeStatus != newSmokeStatus)
|
|
926
951
|
HKSmokeSensorService.getCharacteristic(Characteristic.SmokeDetected).updateValue(
|
|
927
952
|
newSmokeStatus
|
package/lib/fakegato.js
ADDED
|
@@ -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
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
|
|
55
|
-
|
|
56
|
-
| `platform`
|
|
57
|
-
| `login`
|
|
58
|
-
| `password`
|
|
59
|
-
| `originSession`
|
|
60
|
-
| `allowActivation`
|
|
61
|
-
| `refreshTimer`
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
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
|
|