homebridge-carrier-infinity 1.7.0-beta.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +438 -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,461 @@ 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
|
+
await this.fetch();
|
|
268
|
+
const utility_events = this.data_object.config.utilityEvent[0];
|
|
269
|
+
return [Number(utility_events.minLimit[0]), Number(utility_events.maxLimit[0])];
|
|
270
|
+
}
|
|
271
|
+
async getMode() {
|
|
272
|
+
await this.fetch();
|
|
273
|
+
return this.data_object.config.mode[0];
|
|
274
|
+
}
|
|
275
|
+
static getZone(data_object, zone) {
|
|
276
|
+
return data_object.config.zones[0].zone.find((z) => z['$'].id === zone.toString());
|
|
277
|
+
}
|
|
278
|
+
async getZone(zone) {
|
|
279
|
+
await this.fetch();
|
|
280
|
+
return SystemConfigModelReadOnly.getZone(this.data_object, zone);
|
|
281
|
+
}
|
|
282
|
+
async getZoneName(zone) {
|
|
283
|
+
const zone_obj = await this.getZone(zone);
|
|
284
|
+
return zone_obj['name'][0];
|
|
285
|
+
}
|
|
286
|
+
async getZoneHoldStatus(zone) {
|
|
287
|
+
const zone_obj = await this.getZone(zone);
|
|
288
|
+
return [zone_obj['hold'][0], zone_obj['otmr'][0]];
|
|
289
|
+
}
|
|
290
|
+
static getZoneActivity(data_object, zone) {
|
|
291
|
+
const zone_obj = SystemConfigModelReadOnly.getZone(data_object, zone);
|
|
292
|
+
if (zone_obj.hold[0] === constants_1.STATUS.ON) {
|
|
293
|
+
return zone_obj.holdActivity[0];
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const now = new Date();
|
|
297
|
+
const program_obj = SystemConfigModelReadOnly.getZone(data_object, zone).program[0];
|
|
298
|
+
const today_schedule = program_obj.day[now.getDay()].period.filter(period => period.enabled[0] === constants_1.STATUS.ON).reverse();
|
|
299
|
+
for (const i in today_schedule) {
|
|
300
|
+
const time = today_schedule[i].time[0];
|
|
301
|
+
const split = time.split(':');
|
|
302
|
+
if (
|
|
303
|
+
// The hour is past
|
|
304
|
+
Number(split[0]) < now.getHours() ||
|
|
305
|
+
// The hour is now, the minute is past
|
|
306
|
+
(Number(split[0]) === now.getHours() && Number(split[1]) < now.getMinutes())) {
|
|
307
|
+
return today_schedule[i].activity[0];
|
|
308
|
+
}
|
|
213
309
|
}
|
|
214
|
-
|
|
215
|
-
|
|
310
|
+
// If we got to the end without finding the next activity, it means the activity is the last from yesterday
|
|
311
|
+
const yesterday_schedule = program_obj['day'][(now.getDay() + 8) % 7].period.filter(period => period.enabled[0] === constants_1.STATUS.ON).reverse();
|
|
312
|
+
return yesterday_schedule[0].activity[0];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async getZoneActivity(zone) {
|
|
316
|
+
await this.fetch();
|
|
317
|
+
return SystemConfigModelReadOnly.getZoneActivity(this.data_object, zone);
|
|
318
|
+
}
|
|
319
|
+
static getZoneActivityConfig(data_object, zone, activity_name) {
|
|
320
|
+
// Vacation is stored somewhere else...
|
|
321
|
+
if (activity_name === constants_1.ACTIVITY.VACATION) {
|
|
322
|
+
return {
|
|
323
|
+
'$': { id: constants_1.ACTIVITY.VACATION },
|
|
324
|
+
clsp: data_object.config.vacmaxt,
|
|
325
|
+
htsp: data_object.config.vacmint,
|
|
326
|
+
fan: data_object.config.vacfan,
|
|
327
|
+
previousFan: [],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const activities_obj = SystemConfigModelReadOnly.getZone(data_object, zone).activities[0];
|
|
331
|
+
return activities_obj['activity'].find((activity) => activity['$'].id === activity_name);
|
|
332
|
+
}
|
|
333
|
+
async getZoneActivityConfig(zone, activity_name) {
|
|
334
|
+
await this.fetch();
|
|
335
|
+
return SystemConfigModelReadOnly.getZoneActivityConfig(this.data_object, zone, activity_name);
|
|
336
|
+
}
|
|
337
|
+
async getZoneActivityFan(zone, activity) {
|
|
338
|
+
const activity_obj = await this.getZoneActivityConfig(zone, activity);
|
|
339
|
+
return activity_obj.fan[0];
|
|
340
|
+
}
|
|
341
|
+
async getZoneActivityCoolSetpoint(zone, activity) {
|
|
342
|
+
const activity_obj = await this.getZoneActivityConfig(zone, activity);
|
|
343
|
+
return Number(activity_obj.clsp[0]);
|
|
344
|
+
}
|
|
345
|
+
async getZoneActivityHeatSetpoint(zone, activity) {
|
|
346
|
+
const activity_obj = await this.getZoneActivityConfig(zone, activity);
|
|
347
|
+
return Number(activity_obj.htsp[0]);
|
|
348
|
+
}
|
|
349
|
+
async getZoneNextActivityTime(zone) {
|
|
350
|
+
const now = new Date();
|
|
351
|
+
const program_obj = (await this.getZone(zone)).program[0];
|
|
352
|
+
const day_obj = program_obj['day'][now.getDay()];
|
|
353
|
+
for (const i in day_obj['period']) {
|
|
354
|
+
const time = day_obj['period'][i].time[0];
|
|
355
|
+
const split = time.split(':');
|
|
356
|
+
if (
|
|
357
|
+
// The hour is nigh
|
|
358
|
+
Number(split[0]) > now.getHours() ||
|
|
359
|
+
// The hour is now, the minute is nigh
|
|
360
|
+
(Number(split[0]) === now.getHours() && Number(split[1]) > now.getMinutes())) {
|
|
361
|
+
return time;
|
|
216
362
|
}
|
|
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)());
|
|
363
|
+
}
|
|
364
|
+
// If we got to the end without finding the next activity, it means the next activity is the first from tomorrow
|
|
365
|
+
const tomorrow_obj = program_obj['day'][(now.getDay() + 1) % 7];
|
|
366
|
+
return tomorrow_obj['period'][0].time[0];
|
|
227
367
|
}
|
|
228
368
|
}
|
|
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...'));
|
|
369
|
+
exports.SystemConfigModelReadOnly = SystemConfigModelReadOnly;
|
|
370
|
+
class SystemConfigModel extends SystemConfigModelReadOnly {
|
|
371
|
+
constructor() {
|
|
372
|
+
super(...arguments);
|
|
373
|
+
/*
|
|
374
|
+
* A writable version of the system config model.
|
|
375
|
+
*
|
|
376
|
+
* Provides a set data api, which is cached locally, and periodically pushed
|
|
377
|
+
* out to the carrier api.
|
|
378
|
+
*/
|
|
379
|
+
this.mutations = [];
|
|
262
380
|
}
|
|
263
|
-
|
|
264
|
-
|
|
381
|
+
// Skip fetching new data when we have a dirty local state.
|
|
382
|
+
async fetch() {
|
|
383
|
+
if (this.mutations.length > 0) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
await super.fetch();
|
|
265
387
|
}
|
|
266
|
-
|
|
267
|
-
//
|
|
268
|
-
|
|
388
|
+
async push() {
|
|
389
|
+
// While waiting to push to api, push locally to HK.
|
|
390
|
+
this.events.emit(constants_1.SUBSCRIPTION.CONFIG_MUTATE);
|
|
391
|
+
// Wait a bit so we can catch other mutations that came in around the
|
|
392
|
+
// same time.
|
|
393
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
394
|
+
// We only ever need 2 pushes ongoing at a time. One active, and one pending.
|
|
395
|
+
// The first one will handle mutations available at its start, and the next
|
|
396
|
+
// one will cover mutations that arrived during the previous's run.
|
|
397
|
+
// First, to make sure we only ever have one 'pending' push, cancel any other
|
|
398
|
+
// possible 'pending' pushes, and make this one become the 'pending' push.
|
|
399
|
+
this.write_lock.cancel();
|
|
400
|
+
// Then, grab the lock. so this push can move from 'pending' to 'active'.
|
|
401
|
+
try {
|
|
402
|
+
await this.write_lock.runExclusive(async () => {
|
|
403
|
+
// 1. Do mutations
|
|
404
|
+
const mutated = await this.mutate();
|
|
405
|
+
if (mutated === null) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const [mutated_hash, mutated_data_object] = mutated;
|
|
409
|
+
// 2. Push
|
|
410
|
+
await this.forcePush(mutated_data_object);
|
|
411
|
+
this.log.info('... pushing changes complete.');
|
|
412
|
+
// 3. Confirm
|
|
413
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
414
|
+
if (this.mutations.length > 0) {
|
|
415
|
+
// If local state is dirty (from new mutations queued during push)
|
|
416
|
+
// don't do a forceFetch or it will cause apparent bouncing, let the
|
|
417
|
+
// next push refresh from remote api state.
|
|
418
|
+
// This is safe because mutations are always done on a fresh fetched
|
|
419
|
+
// config, even if local config is dirty or stale.
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await this.forceFetch();
|
|
423
|
+
if (mutated_hash === this.data_object_hash) {
|
|
424
|
+
this.log.debug('Successful propagation to carrier api is confirmed.');
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
this.log.warn('Changes do not (yet?) appear to have propagated to the carrier api.');
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
if (e === async_mutex_1.E_CANCELED) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
else if (e === async_mutex_1.E_TIMEOUT || e === async_mutex_1.E_ALREADY_LOCKED) {
|
|
436
|
+
this.log.error(`Deadlock on push ${e}. Report bug: https://bit.ly/3igbU7D`);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
this.log.error('Failed to push updates: ', axios_1.default.isAxiosError(e) ? e.message : e);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
269
442
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
443
|
+
async mutate() {
|
|
444
|
+
// short circuit if no mutations in queue
|
|
445
|
+
if (this.mutations.length === 0) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
// Refresh config, to make sure we don't write back old data accidentally.
|
|
449
|
+
const stale_hash = this.data_object_hash;
|
|
450
|
+
const [fresh_hash, fresh_data_object] = await this.forceFetchInternal();
|
|
451
|
+
if (stale_hash !== fresh_hash) {
|
|
452
|
+
this.log.warn('Cached config was stale before mutation and push.');
|
|
453
|
+
}
|
|
454
|
+
// Take config mutations of the queue and run them against the fresh object.
|
|
455
|
+
// This ensures we don't overwrite other data if the api config has changed,
|
|
456
|
+
// and avoids a race condition where new mutations come in during the push.
|
|
457
|
+
const mutated_data_object = fresh_data_object;
|
|
458
|
+
while (this.mutations.length > 0) {
|
|
459
|
+
const m = this.mutations.shift();
|
|
460
|
+
if (m) {
|
|
461
|
+
m(mutated_data_object);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const mutated_hash = this.hashDataObject(mutated_data_object);
|
|
276
465
|
// 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;
|
|
466
|
+
if (fresh_hash === mutated_hash) {
|
|
467
|
+
this.log.warn('Config doesn\'t appear to have changed. No changes sent.');
|
|
468
|
+
return null;
|
|
298
469
|
}
|
|
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');
|
|
470
|
+
return [mutated_hash, mutated_data_object];
|
|
320
471
|
}
|
|
321
|
-
async forcePush(
|
|
322
|
-
this.log.info('
|
|
472
|
+
async forcePush(data_object) {
|
|
473
|
+
this.log.info('Pushing changes to carrier api...');
|
|
323
474
|
const builder = new xml2js.Builder();
|
|
324
|
-
const new_xml = builder.buildObject(
|
|
325
|
-
const
|
|
326
|
-
await this.infinity_client.axios.post(this.getPath(),
|
|
475
|
+
const new_xml = builder.buildObject(data_object);
|
|
476
|
+
const data = `data=${encodeURIComponent(new_xml)}`;
|
|
477
|
+
await this.infinity_client.axios.post(this.getPath(), data, {
|
|
327
478
|
headers: {
|
|
328
479
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
329
480
|
},
|
|
330
481
|
});
|
|
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
482
|
}
|
|
335
483
|
async setMode(mode) {
|
|
336
484
|
this.log.debug('Setting mode to ' + mode);
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
485
|
+
const m = (data_object) => {
|
|
486
|
+
SystemConfigModel.mutateMode(data_object, mode);
|
|
487
|
+
};
|
|
488
|
+
// Mutate the local state to propagate changes throughout HK.
|
|
489
|
+
m(this.data_object);
|
|
490
|
+
// Schedule the push event, but don't wait for it to return.
|
|
491
|
+
this.mutations.push(m);
|
|
492
|
+
this.push();
|
|
493
|
+
}
|
|
494
|
+
static mutateMode(data_object, mode) {
|
|
495
|
+
data_object.config.mode[0] = mode;
|
|
340
496
|
}
|
|
341
497
|
async setZoneActivityHold(zone, activity, hold_until) {
|
|
342
498
|
this.log.debug(`Setting zone ${zone} activity to ${activity} until ${hold_until}`);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
499
|
+
const m = (data_object) => {
|
|
500
|
+
SystemConfigModel.mutateZoneActivityHold(data_object, zone, activity, hold_until);
|
|
501
|
+
};
|
|
502
|
+
// Mutate the local state to propagate changes throughout HK.
|
|
503
|
+
m(this.data_object);
|
|
504
|
+
// Schedule the push event, but don't wait for it to return.
|
|
505
|
+
this.mutations.push(m);
|
|
506
|
+
this.push();
|
|
507
|
+
}
|
|
508
|
+
static mutateZoneActivityHold(data_object, zone, activity, hold_until) {
|
|
509
|
+
const zone_obj = SystemConfigModel.getZone(data_object, zone);
|
|
346
510
|
zone_obj['holdActivity'][0] = activity;
|
|
347
511
|
zone_obj['hold'][0] = activity ? constants_1.STATUS.ON : constants_1.STATUS.OFF;
|
|
348
512
|
zone_obj['otmr'][0] = activity ? hold_until || '' : '';
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
513
|
+
}
|
|
514
|
+
async setZoneActivityManualHold(zone, clsp, htsp, hold_until, fan = null) {
|
|
515
|
+
this.log.debug(`Setting zone ${zone} to`, clsp ? `clsp=${clsp}` : '', htsp ? `htsp=${htsp}` : '', fan ? `fan=${fan}` : '', '.');
|
|
516
|
+
const m = (data_object) => {
|
|
517
|
+
// Modify MANUAL activity to the requested setpoints
|
|
518
|
+
SystemConfigModel.mutateZoneActivityManualHold(data_object, zone, clsp, htsp, fan);
|
|
519
|
+
// Set hold to MANUAL activity
|
|
520
|
+
SystemConfigModel.mutateZoneActivityHold(data_object, zone, constants_1.ACTIVITY.MANUAL, hold_until);
|
|
521
|
+
};
|
|
522
|
+
// Mutate the local state to propagate changes throughout HK.
|
|
523
|
+
m(this.data_object);
|
|
524
|
+
// Schedule the push event, but don't wait for it to return.
|
|
525
|
+
this.mutations.push(m);
|
|
526
|
+
this.push();
|
|
527
|
+
}
|
|
528
|
+
static mutateZoneActivityManualHold(data_object, zone, clsp, htsp, fan = null) {
|
|
529
|
+
const zone_obj = SystemConfigModel.getZone(data_object, zone);
|
|
530
|
+
// When moving to manual activity, default to prev activity settings.
|
|
531
|
+
const manual_activity_obj = SystemConfigModel.getZoneActivityConfig(data_object, zone, constants_1.ACTIVITY.MANUAL);
|
|
532
|
+
if (zone_obj['holdActivity'][0] !== constants_1.ACTIVITY.MANUAL) {
|
|
533
|
+
const prev_activity_obj = SystemConfigModel.getZoneActivityConfig(data_object, zone, SystemConfigModel.getZoneActivity(data_object, zone));
|
|
366
534
|
manual_activity_obj['clsp'][0] = prev_activity_obj['clsp'][0];
|
|
367
535
|
manual_activity_obj['htsp'][0] = prev_activity_obj['htsp'][0];
|
|
368
536
|
manual_activity_obj['fan'][0] = prev_activity_obj['fan'][0];
|
|
369
|
-
// Push changes
|
|
370
|
-
this.dirty_data$.next(data);
|
|
371
537
|
}
|
|
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
538
|
// 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
|
|
539
|
+
[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
540
|
// when setpoints are too close, make clsp sticky when no change made to htsp
|
|
382
541
|
htsp === null);
|
|
383
542
|
manual_activity_obj['htsp'][0] = htsp.toFixed(1);
|
|
384
543
|
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);
|
|
544
|
+
// Set fan on manual activity
|
|
545
|
+
if (fan) {
|
|
546
|
+
manual_activity_obj['fan'][0] = fan;
|
|
547
|
+
}
|
|
396
548
|
}
|
|
397
549
|
}
|
|
398
550
|
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
551
|
class SystemModel {
|
|
465
552
|
constructor(infinity_client, serialNumber) {
|
|
466
553
|
this.infinity_client = infinity_client;
|
|
467
554
|
this.serialNumber = serialNumber;
|
|
468
555
|
this.log = new helper_logging_1.PrefixLogger(this.infinity_client.log, this.serialNumber);
|
|
556
|
+
this.events = new events_1.default().setMaxListeners(100);
|
|
469
557
|
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)());
|
|
558
|
+
this.status = new SystemStatusModel(infinity_client, serialNumber, api_logger, this.events);
|
|
559
|
+
this.config = new SystemConfigModel(infinity_client, serialNumber, api_logger, this.events);
|
|
560
|
+
this.profile = new SystemProfileModel(infinity_client, serialNumber, api_logger, this.events);
|
|
486
561
|
}
|
|
487
562
|
}
|
|
488
563
|
exports.SystemModel = SystemModel;
|