homebridge-carrier-infinity 1.7.0-beta.1 → 1.7.1
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/dist/accessory_base.js.map +1 -1
- package/dist/accessory_comfort_activity.d.ts.map +1 -1
- package/dist/accessory_comfort_activity.js +12 -8
- package/dist/accessory_comfort_activity.js.map +1 -1
- package/dist/accessory_oat.d.ts +1 -1
- package/dist/accessory_oat.d.ts.map +1 -1
- package/dist/accessory_oat.js +3 -4
- package/dist/accessory_oat.js.map +1 -1
- package/dist/accessory_thermostat.d.ts.map +1 -1
- package/dist/accessory_thermostat.js +27 -6
- package/dist/accessory_thermostat.js.map +1 -1
- package/dist/api/constants.d.ts +6 -0
- package/dist/api/constants.d.ts.map +1 -1
- package/dist/api/constants.js +7 -1
- package/dist/api/constants.js.map +1 -1
- package/dist/api/models.d.ts +71 -79
- package/dist/api/models.d.ts.map +1 -1
- package/dist/api/models.js +440 -363
- package/dist/api/models.js.map +1 -1
- package/dist/api/oauth.d.ts +2 -2
- package/dist/api/oauth.d.ts.map +1 -1
- package/dist/api/oauth.js +0 -3
- package/dist/api/oauth.js.map +1 -1
- package/dist/api/rest_client.js +1 -1
- package/dist/api/rest_client.js.map +1 -1
- package/dist/characteristics_ac.d.ts +1 -1
- package/dist/characteristics_ac.d.ts.map +1 -1
- package/dist/characteristics_ac.js +42 -70
- package/dist/characteristics_ac.js.map +1 -1
- package/dist/characteristics_base.d.ts +6 -9
- package/dist/characteristics_base.d.ts.map +1 -1
- package/dist/characteristics_base.js +48 -57
- package/dist/characteristics_base.js.map +1 -1
- package/dist/characteristics_fan.d.ts +6 -6
- package/dist/characteristics_fan.d.ts.map +1 -1
- package/dist/characteristics_fan.js +63 -75
- package/dist/characteristics_fan.js.map +1 -1
- package/dist/characteristics_filter.d.ts +1 -1
- package/dist/characteristics_filter.d.ts.map +1 -1
- package/dist/characteristics_filter.js +8 -5
- package/dist/characteristics_filter.js.map +1 -1
- package/dist/characteristics_humidity.d.ts +5 -2
- package/dist/characteristics_humidity.d.ts.map +1 -1
- package/dist/characteristics_humidity.js +17 -9
- package/dist/characteristics_humidity.js.map +1 -1
- package/dist/helper_logging.d.ts +1 -0
- package/dist/helper_logging.d.ts.map +1 -1
- package/dist/helper_logging.js +3 -0
- package/dist/helper_logging.js.map +1 -1
- package/dist/helpers.js +7 -8
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +6 -12
- package/dist/platform.js.map +1 -1
- package/package.json +28 -28
- package/dist/api/helpers.d.ts +0 -5
- package/dist/api/helpers.d.ts.map +0 -1
- package/dist/api/helpers.js +0 -31
- package/dist/api/helpers.js.map +0 -1
- package/dist/api/helpers_rxjs.d.ts +0 -3
- package/dist/api/helpers_rxjs.d.ts.map +0 -1
- package/dist/api/helpers_rxjs.js +0 -16
- package/dist/api/helpers_rxjs.js.map +0 -1
package/dist/api/models.js
CHANGED
|
@@ -15,80 +15,87 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
|
|
15
15
|
}) : function(o, v) {
|
|
16
16
|
o["default"] = v;
|
|
17
17
|
});
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
return result;
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
24
23
|
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
25
41
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
42
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
43
|
};
|
|
28
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.SystemModel = exports.
|
|
45
|
+
exports.SystemModel = exports.SystemConfigModel = exports.SystemConfigModelReadOnly = exports.SystemStatusModel = exports.SystemProfileModel = exports.LocationsModel = void 0;
|
|
30
46
|
const helpers_1 = require("../helpers");
|
|
47
|
+
const typescript_memoize_1 = require("typescript-memoize");
|
|
31
48
|
const async_mutex_1 = require("async-mutex");
|
|
32
49
|
const xml2js = __importStar(require("xml2js"));
|
|
33
50
|
const object_hash_1 = __importDefault(require("object-hash"));
|
|
34
51
|
const helper_logging_1 = require("../helper_logging");
|
|
35
52
|
const axios_1 = __importDefault(require("axios"));
|
|
36
|
-
const constants_1 = require("./constants");
|
|
37
|
-
const rxjs_1 = require("rxjs");
|
|
38
53
|
const events_1 = __importDefault(require("events"));
|
|
39
|
-
const
|
|
40
|
-
const helpers_rxjs_1 = require("./helpers_rxjs");
|
|
54
|
+
const constants_1 = require("./constants");
|
|
41
55
|
class BaseModel {
|
|
42
56
|
constructor(infinity_client) {
|
|
43
57
|
this.infinity_client = infinity_client;
|
|
44
|
-
// Raw clean api data for use inside class and in children.
|
|
45
|
-
this.clean_data$ = new rxjs_1.ReplaySubject(1);
|
|
46
|
-
// Protected form for use outside the class
|
|
47
|
-
this.data$ = this.clean_data$.asObservable();
|
|
48
58
|
this.HASH_IGNORE_KEYS = new Set();
|
|
49
59
|
this.log = new helper_logging_1.PrefixLogger(this.infinity_client.log, 'API');
|
|
50
|
-
this.events = new events_1.default();
|
|
51
60
|
this.write_lock = new async_mutex_1.Mutex();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
(0, rxjs_1.of)(1),
|
|
56
|
-
// Periodic Fetch
|
|
57
|
-
(0, rxjs_1.interval)(5 * 60 * 1000),
|
|
58
|
-
// On Demand Fetch
|
|
59
|
-
(0, rxjs_1.fromEvent)(this.events, 'onGet'), (0, rxjs_1.fromEvent)(this.events, 'post_push_refresh')).pipe((0, rxjs_1.throttleTime)(10000));
|
|
60
|
-
// Use these 'ticks' triggers to update the data.
|
|
61
|
-
ticks.pipe((0, rxjs_1.switchMap)(() => this.fetchObservable()), (0, rxjs_1.distinctUntilChanged)((prev, cur) => this.isUnchanged(prev, cur))).subscribe(this.clean_data$);
|
|
62
|
-
}
|
|
63
|
-
isUnchanged(x, y) {
|
|
64
|
-
return this.hash(x) === this.hash(y);
|
|
65
|
-
}
|
|
66
|
-
hash(data) {
|
|
67
|
-
return (0, object_hash_1.default)(data, { excludeKeys: (key) => {
|
|
61
|
+
}
|
|
62
|
+
hashDataObject(data) {
|
|
63
|
+
return (0, object_hash_1.default)(data || this.data_object, { excludeKeys: (key) => {
|
|
68
64
|
return this.HASH_IGNORE_KEYS.has(key);
|
|
69
65
|
} });
|
|
70
66
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
observer.complete();
|
|
77
|
-
})
|
|
78
|
-
// An observable can never return an error, or it completes.
|
|
79
|
-
// Log errors, swallow them, and send no new value.
|
|
80
|
-
.catch((error) => {
|
|
81
|
-
this.log.error('Failed to fetch updates: ', axios_1.default.isAxiosError(error) ? error.message : error);
|
|
82
|
-
observer.complete();
|
|
67
|
+
async fetch() {
|
|
68
|
+
// If push is ongoing, skip this update fetch. The push will do a fetch.
|
|
69
|
+
try {
|
|
70
|
+
await (0, async_mutex_1.tryAcquire)(this.write_lock).runExclusive(async () => {
|
|
71
|
+
await this.forceFetch();
|
|
83
72
|
});
|
|
84
|
-
}
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
if (e === async_mutex_1.E_ALREADY_LOCKED) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
else if (e === async_mutex_1.E_TIMEOUT || e === async_mutex_1.E_CANCELED) {
|
|
79
|
+
this.log.error(`Deadlock on fetch ${e}. Report bug: https://bit.ly/3igbU7D`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.log.error('Failed to fetch updates: ', axios_1.default.isAxiosError(e) ? e.message : e);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
85
|
}
|
|
86
86
|
async forceFetch() {
|
|
87
|
+
const [data_object_hash, data_object] = await this.forceFetchInternal();
|
|
88
|
+
this.data_object = data_object;
|
|
89
|
+
this.data_object_hash = data_object_hash;
|
|
90
|
+
}
|
|
91
|
+
async forceFetchInternal() {
|
|
87
92
|
await this.infinity_client.refreshToken();
|
|
88
93
|
await this.infinity_client.activate();
|
|
89
94
|
const response = await this.infinity_client.axios.get(this.getPath());
|
|
90
95
|
if (response.data) {
|
|
91
|
-
|
|
96
|
+
const data_object = await xml2js.parseStringPromise(response.data);
|
|
97
|
+
const data_object_hash = this.hashDataObject(data_object);
|
|
98
|
+
return [data_object_hash, data_object];
|
|
92
99
|
}
|
|
93
100
|
else {
|
|
94
101
|
this.log.debug(response.data);
|
|
@@ -96,393 +103,463 @@ class BaseModel {
|
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
105
|
}
|
|
106
|
+
__decorate([
|
|
107
|
+
(0, typescript_memoize_1.MemoizeExpiring)(10 * 1000)
|
|
108
|
+
], BaseModel.prototype, "fetch", null);
|
|
99
109
|
class LocationsModel extends BaseModel {
|
|
100
|
-
constructor() {
|
|
101
|
-
super(...arguments);
|
|
102
|
-
this.system_serials = this.data$.pipe((0, rxjs_1.map)(data => {
|
|
103
|
-
const systems = [];
|
|
104
|
-
for (const location of data.locations.location) {
|
|
105
|
-
for (const system of location.systems[0].system || []) {
|
|
106
|
-
const link_parts = system['atom:link'][0]['$']['href'].split('/');
|
|
107
|
-
systems.push(link_parts[link_parts.length - 1]);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return systems;
|
|
111
|
-
}), (0, rxjs_1.distinctUntilChanged)());
|
|
112
|
-
}
|
|
113
110
|
getPath() {
|
|
114
111
|
return `/users/${this.infinity_client.username}/locations`;
|
|
115
112
|
}
|
|
113
|
+
async getSystems() {
|
|
114
|
+
await this.fetch();
|
|
115
|
+
const systems = [];
|
|
116
|
+
for (const location of this.data_object.locations.location) {
|
|
117
|
+
for (const system of location.systems[0].system || []) {
|
|
118
|
+
const link_parts = system['atom:link'][0]['$']['href'].split('/');
|
|
119
|
+
systems.push(link_parts[link_parts.length - 1]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return systems;
|
|
123
|
+
}
|
|
116
124
|
}
|
|
117
125
|
exports.LocationsModel = LocationsModel;
|
|
118
126
|
class BaseSystemModel extends BaseModel {
|
|
119
|
-
constructor(infinity_client, serialNumber, log) {
|
|
127
|
+
constructor(infinity_client, serialNumber, log, events) {
|
|
120
128
|
super(infinity_client);
|
|
121
129
|
this.infinity_client = infinity_client;
|
|
122
130
|
this.serialNumber = serialNumber;
|
|
123
131
|
this.log = log;
|
|
124
|
-
|
|
125
|
-
this.
|
|
126
|
-
this.
|
|
127
|
-
this.HASH_IGNORE_KEYS = new Set(['timestamp', 'localTime', 'previousMode']);
|
|
132
|
+
this.events = events;
|
|
133
|
+
this.last_updated = 0; // TODO use this
|
|
134
|
+
this.HASH_IGNORE_KEYS = new Set(['timestamp', 'localTime']);
|
|
128
135
|
}
|
|
129
136
|
async forceFetch() {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
this.
|
|
136
|
-
this.log.debug(`
|
|
137
|
-
|
|
137
|
+
const old_hash = this.data_object_hash;
|
|
138
|
+
await super.forceFetch();
|
|
139
|
+
const new_hash = this.data_object_hash;
|
|
140
|
+
const top_level_key = Object.keys(this.data_object)[0];
|
|
141
|
+
const ts = this.data_object[top_level_key].timestamp[0];
|
|
142
|
+
this.last_updated = Date.parse(ts);
|
|
143
|
+
this.log.debug(`TIMESTAMP ${this.getPath()} reports ${ts} (${this.last_updated})`);
|
|
144
|
+
if (old_hash !== new_hash) {
|
|
145
|
+
this.events.emit(`updated_${top_level_key}`);
|
|
146
|
+
}
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
149
|
class SystemProfileModel extends BaseSystemModel {
|
|
141
|
-
constructor() {
|
|
142
|
-
super(...arguments);
|
|
143
|
-
this.name = this.data$.pipe((0, rxjs_1.map)(data => data.system_profile.name[0]), (0, rxjs_1.distinctUntilChanged)());
|
|
144
|
-
this.brand = this.data$.pipe((0, rxjs_1.map)(data => data.system_profile.brand[0]), (0, rxjs_1.distinctUntilChanged)());
|
|
145
|
-
this.model = this.data$.pipe((0, rxjs_1.map)(data => data.system_profile.model[0]), (0, rxjs_1.distinctUntilChanged)());
|
|
146
|
-
this.firmware = this.data$.pipe((0, rxjs_1.map)(data => data.system_profile.firmware[0]), (0, rxjs_1.distinctUntilChanged)());
|
|
147
|
-
this.zone_ids = this.data$.pipe((0, rxjs_1.map)(data => {
|
|
148
|
-
return data.system_profile.zones[0].zone.filter((zone) => zone['present'][0] === constants_1.STATUS.ON).map((zone) => zone['$'].id);
|
|
149
|
-
}), (0, rxjs_1.distinctUntilChanged)());
|
|
150
|
-
}
|
|
151
150
|
getPath() {
|
|
152
151
|
return `/systems/${this.serialNumber}/profile`;
|
|
153
152
|
}
|
|
153
|
+
async getName() {
|
|
154
|
+
await this.fetch();
|
|
155
|
+
return this.data_object.system_profile.name[0];
|
|
156
|
+
}
|
|
157
|
+
async getBrand() {
|
|
158
|
+
await this.fetch();
|
|
159
|
+
return this.data_object.system_profile.brand[0];
|
|
160
|
+
}
|
|
161
|
+
async getModel() {
|
|
162
|
+
await this.fetch();
|
|
163
|
+
return this.data_object.system_profile.model[0];
|
|
164
|
+
}
|
|
165
|
+
async getFirmware() {
|
|
166
|
+
await this.fetch();
|
|
167
|
+
return this.data_object.system_profile.firmware[0];
|
|
168
|
+
}
|
|
169
|
+
async getZones() {
|
|
170
|
+
await this.fetch();
|
|
171
|
+
return this.data_object.system_profile.zones[0].zone.filter((zone) => zone['present'][0] === constants_1.STATUS.ON).map((zone) => zone['$'].id);
|
|
172
|
+
}
|
|
154
173
|
}
|
|
155
174
|
exports.SystemProfileModel = SystemProfileModel;
|
|
156
175
|
class SystemStatusModel extends BaseSystemModel {
|
|
157
|
-
constructor() {
|
|
158
|
-
super(...arguments);
|
|
159
|
-
this.outdoor_temp = this.data$.pipe((0, rxjs_1.map)(data => Number(data.status.oat[0])), (0, rxjs_1.distinctUntilChanged)());
|
|
160
|
-
this.filter_used = this.data$.pipe((0, rxjs_1.map)(data => Number(data.status.filtrlvl[0])), (0, rxjs_1.distinctUntilChanged)());
|
|
161
|
-
this.temp_units = this.data$.pipe((0, rxjs_1.map)(data => data.status.cfgem[0]), (0, rxjs_1.distinctUntilChanged)());
|
|
162
|
-
this.mode = this.data$.pipe((0, rxjs_1.map)(data => {
|
|
163
|
-
const raw_mode = data.status.mode[0];
|
|
164
|
-
switch (raw_mode) {
|
|
165
|
-
case 'gasheat':
|
|
166
|
-
case 'electric':
|
|
167
|
-
case 'hpheat':
|
|
168
|
-
return constants_1.SYSTEM_MODE.HEAT;
|
|
169
|
-
case 'dehumidify':
|
|
170
|
-
return constants_1.SYSTEM_MODE.COOL;
|
|
171
|
-
default:
|
|
172
|
-
return raw_mode;
|
|
173
|
-
}
|
|
174
|
-
}), (0, rxjs_1.distinctUntilChanged)());
|
|
175
|
-
this.raw_zone_data$ = this.data$.pipe((0, rxjs_1.map)(data => data.status.zones[0].zone), (0, rxjs_1.distinctUntilChanged)());
|
|
176
|
-
}
|
|
177
176
|
getPath() {
|
|
178
177
|
return `/systems/${this.serialNumber}/status`;
|
|
179
178
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return
|
|
179
|
+
async getUnits() {
|
|
180
|
+
await this.fetch();
|
|
181
|
+
return this.data_object.status.cfgem[0];
|
|
182
|
+
}
|
|
183
|
+
async getOutdoorTemp() {
|
|
184
|
+
await this.fetch();
|
|
185
|
+
return Number(this.data_object.status.oat[0]);
|
|
186
|
+
}
|
|
187
|
+
async getFilterUsed() {
|
|
188
|
+
await this.fetch();
|
|
189
|
+
return Number(this.data_object.status.filtrlvl[0]);
|
|
190
|
+
}
|
|
191
|
+
async getMode() {
|
|
192
|
+
await this.fetch();
|
|
193
|
+
const raw_mode = this.data_object.status.mode[0];
|
|
194
|
+
switch (raw_mode) {
|
|
195
|
+
case 'gasheat':
|
|
196
|
+
case 'electric':
|
|
197
|
+
case 'hpheat':
|
|
198
|
+
return constants_1.SYSTEM_MODE.HEAT;
|
|
199
|
+
case 'dehumidify':
|
|
200
|
+
return constants_1.SYSTEM_MODE.COOL;
|
|
201
|
+
default:
|
|
202
|
+
return raw_mode;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async getZone(zone) {
|
|
206
|
+
await this.fetch();
|
|
207
|
+
return this.data_object.status.zones[0].zone.find((z) => z['$'].id === zone.toString());
|
|
208
|
+
}
|
|
209
|
+
async getZoneConditioning(zone) {
|
|
210
|
+
const raw_mode = (await this.getZone(zone)).zoneconditioning[0];
|
|
211
|
+
switch (raw_mode) {
|
|
212
|
+
case 'active_heat':
|
|
213
|
+
case 'prep_heat':
|
|
214
|
+
case 'pending_heat':
|
|
215
|
+
return constants_1.SYSTEM_MODE.HEAT;
|
|
216
|
+
case 'active_cool':
|
|
217
|
+
case 'prep_cool':
|
|
218
|
+
case 'pending_cool':
|
|
219
|
+
return constants_1.SYSTEM_MODE.COOL;
|
|
220
|
+
case 'idle':
|
|
221
|
+
return constants_1.SYSTEM_MODE.OFF;
|
|
222
|
+
default:
|
|
223
|
+
return raw_mode;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async getZoneFan(zone) {
|
|
227
|
+
const zone_obj = await this.getZone(zone);
|
|
228
|
+
if (zone_obj.damperposition[0] === '0') {
|
|
229
|
+
return constants_1.FAN_MODE.OFF;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
return zone_obj.fan[0];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async getZoneOpen(zone) {
|
|
236
|
+
return (await this.getZone(zone)).damperposition[0] !== '0';
|
|
237
|
+
}
|
|
238
|
+
async getZoneTemp(zone) {
|
|
239
|
+
return Number((await this.getZone(zone)).rt[0]);
|
|
240
|
+
}
|
|
241
|
+
async getZoneHumidity(zone) {
|
|
242
|
+
return Number((await this.getZone(zone)).rh[0]);
|
|
243
|
+
}
|
|
244
|
+
async getZoneActivity(zone) {
|
|
245
|
+
return (await this.getZone(zone)).currentActivity[0];
|
|
246
|
+
}
|
|
247
|
+
async getZoneCoolSetpoint(zone) {
|
|
248
|
+
return Number((await this.getZone(zone)).clsp[0]);
|
|
249
|
+
}
|
|
250
|
+
async getZoneHeatSetpoint(zone) {
|
|
251
|
+
return Number((await this.getZone(zone)).htsp[0]);
|
|
183
252
|
}
|
|
184
253
|
}
|
|
185
254
|
exports.SystemStatusModel = SystemStatusModel;
|
|
186
|
-
class
|
|
187
|
-
|
|
188
|
-
this.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
255
|
+
class SystemConfigModelReadOnly extends BaseSystemModel {
|
|
256
|
+
getPath() {
|
|
257
|
+
return `/systems/${this.serialNumber}/config`;
|
|
258
|
+
}
|
|
259
|
+
static getUnits(data_object) {
|
|
260
|
+
return data_object.config.cfgem[0];
|
|
261
|
+
}
|
|
262
|
+
async getUnits() {
|
|
263
|
+
await this.fetch();
|
|
264
|
+
return SystemConfigModelReadOnly.getUnits(this.data_object);
|
|
265
|
+
}
|
|
266
|
+
async getTempBounds() {
|
|
267
|
+
var _a;
|
|
268
|
+
// TODO: Utility event isn't always set. Find somewhere else to get this #543
|
|
269
|
+
await this.fetch();
|
|
270
|
+
const utility_events = ((_a = this.data_object.config.utilityEvent) === null || _a === void 0 ? void 0 : _a[0]) || { 'minLimit': ['50'], 'maxLimit': ['90'] };
|
|
271
|
+
return [Number(utility_events.minLimit[0]), Number(utility_events.maxLimit[0])];
|
|
272
|
+
}
|
|
273
|
+
async getMode() {
|
|
274
|
+
await this.fetch();
|
|
275
|
+
return this.data_object.config.mode[0];
|
|
276
|
+
}
|
|
277
|
+
static getZone(data_object, zone) {
|
|
278
|
+
return data_object.config.zones[0].zone.find((z) => z['$'].id === zone.toString());
|
|
279
|
+
}
|
|
280
|
+
async getZone(zone) {
|
|
281
|
+
await this.fetch();
|
|
282
|
+
return SystemConfigModelReadOnly.getZone(this.data_object, zone);
|
|
283
|
+
}
|
|
284
|
+
async getZoneName(zone) {
|
|
285
|
+
const zone_obj = await this.getZone(zone);
|
|
286
|
+
return zone_obj['name'][0];
|
|
287
|
+
}
|
|
288
|
+
async getZoneHoldStatus(zone) {
|
|
289
|
+
const zone_obj = await this.getZone(zone);
|
|
290
|
+
return [zone_obj['hold'][0], zone_obj['otmr'][0]];
|
|
291
|
+
}
|
|
292
|
+
static getZoneActivity(data_object, zone) {
|
|
293
|
+
const zone_obj = SystemConfigModelReadOnly.getZone(data_object, zone);
|
|
294
|
+
if (zone_obj.hold[0] === constants_1.STATUS.ON) {
|
|
295
|
+
return zone_obj.holdActivity[0];
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const now = new Date();
|
|
299
|
+
const program_obj = SystemConfigModelReadOnly.getZone(data_object, zone).program[0];
|
|
300
|
+
const today_schedule = program_obj.day[now.getDay()].period.filter(period => period.enabled[0] === constants_1.STATUS.ON).reverse();
|
|
301
|
+
for (const i in today_schedule) {
|
|
302
|
+
const time = today_schedule[i].time[0];
|
|
303
|
+
const split = time.split(':');
|
|
304
|
+
if (
|
|
305
|
+
// The hour is past
|
|
306
|
+
Number(split[0]) < now.getHours() ||
|
|
307
|
+
// The hour is now, the minute is past
|
|
308
|
+
(Number(split[0]) === now.getHours() && Number(split[1]) < now.getMinutes())) {
|
|
309
|
+
return today_schedule[i].activity[0];
|
|
310
|
+
}
|
|
213
311
|
}
|
|
214
|
-
|
|
215
|
-
|
|
312
|
+
// If we got to the end without finding the next activity, it means the activity is the last from yesterday
|
|
313
|
+
const yesterday_schedule = program_obj['day'][(now.getDay() + 8) % 7].period.filter(period => period.enabled[0] === constants_1.STATUS.ON).reverse();
|
|
314
|
+
return yesterday_schedule[0].activity[0];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async getZoneActivity(zone) {
|
|
318
|
+
await this.fetch();
|
|
319
|
+
return SystemConfigModelReadOnly.getZoneActivity(this.data_object, zone);
|
|
320
|
+
}
|
|
321
|
+
static getZoneActivityConfig(data_object, zone, activity_name) {
|
|
322
|
+
// Vacation is stored somewhere else...
|
|
323
|
+
if (activity_name === constants_1.ACTIVITY.VACATION) {
|
|
324
|
+
return {
|
|
325
|
+
'$': { id: constants_1.ACTIVITY.VACATION },
|
|
326
|
+
clsp: data_object.config.vacmaxt,
|
|
327
|
+
htsp: data_object.config.vacmint,
|
|
328
|
+
fan: data_object.config.vacfan,
|
|
329
|
+
previousFan: [],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const activities_obj = SystemConfigModelReadOnly.getZone(data_object, zone).activities[0];
|
|
333
|
+
return activities_obj['activity'].find((activity) => activity['$'].id === activity_name);
|
|
334
|
+
}
|
|
335
|
+
async getZoneActivityConfig(zone, activity_name) {
|
|
336
|
+
await this.fetch();
|
|
337
|
+
return SystemConfigModelReadOnly.getZoneActivityConfig(this.data_object, zone, activity_name);
|
|
338
|
+
}
|
|
339
|
+
async getZoneActivityFan(zone, activity) {
|
|
340
|
+
const activity_obj = await this.getZoneActivityConfig(zone, activity);
|
|
341
|
+
return activity_obj.fan[0];
|
|
342
|
+
}
|
|
343
|
+
async getZoneActivityCoolSetpoint(zone, activity) {
|
|
344
|
+
const activity_obj = await this.getZoneActivityConfig(zone, activity);
|
|
345
|
+
return Number(activity_obj.clsp[0]);
|
|
346
|
+
}
|
|
347
|
+
async getZoneActivityHeatSetpoint(zone, activity) {
|
|
348
|
+
const activity_obj = await this.getZoneActivityConfig(zone, activity);
|
|
349
|
+
return Number(activity_obj.htsp[0]);
|
|
350
|
+
}
|
|
351
|
+
async getZoneNextActivityTime(zone) {
|
|
352
|
+
const now = new Date();
|
|
353
|
+
const program_obj = (await this.getZone(zone)).program[0];
|
|
354
|
+
const day_obj = program_obj['day'][now.getDay()];
|
|
355
|
+
for (const i in day_obj['period']) {
|
|
356
|
+
const time = day_obj['period'][i].time[0];
|
|
357
|
+
const split = time.split(':');
|
|
358
|
+
if (
|
|
359
|
+
// The hour is nigh
|
|
360
|
+
Number(split[0]) > now.getHours() ||
|
|
361
|
+
// The hour is now, the minute is nigh
|
|
362
|
+
(Number(split[0]) === now.getHours() && Number(split[1]) > now.getMinutes())) {
|
|
363
|
+
return time;
|
|
216
364
|
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// This helps with some edge cases around zoned systems
|
|
222
|
-
this.closed = this.zone.pipe((0, rxjs_1.map)(zone => zone.damperposition[0] === '0'), (0, rxjs_1.distinctUntilChanged)());
|
|
223
|
-
this.temp = (0, rxjs_1.combineLatest)([this.zone, this.temp_units$]).pipe((0, rxjs_1.debounceTime)(50), (0, rxjs_1.map)(([zone, temp_units]) => [Number(zone.rt[0]), temp_units]), (0, helpers_rxjs_1.distinctUntilChangedWithEpsilon)());
|
|
224
|
-
this.cool_setpoint = (0, rxjs_1.combineLatest)([this.zone, this.temp_units$]).pipe((0, rxjs_1.debounceTime)(50), (0, rxjs_1.map)(([zone, temp_units]) => [Number(zone.clsp[0]), temp_units]), (0, helpers_rxjs_1.distinctUntilChangedWithEpsilon)());
|
|
225
|
-
this.heat_setpoint = (0, rxjs_1.combineLatest)([this.zone, this.temp_units$]).pipe((0, rxjs_1.debounceTime)(50), (0, rxjs_1.map)(([zone, temp_units]) => [Number(zone.htsp[0]), temp_units]), (0, helpers_rxjs_1.distinctUntilChangedWithEpsilon)());
|
|
226
|
-
this.humidity = this.zone.pipe((0, rxjs_1.map)(zone => Number(zone.rh[0])), (0, rxjs_1.distinctUntilChanged)());
|
|
365
|
+
}
|
|
366
|
+
// If we got to the end without finding the next activity, it means the next activity is the first from tomorrow
|
|
367
|
+
const tomorrow_obj = program_obj['day'][(now.getDay() + 1) % 7];
|
|
368
|
+
return tomorrow_obj['period'][0].time[0];
|
|
227
369
|
}
|
|
228
370
|
}
|
|
229
|
-
exports.
|
|
230
|
-
class SystemConfigModel extends
|
|
231
|
-
constructor(
|
|
232
|
-
super(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
this.data$ = (0, rxjs_1.merge)(this.clean_data$.pipe(
|
|
241
|
-
// When a push is active, do not pass clean data unless it is from after
|
|
242
|
-
// the push.
|
|
243
|
-
// TODO add an OR to allow if the clean data matches the last dirty
|
|
244
|
-
(0, rxjs_1.filter)((data) => Date.parse(data.config.timestamp[0]) >= this.last_pushed_ts)), this.dirty_data$);
|
|
245
|
-
// Indicates the local data$ has been modified, and clean_data$ should only be
|
|
246
|
-
// used after if it is from after this time.
|
|
247
|
-
this.last_pushed_ts = 0;
|
|
248
|
-
this.mode = this.data$.pipe((0, rxjs_1.map)(data => data.config.mode[0]), (0, rxjs_1.distinctUntilChanged)());
|
|
249
|
-
this.temp_units = this.data$.pipe((0, rxjs_1.map)(data => data.config.cfgem[0]), (0, rxjs_1.distinctUntilChanged)());
|
|
250
|
-
this.temp_bounds = this.data$.pipe((0, rxjs_1.map)(data => [
|
|
251
|
-
[Number(data.config.utilityEvent[0].minLimit[0]), data.config.cfgem[0]],
|
|
252
|
-
[Number(data.config.utilityEvent[0].maxLimit[0]), data.config.cfgem[0]],
|
|
253
|
-
]), (0, rxjs_1.distinctUntilChanged)());
|
|
254
|
-
this.raw_zone_data$ = this.data$.pipe((0, rxjs_1.map)(data => data.config.zones[0].zone), (0, rxjs_1.distinctUntilChanged)());
|
|
255
|
-
// Send changes from the dirty Subject back to the carrier api.
|
|
256
|
-
this.dirty_data$.pipe(
|
|
257
|
-
// Wait x seconds after last change before sending.
|
|
258
|
-
(0, rxjs_1.debounceTime)(3 * 1000)).subscribe(async (data) => this.push(data));
|
|
259
|
-
this.clean_data$.subscribe(() => this.log.debug('New config data observed from api'));
|
|
260
|
-
this.dirty_data$.subscribe(() => this.log.debug('New config data observed from local'));
|
|
261
|
-
this.data$.subscribe(() => this.log.debug('Propagating new config data to HK...'));
|
|
371
|
+
exports.SystemConfigModelReadOnly = SystemConfigModelReadOnly;
|
|
372
|
+
class SystemConfigModel extends SystemConfigModelReadOnly {
|
|
373
|
+
constructor() {
|
|
374
|
+
super(...arguments);
|
|
375
|
+
/*
|
|
376
|
+
* A writable version of the system config model.
|
|
377
|
+
*
|
|
378
|
+
* Provides a set data api, which is cached locally, and periodically pushed
|
|
379
|
+
* out to the carrier api.
|
|
380
|
+
*/
|
|
381
|
+
this.mutations = [];
|
|
262
382
|
}
|
|
263
|
-
|
|
264
|
-
|
|
383
|
+
// Skip fetching new data when we have a dirty local state.
|
|
384
|
+
async fetch() {
|
|
385
|
+
if (this.mutations.length > 0) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
await super.fetch();
|
|
265
389
|
}
|
|
266
|
-
|
|
267
|
-
//
|
|
268
|
-
|
|
390
|
+
async push() {
|
|
391
|
+
// While waiting to push to api, push locally to HK.
|
|
392
|
+
this.events.emit(constants_1.SUBSCRIPTION.CONFIG_MUTATE);
|
|
393
|
+
// Wait a bit so we can catch other mutations that came in around the
|
|
394
|
+
// same time.
|
|
395
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
396
|
+
// We only ever need 2 pushes ongoing at a time. One active, and one pending.
|
|
397
|
+
// The first one will handle mutations available at its start, and the next
|
|
398
|
+
// one will cover mutations that arrived during the previous's run.
|
|
399
|
+
// First, to make sure we only ever have one 'pending' push, cancel any other
|
|
400
|
+
// possible 'pending' pushes, and make this one become the 'pending' push.
|
|
401
|
+
this.write_lock.cancel();
|
|
402
|
+
// Then, grab the lock. so this push can move from 'pending' to 'active'.
|
|
403
|
+
try {
|
|
404
|
+
await this.write_lock.runExclusive(async () => {
|
|
405
|
+
// 1. Do mutations
|
|
406
|
+
const mutated = await this.mutate();
|
|
407
|
+
if (mutated === null) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const [mutated_hash, mutated_data_object] = mutated;
|
|
411
|
+
// 2. Push
|
|
412
|
+
await this.forcePush(mutated_data_object);
|
|
413
|
+
this.log.info('... pushing changes complete.');
|
|
414
|
+
// 3. Confirm
|
|
415
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
416
|
+
if (this.mutations.length > 0) {
|
|
417
|
+
// If local state is dirty (from new mutations queued during push)
|
|
418
|
+
// don't do a forceFetch or it will cause apparent bouncing, let the
|
|
419
|
+
// next push refresh from remote api state.
|
|
420
|
+
// This is safe because mutations are always done on a fresh fetched
|
|
421
|
+
// config, even if local config is dirty or stale.
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
await this.forceFetch();
|
|
425
|
+
if (mutated_hash === this.data_object_hash) {
|
|
426
|
+
this.log.debug('Successful propagation to carrier api is confirmed.');
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
this.log.warn('Changes do not (yet?) appear to have propagated to the carrier api.');
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
if (e === async_mutex_1.E_CANCELED) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
else if (e === async_mutex_1.E_TIMEOUT || e === async_mutex_1.E_ALREADY_LOCKED) {
|
|
438
|
+
this.log.error(`Deadlock on push ${e}. Report bug: https://bit.ly/3igbU7D`);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
this.log.error('Failed to push updates: ', axios_1.default.isAxiosError(e) ? e.message : e);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
269
444
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
445
|
+
async mutate() {
|
|
446
|
+
// short circuit if no mutations in queue
|
|
447
|
+
if (this.mutations.length === 0) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
// Refresh config, to make sure we don't write back old data accidentally.
|
|
451
|
+
const stale_hash = this.data_object_hash;
|
|
452
|
+
const [fresh_hash, fresh_data_object] = await this.forceFetchInternal();
|
|
453
|
+
if (stale_hash !== fresh_hash) {
|
|
454
|
+
this.log.warn('Cached config was stale before mutation and push.');
|
|
455
|
+
}
|
|
456
|
+
// Take config mutations of the queue and run them against the fresh object.
|
|
457
|
+
// This ensures we don't overwrite other data if the api config has changed,
|
|
458
|
+
// and avoids a race condition where new mutations come in during the push.
|
|
459
|
+
const mutated_data_object = fresh_data_object;
|
|
460
|
+
while (this.mutations.length > 0) {
|
|
461
|
+
const m = this.mutations.shift();
|
|
462
|
+
if (m) {
|
|
463
|
+
m(mutated_data_object);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const mutated_hash = this.hashDataObject(mutated_data_object);
|
|
276
467
|
// If nothing actually changed, no need to push.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// this.log.warn(`Config (hash=${dirty_hash}) doesn't appear to have changed. No changes sent.`);
|
|
281
|
-
// this.last_pushed_ts = 0; // revert to clean config
|
|
282
|
-
// return;
|
|
283
|
-
// }
|
|
284
|
-
// Make sure the config base revision is not outdated
|
|
285
|
-
// TODO explicitly track and check base rev of dirty
|
|
286
|
-
// TODO use old config directly, instead of these vars?
|
|
287
|
-
// TODO make this just check that fields we dont play with haven't changed
|
|
288
|
-
// aka hash not changed minus things we modify
|
|
289
|
-
const prev_last_fetched_hash = this.last_fetched_hash;
|
|
290
|
-
const prev_last_fetched_ts = this.last_fetched_ts;
|
|
291
|
-
const new_clean_data = await this.forceFetch();
|
|
292
|
-
if (this.last_fetched_hash !== prev_last_fetched_hash ||
|
|
293
|
-
this.last_fetched_ts !== prev_last_fetched_ts) {
|
|
294
|
-
this.log.error('Aborting Push: API shows a newer, modified config.');
|
|
295
|
-
this.last_pushed_ts = 0; // revert to clean config
|
|
296
|
-
this.clean_data$.next(new_clean_data); // share new config
|
|
297
|
-
return;
|
|
468
|
+
if (fresh_hash === mutated_hash) {
|
|
469
|
+
this.log.warn('Config doesn\'t appear to have changed. No changes sent.');
|
|
470
|
+
return null;
|
|
298
471
|
}
|
|
299
|
-
|
|
300
|
-
await this.forcePush(data);
|
|
301
|
-
// Wait for a bit, and confirm if we see the api update on the server
|
|
302
|
-
this.clean_data$.pipe(
|
|
303
|
-
// Check for the next x seconds
|
|
304
|
-
(0, rxjs_1.timeout)(15 * 1000),
|
|
305
|
-
// Stop looking when we see the first successful update appear
|
|
306
|
-
(0, rxjs_1.filter)((new_clean_data) => dirty_hash === this.hash(new_clean_data)), (0, rxjs_1.take)(1)).subscribe({
|
|
307
|
-
next: (data) => {
|
|
308
|
-
this.log.info(`Successful propagation to carrier api is confirmed for ${this.hash(data)}`);
|
|
309
|
-
this.events.emit('post_push_refresh');
|
|
310
|
-
},
|
|
311
|
-
// As a fail-safe, revert to clean config if update failed
|
|
312
|
-
error: () => {
|
|
313
|
-
this.log.error('Changes do not (yet?) appear to have propagated to the carrier api.');
|
|
314
|
-
this.last_pushed_ts = 0; // revert to clean config
|
|
315
|
-
this.events.emit('post_push_refresh');
|
|
316
|
-
},
|
|
317
|
-
});
|
|
318
|
-
// Poll for updates for the verification above
|
|
319
|
-
this.events.emit('post_push_refresh');
|
|
472
|
+
return [mutated_hash, mutated_data_object];
|
|
320
473
|
}
|
|
321
|
-
async forcePush(
|
|
322
|
-
this.log.info('
|
|
474
|
+
async forcePush(data_object) {
|
|
475
|
+
this.log.info('Pushing changes to carrier api...');
|
|
323
476
|
const builder = new xml2js.Builder();
|
|
324
|
-
const new_xml = builder.buildObject(
|
|
325
|
-
const
|
|
326
|
-
await this.infinity_client.axios.post(this.getPath(),
|
|
477
|
+
const new_xml = builder.buildObject(data_object);
|
|
478
|
+
const data = `data=${encodeURIComponent(new_xml)}`;
|
|
479
|
+
await this.infinity_client.axios.post(this.getPath(), data, {
|
|
327
480
|
headers: {
|
|
328
481
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
329
482
|
},
|
|
330
483
|
});
|
|
331
|
-
this.log.debug(`TIMESTAMP UPDATED CONFIG reports ${data.config.timestamp[0]} (${this.last_pushed_ts})`);
|
|
332
|
-
this.log.debug(`HASH UPDATED CONFIG hashes to (${this.hash(data)})`);
|
|
333
|
-
this.log.info('... done sending changes to carrier api.');
|
|
334
484
|
}
|
|
335
485
|
async setMode(mode) {
|
|
336
486
|
this.log.debug('Setting mode to ' + mode);
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
487
|
+
const m = (data_object) => {
|
|
488
|
+
SystemConfigModel.mutateMode(data_object, mode);
|
|
489
|
+
};
|
|
490
|
+
// Mutate the local state to propagate changes throughout HK.
|
|
491
|
+
m(this.data_object);
|
|
492
|
+
// Schedule the push event, but don't wait for it to return.
|
|
493
|
+
this.mutations.push(m);
|
|
494
|
+
this.push();
|
|
495
|
+
}
|
|
496
|
+
static mutateMode(data_object, mode) {
|
|
497
|
+
data_object.config.mode[0] = mode;
|
|
340
498
|
}
|
|
341
499
|
async setZoneActivityHold(zone, activity, hold_until) {
|
|
342
500
|
this.log.debug(`Setting zone ${zone} activity to ${activity} until ${hold_until}`);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
501
|
+
const m = (data_object) => {
|
|
502
|
+
SystemConfigModel.mutateZoneActivityHold(data_object, zone, activity, hold_until);
|
|
503
|
+
};
|
|
504
|
+
// Mutate the local state to propagate changes throughout HK.
|
|
505
|
+
m(this.data_object);
|
|
506
|
+
// Schedule the push event, but don't wait for it to return.
|
|
507
|
+
this.mutations.push(m);
|
|
508
|
+
this.push();
|
|
509
|
+
}
|
|
510
|
+
static mutateZoneActivityHold(data_object, zone, activity, hold_until) {
|
|
511
|
+
const zone_obj = SystemConfigModel.getZone(data_object, zone);
|
|
346
512
|
zone_obj['holdActivity'][0] = activity;
|
|
347
513
|
zone_obj['hold'][0] = activity ? constants_1.STATUS.ON : constants_1.STATUS.OFF;
|
|
348
514
|
zone_obj['otmr'][0] = activity ? hold_until || '' : '';
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
515
|
+
}
|
|
516
|
+
async setZoneActivityManualHold(zone, clsp, htsp, hold_until, fan = null) {
|
|
517
|
+
this.log.debug(`Setting zone ${zone} to`, clsp ? `clsp=${clsp}` : '', htsp ? `htsp=${htsp}` : '', fan ? `fan=${fan}` : '', '.');
|
|
518
|
+
const m = (data_object) => {
|
|
519
|
+
// Modify MANUAL activity to the requested setpoints
|
|
520
|
+
SystemConfigModel.mutateZoneActivityManualHold(data_object, zone, clsp, htsp, fan);
|
|
521
|
+
// Set hold to MANUAL activity
|
|
522
|
+
SystemConfigModel.mutateZoneActivityHold(data_object, zone, constants_1.ACTIVITY.MANUAL, hold_until);
|
|
523
|
+
};
|
|
524
|
+
// Mutate the local state to propagate changes throughout HK.
|
|
525
|
+
m(this.data_object);
|
|
526
|
+
// Schedule the push event, but don't wait for it to return.
|
|
527
|
+
this.mutations.push(m);
|
|
528
|
+
this.push();
|
|
529
|
+
}
|
|
530
|
+
static mutateZoneActivityManualHold(data_object, zone, clsp, htsp, fan = null) {
|
|
531
|
+
const zone_obj = SystemConfigModel.getZone(data_object, zone);
|
|
532
|
+
// When moving to manual activity, default to prev activity settings.
|
|
533
|
+
const manual_activity_obj = SystemConfigModel.getZoneActivityConfig(data_object, zone, constants_1.ACTIVITY.MANUAL);
|
|
534
|
+
if (zone_obj['holdActivity'][0] !== constants_1.ACTIVITY.MANUAL) {
|
|
535
|
+
const prev_activity_obj = SystemConfigModel.getZoneActivityConfig(data_object, zone, SystemConfigModel.getZoneActivity(data_object, zone));
|
|
366
536
|
manual_activity_obj['clsp'][0] = prev_activity_obj['clsp'][0];
|
|
367
537
|
manual_activity_obj['htsp'][0] = prev_activity_obj['htsp'][0];
|
|
368
538
|
manual_activity_obj['fan'][0] = prev_activity_obj['fan'][0];
|
|
369
|
-
// Push changes
|
|
370
|
-
this.dirty_data$.next(data);
|
|
371
539
|
}
|
|
372
|
-
}
|
|
373
|
-
async setZoneActivityManualSetpoints(zone, clsp, htsp) {
|
|
374
|
-
this.log.debug(`Setting zone ${zone} to`, clsp ? `clsp=${clsp}` : '', htsp ? `htsp=${htsp}` : '', '.');
|
|
375
|
-
// Get data from zone / activity
|
|
376
|
-
const data = await (0, rxjs_1.firstValueFrom)(this.data$);
|
|
377
|
-
const manual_activity_obj = (0, helpers_2.getZoneActivityConfig)(data, zone, constants_1.ACTIVITY.MANUAL);
|
|
378
540
|
// Set setpoints on manual activity
|
|
379
|
-
[htsp, clsp] = (0, helpers_1.processSetpointDeadband)(htsp || parseFloat(manual_activity_obj['htsp'][0]), clsp || parseFloat(manual_activity_obj['clsp'][0]),
|
|
380
|
-
// TODO: rethink setpoint deadband
|
|
541
|
+
[htsp, clsp] = (0, helpers_1.processSetpointDeadband)(htsp || parseFloat(manual_activity_obj['htsp'][0]), clsp || parseFloat(manual_activity_obj['clsp'][0]), SystemConfigModel.getUnits(data_object),
|
|
381
542
|
// when setpoints are too close, make clsp sticky when no change made to htsp
|
|
382
543
|
htsp === null);
|
|
383
544
|
manual_activity_obj['htsp'][0] = htsp.toFixed(1);
|
|
384
545
|
manual_activity_obj['clsp'][0] = clsp.toFixed(1);
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
this.log.debug(`Setting zone ${zone} to fan=${fan}.`);
|
|
390
|
-
// Get data from zone / activity
|
|
391
|
-
const data = await (0, rxjs_1.firstValueFrom)(this.data$);
|
|
392
|
-
const manual_activity_obj = (0, helpers_2.getZoneActivityConfig)(data, zone, constants_1.ACTIVITY.MANUAL);
|
|
393
|
-
manual_activity_obj['fan'][0] = fan;
|
|
394
|
-
// Push changes
|
|
395
|
-
this.dirty_data$.next(data);
|
|
546
|
+
// Set fan on manual activity
|
|
547
|
+
if (fan) {
|
|
548
|
+
manual_activity_obj['fan'][0] = fan;
|
|
549
|
+
}
|
|
396
550
|
}
|
|
397
551
|
}
|
|
398
552
|
exports.SystemConfigModel = SystemConfigModel;
|
|
399
|
-
class SystemConfigZoneModel {
|
|
400
|
-
constructor(zone, temp_units$) {
|
|
401
|
-
this.zone = zone;
|
|
402
|
-
this.temp_units$ = temp_units$;
|
|
403
|
-
this.name = this.zone.pipe((0, rxjs_1.map)(zone => zone.name[0]), (0, rxjs_1.distinctUntilChanged)());
|
|
404
|
-
this.hold_status = this.zone.pipe((0, rxjs_1.map)(zone => [zone.hold[0], zone.otmr[0]]), (0, rxjs_1.distinctUntilChanged)());
|
|
405
|
-
// TODO Add a unit test to this
|
|
406
|
-
this.activity = this.zone.pipe((0, rxjs_1.map)(zone => {
|
|
407
|
-
if (zone.hold[0] === constants_1.STATUS.ON) {
|
|
408
|
-
return zone.holdActivity[0];
|
|
409
|
-
}
|
|
410
|
-
else {
|
|
411
|
-
const now = new Date();
|
|
412
|
-
const program_obj = zone.program[0];
|
|
413
|
-
const today_schedule = program_obj.day[now.getDay()].period.filter(period => period.enabled[0] === constants_1.STATUS.ON).reverse();
|
|
414
|
-
for (const i in today_schedule) {
|
|
415
|
-
const time = today_schedule[i].time[0];
|
|
416
|
-
const split = time.split(':');
|
|
417
|
-
if (
|
|
418
|
-
// The hour is past
|
|
419
|
-
Number(split[0]) < now.getHours() ||
|
|
420
|
-
// The hour is now, the minute is past
|
|
421
|
-
(Number(split[0]) === now.getHours() && Number(split[1]) < now.getMinutes())) {
|
|
422
|
-
return today_schedule[i].activity[0];
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
// If we got to the end without finding the next activity, it means the activity is the last from yesterday
|
|
426
|
-
const yesterday_schedule = program_obj['day'][(now.getDay() + 8) % 7].period.filter(period => period.enabled[0] === constants_1.STATUS.ON).reverse();
|
|
427
|
-
return yesterday_schedule[0].activity[0];
|
|
428
|
-
}
|
|
429
|
-
}), (0, rxjs_1.distinctUntilChanged)());
|
|
430
|
-
// TODO this could be made better, and more similar to above.
|
|
431
|
-
// maybe merge into one fxn?
|
|
432
|
-
// TODO: this doesnt seem to work right, add tests
|
|
433
|
-
this.next_activity_time = this.zone.pipe((0, rxjs_1.map)(zone => {
|
|
434
|
-
const now = new Date();
|
|
435
|
-
const program_obj = zone.program[0];
|
|
436
|
-
const day_obj = program_obj['day'][now.getDay()];
|
|
437
|
-
for (const i in day_obj['period']) {
|
|
438
|
-
const time = day_obj['period'][i].time[0];
|
|
439
|
-
const split = time.split(':');
|
|
440
|
-
if (
|
|
441
|
-
// The hour is nigh
|
|
442
|
-
Number(split[0]) > now.getHours() ||
|
|
443
|
-
// The hour is now, the minute is nigh
|
|
444
|
-
(Number(split[0]) === now.getHours() && Number(split[1]) > now.getMinutes())) {
|
|
445
|
-
return time;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
// If we got to the end without finding the next activity, it means the next activity is the first from tomorrow
|
|
449
|
-
const tomorrow_obj = program_obj['day'][(now.getDay() + 1) % 7];
|
|
450
|
-
return tomorrow_obj['period'][0].time[0];
|
|
451
|
-
}), (0, rxjs_1.distinctUntilChanged)());
|
|
452
|
-
this.current_activity_config$ = (0, rxjs_1.combineLatest)([
|
|
453
|
-
this.zone,
|
|
454
|
-
this.activity
|
|
455
|
-
]).pipe((0, rxjs_1.debounceTime)(50), (0, rxjs_1.map)(([zone, activity_name]) => {
|
|
456
|
-
return zone.activities[0].activity.find((a) => a['$'].id === activity_name);
|
|
457
|
-
}), (0, rxjs_1.distinctUntilChanged)());
|
|
458
|
-
this.fan = this.current_activity_config$.pipe((0, rxjs_1.map)(activity => activity.fan[0]), (0, rxjs_1.distinctUntilChanged)());
|
|
459
|
-
this.cool_setpoint = (0, rxjs_1.combineLatest)([this.current_activity_config$, this.temp_units$]).pipe((0, rxjs_1.debounceTime)(50), (0, rxjs_1.map)(([activity, temp_units]) => [Number(activity.clsp[0]), temp_units]), (0, helpers_rxjs_1.distinctUntilChangedWithEpsilon)());
|
|
460
|
-
this.heat_setpoint = (0, rxjs_1.combineLatest)([this.current_activity_config$, this.temp_units$]).pipe((0, rxjs_1.debounceTime)(50), (0, rxjs_1.map)(([activity, temp_units]) => [Number(activity.htsp[0]), temp_units]), (0, helpers_rxjs_1.distinctUntilChangedWithEpsilon)());
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
exports.SystemConfigZoneModel = SystemConfigZoneModel;
|
|
464
553
|
class SystemModel {
|
|
465
554
|
constructor(infinity_client, serialNumber) {
|
|
466
555
|
this.infinity_client = infinity_client;
|
|
467
556
|
this.serialNumber = serialNumber;
|
|
468
557
|
this.log = new helper_logging_1.PrefixLogger(this.infinity_client.log, this.serialNumber);
|
|
558
|
+
this.events = new events_1.default().setMaxListeners(100);
|
|
469
559
|
const api_logger = new helper_logging_1.PrefixLogger(this.log, 'API');
|
|
470
|
-
this.status = new SystemStatusModel(infinity_client, serialNumber, api_logger);
|
|
471
|
-
this.config = new SystemConfigModel(infinity_client, serialNumber, api_logger);
|
|
472
|
-
this.profile = new SystemProfileModel(infinity_client, serialNumber, api_logger);
|
|
473
|
-
}
|
|
474
|
-
getZoneActivity(zone) {
|
|
475
|
-
return (0, rxjs_1.combineLatest)([
|
|
476
|
-
this.status.getZone(zone).activity,
|
|
477
|
-
this.config.getZone(zone).activity,
|
|
478
|
-
]).pipe((0, rxjs_1.debounceTime)(50), (0, rxjs_1.map)(([s_activity, c_activity]) => {
|
|
479
|
-
// Vacation scheduling is weird, and changes infrequently. Just get it from status.
|
|
480
|
-
if (s_activity === constants_1.ACTIVITY.VACATION) {
|
|
481
|
-
return constants_1.ACTIVITY.VACATION;
|
|
482
|
-
}
|
|
483
|
-
// Config has more up to date activity settings.
|
|
484
|
-
return c_activity;
|
|
485
|
-
}), (0, rxjs_1.distinctUntilChanged)());
|
|
560
|
+
this.status = new SystemStatusModel(infinity_client, serialNumber, api_logger, this.events);
|
|
561
|
+
this.config = new SystemConfigModel(infinity_client, serialNumber, api_logger, this.events);
|
|
562
|
+
this.profile = new SystemProfileModel(infinity_client, serialNumber, api_logger, this.events);
|
|
486
563
|
}
|
|
487
564
|
}
|
|
488
565
|
exports.SystemModel = SystemModel;
|