homebridge-carrier-infinity 1.6.9 → 1.7.0-beta.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_comfort_activity.d.ts.map +1 -1
- package/dist/accessory_comfort_activity.js +6 -9
- 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 +4 -3
- package/dist/accessory_oat.js.map +1 -1
- package/dist/accessory_thermostat.d.ts.map +1 -1
- package/dist/accessory_thermostat.js +6 -27
- package/dist/accessory_thermostat.js.map +1 -1
- package/dist/api/helpers.d.ts +5 -0
- package/dist/api/helpers.d.ts.map +1 -0
- package/dist/api/helpers.js +31 -0
- package/dist/api/helpers.js.map +1 -0
- package/dist/api/helpers_rxjs.d.ts +3 -0
- package/dist/api/helpers_rxjs.d.ts.map +1 -0
- package/dist/api/helpers_rxjs.js +16 -0
- package/dist/api/helpers_rxjs.js.map +1 -0
- package/dist/api/models.d.ts +78 -56
- package/dist/api/models.d.ts.map +1 -1
- package/dist/api/models.js +352 -356
- package/dist/api/models.js.map +1 -1
- package/dist/api/oauth.js +1 -1
- package/dist/api/oauth.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 +70 -42
- package/dist/characteristics_ac.js.map +1 -1
- package/dist/characteristics_base.d.ts +9 -6
- package/dist/characteristics_base.d.ts.map +1 -1
- package/dist/characteristics_base.js +56 -37
- 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 +75 -63
- 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 +5 -8
- package/dist/characteristics_filter.js.map +1 -1
- package/dist/characteristics_humidity.d.ts +2 -5
- package/dist/characteristics_humidity.d.ts.map +1 -1
- package/dist/characteristics_humidity.js +9 -17
- package/dist/characteristics_humidity.js.map +1 -1
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +12 -6
- package/dist/platform.js.map +1 -1
- package/package.json +28 -23
package/dist/api/models.js
CHANGED
|
@@ -15,12 +15,6 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
|
|
15
15
|
}) : function(o, v) {
|
|
16
16
|
o["default"] = v;
|
|
17
17
|
});
|
|
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;
|
|
23
|
-
};
|
|
24
18
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
25
19
|
if (mod && mod.__esModule) return mod;
|
|
26
20
|
var result = {};
|
|
@@ -32,53 +26,69 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
32
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
33
27
|
};
|
|
34
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
-
exports.SystemModel = exports.SystemConfigModel = exports.SystemStatusModel = exports.SystemProfileModel = exports.LocationsModel = void 0;
|
|
29
|
+
exports.SystemModel = exports.SystemConfigZoneModel = exports.SystemConfigModel = exports.SystemStatusZoneModel = exports.SystemStatusModel = exports.SystemProfileModel = exports.LocationsModel = void 0;
|
|
36
30
|
const helpers_1 = require("../helpers");
|
|
37
|
-
const typescript_memoize_1 = require("typescript-memoize");
|
|
38
31
|
const async_mutex_1 = require("async-mutex");
|
|
39
32
|
const xml2js = __importStar(require("xml2js"));
|
|
40
33
|
const object_hash_1 = __importDefault(require("object-hash"));
|
|
41
34
|
const helper_logging_1 = require("../helper_logging");
|
|
42
35
|
const axios_1 = __importDefault(require("axios"));
|
|
43
36
|
const constants_1 = require("./constants");
|
|
37
|
+
const rxjs_1 = require("rxjs");
|
|
38
|
+
const events_1 = __importDefault(require("events"));
|
|
39
|
+
const helpers_2 = require("./helpers");
|
|
40
|
+
const helpers_rxjs_1 = require("./helpers_rxjs");
|
|
44
41
|
class BaseModel {
|
|
45
42
|
constructor(infinity_client) {
|
|
46
43
|
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();
|
|
47
48
|
this.HASH_IGNORE_KEYS = new Set();
|
|
48
49
|
this.log = new helper_logging_1.PrefixLogger(this.infinity_client.log, 'API');
|
|
50
|
+
this.events = new events_1.default();
|
|
49
51
|
this.write_lock = new async_mutex_1.Mutex();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
// Set up the triggers for updating our api data
|
|
53
|
+
const ticks = (0, rxjs_1.merge)(
|
|
54
|
+
// Immediate Fetch
|
|
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) => {
|
|
53
68
|
return this.HASH_IGNORE_KEYS.has(key);
|
|
54
69
|
} });
|
|
55
70
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
fetchObservable() {
|
|
72
|
+
return new rxjs_1.Observable((observer) => {
|
|
73
|
+
this.forceFetch()
|
|
74
|
+
.then((data) => {
|
|
75
|
+
observer.next(data);
|
|
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();
|
|
61
83
|
});
|
|
62
|
-
}
|
|
63
|
-
catch (e) {
|
|
64
|
-
if (e === async_mutex_1.E_ALREADY_LOCKED) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
else if (e === async_mutex_1.E_TIMEOUT || e === async_mutex_1.E_CANCELED) {
|
|
68
|
-
this.log.error(`Deadlock on fetch ${e}. Report bug: https://bit.ly/3igbU7D`);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
this.log.error('Failed to fetch updates: ', axios_1.default.isAxiosError(e) ? e.message : e);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
84
|
+
});
|
|
74
85
|
}
|
|
75
86
|
async forceFetch() {
|
|
76
87
|
await this.infinity_client.refreshToken();
|
|
77
88
|
await this.infinity_client.activate();
|
|
78
89
|
const response = await this.infinity_client.axios.get(this.getPath());
|
|
79
90
|
if (response.data) {
|
|
80
|
-
|
|
81
|
-
this.data_object_hash = this.hashDataObject();
|
|
91
|
+
return await xml2js.parseStringPromise(response.data);
|
|
82
92
|
}
|
|
83
93
|
else {
|
|
84
94
|
this.log.debug(response.data);
|
|
@@ -86,24 +96,23 @@ class BaseModel {
|
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
|
-
__decorate([
|
|
90
|
-
(0, typescript_memoize_1.MemoizeExpiring)(10 * 1000)
|
|
91
|
-
], BaseModel.prototype, "fetch", null);
|
|
92
99
|
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
|
+
}
|
|
93
113
|
getPath() {
|
|
94
114
|
return `/users/${this.infinity_client.username}/locations`;
|
|
95
115
|
}
|
|
96
|
-
async getSystems() {
|
|
97
|
-
await this.fetch();
|
|
98
|
-
const systems = [];
|
|
99
|
-
for (const location of this.data_object.locations.location) {
|
|
100
|
-
for (const system of location.systems[0].system || []) {
|
|
101
|
-
const link_parts = system['atom:link'][0]['$']['href'].split('/');
|
|
102
|
-
systems.push(link_parts[link_parts.length - 1]);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return systems;
|
|
106
|
-
}
|
|
107
116
|
}
|
|
108
117
|
exports.LocationsModel = LocationsModel;
|
|
109
118
|
class BaseSystemModel extends BaseModel {
|
|
@@ -112,372 +121,346 @@ class BaseSystemModel extends BaseModel {
|
|
|
112
121
|
this.infinity_client = infinity_client;
|
|
113
122
|
this.serialNumber = serialNumber;
|
|
114
123
|
this.log = log;
|
|
115
|
-
|
|
116
|
-
this.
|
|
124
|
+
// TODO: these 'last' values are problematic, since they can be race-y.
|
|
125
|
+
this.last_fetched_ts = 0;
|
|
126
|
+
this.last_fetched_hash = '';
|
|
127
|
+
this.HASH_IGNORE_KEYS = new Set(['timestamp', 'localTime', 'previousMode']);
|
|
117
128
|
}
|
|
118
129
|
async forceFetch() {
|
|
119
|
-
await super.forceFetch();
|
|
120
|
-
const top_level_key = Object.keys(
|
|
121
|
-
const ts =
|
|
122
|
-
this.
|
|
123
|
-
this.
|
|
130
|
+
const data_object = await super.forceFetch();
|
|
131
|
+
const top_level_key = Object.keys(data_object)[0];
|
|
132
|
+
const ts = data_object[top_level_key].timestamp[0];
|
|
133
|
+
this.last_fetched_ts = Date.parse(ts);
|
|
134
|
+
this.last_fetched_hash = this.hash(data_object);
|
|
135
|
+
this.log.debug(`TIMESTAMP ${this.getPath()} reports ${ts} (${this.last_fetched_ts})`);
|
|
136
|
+
this.log.debug(`HASH ${this.getPath()} hashes to (${this.last_fetched_hash})`);
|
|
137
|
+
return data_object;
|
|
124
138
|
}
|
|
125
139
|
}
|
|
126
140
|
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
|
+
}
|
|
127
151
|
getPath() {
|
|
128
152
|
return `/systems/${this.serialNumber}/profile`;
|
|
129
153
|
}
|
|
130
|
-
async getName() {
|
|
131
|
-
await this.fetch();
|
|
132
|
-
return this.data_object.system_profile.name[0];
|
|
133
|
-
}
|
|
134
|
-
async getBrand() {
|
|
135
|
-
await this.fetch();
|
|
136
|
-
return this.data_object.system_profile.brand[0];
|
|
137
|
-
}
|
|
138
|
-
async getModel() {
|
|
139
|
-
await this.fetch();
|
|
140
|
-
return this.data_object.system_profile.model[0];
|
|
141
|
-
}
|
|
142
|
-
async getFirmware() {
|
|
143
|
-
await this.fetch();
|
|
144
|
-
return this.data_object.system_profile.firmware[0];
|
|
145
|
-
}
|
|
146
|
-
async getZones() {
|
|
147
|
-
await this.fetch();
|
|
148
|
-
return this.data_object.system_profile.zones[0].zone.filter((zone) => zone['present'][0] === constants_1.STATUS.ON).map((zone) => zone['$'].id);
|
|
149
|
-
}
|
|
150
154
|
}
|
|
151
155
|
exports.SystemProfileModel = SystemProfileModel;
|
|
152
156
|
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
|
+
}
|
|
153
177
|
getPath() {
|
|
154
178
|
return `/systems/${this.serialNumber}/status`;
|
|
155
179
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return this.
|
|
159
|
-
}
|
|
160
|
-
async getOutdoorTemp() {
|
|
161
|
-
await this.fetch();
|
|
162
|
-
return Number(this.data_object.status.oat[0]);
|
|
163
|
-
}
|
|
164
|
-
async getFilterUsed() {
|
|
165
|
-
await this.fetch();
|
|
166
|
-
return Number(this.data_object.status.filtrlvl[0]);
|
|
180
|
+
getZone(zone) {
|
|
181
|
+
// TODO save SystemStatusZoneModel to dedup
|
|
182
|
+
return new SystemStatusZoneModel(this.raw_zone_data$.pipe((0, rxjs_1.map)(data => (0, helpers_2.findZoneByID)(data, zone))), this.temp_units);
|
|
167
183
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
case 'dehumidify':
|
|
177
|
-
return constants_1.SYSTEM_MODE.COOL;
|
|
178
|
-
default:
|
|
179
|
-
return raw_mode;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
async getZone(zone) {
|
|
183
|
-
await this.fetch();
|
|
184
|
-
return this.data_object.status.zones[0].zone.find((z) => z['$'].id === zone.toString());
|
|
185
|
-
}
|
|
186
|
-
async getZoneConditioning(zone) {
|
|
187
|
-
const raw_mode = (await this.getZone(zone)).zoneconditioning[0];
|
|
188
|
-
switch (raw_mode) {
|
|
189
|
-
case 'active_heat':
|
|
190
|
-
case 'prep_heat':
|
|
191
|
-
case 'pending_heat':
|
|
192
|
-
return constants_1.SYSTEM_MODE.HEAT;
|
|
193
|
-
case 'active_cool':
|
|
194
|
-
case 'prep_cool':
|
|
195
|
-
case 'pending_cool':
|
|
196
|
-
return constants_1.SYSTEM_MODE.COOL;
|
|
197
|
-
case 'idle':
|
|
184
|
+
}
|
|
185
|
+
exports.SystemStatusModel = SystemStatusModel;
|
|
186
|
+
class SystemStatusZoneModel {
|
|
187
|
+
constructor(zone, temp_units$) {
|
|
188
|
+
this.zone = zone;
|
|
189
|
+
this.temp_units$ = temp_units$;
|
|
190
|
+
this.mode = this.zone.pipe((0, rxjs_1.map)(zone => {
|
|
191
|
+
if (zone.damperposition[0] === '0') {
|
|
198
192
|
return constants_1.SYSTEM_MODE.OFF;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
193
|
+
}
|
|
194
|
+
const raw_mode = zone.zoneconditioning[0];
|
|
195
|
+
switch (raw_mode) {
|
|
196
|
+
case 'active_heat':
|
|
197
|
+
case 'prep_heat':
|
|
198
|
+
case 'pending_heat':
|
|
199
|
+
return constants_1.SYSTEM_MODE.HEAT;
|
|
200
|
+
case 'active_cool':
|
|
201
|
+
case 'prep_cool':
|
|
202
|
+
case 'pending_cool':
|
|
203
|
+
return constants_1.SYSTEM_MODE.COOL;
|
|
204
|
+
case 'idle':
|
|
205
|
+
return constants_1.SYSTEM_MODE.OFF;
|
|
206
|
+
default:
|
|
207
|
+
return raw_mode;
|
|
208
|
+
}
|
|
209
|
+
}), (0, rxjs_1.distinctUntilChanged)());
|
|
210
|
+
this.fan = this.zone.pipe((0, rxjs_1.map)(zone => {
|
|
211
|
+
if (zone.damperposition[0] === '0') {
|
|
212
|
+
return constants_1.FAN_MODE.OFF;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
return zone.fan[0];
|
|
216
|
+
}
|
|
217
|
+
}), (0, rxjs_1.distinctUntilChanged)());
|
|
218
|
+
this.activity = this.zone.pipe((0, rxjs_1.map)(zone => zone.currentActivity[0]), (0, rxjs_1.distinctUntilChanged)());
|
|
219
|
+
// The zone is blowing if the mode is on or the fan is on
|
|
220
|
+
this.blowing = (0, rxjs_1.combineLatest)([this.mode, this.fan]).pipe((0, rxjs_1.debounceTime)(50), (0, rxjs_1.map)(([mode, fan]) => mode !== constants_1.SYSTEM_MODE.OFF || fan !== constants_1.FAN_MODE.OFF), (0, rxjs_1.distinctUntilChanged)());
|
|
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)());
|
|
229
227
|
}
|
|
230
228
|
}
|
|
231
|
-
exports.
|
|
229
|
+
exports.SystemStatusZoneModel = SystemStatusZoneModel;
|
|
232
230
|
class SystemConfigModel extends BaseSystemModel {
|
|
233
|
-
constructor() {
|
|
234
|
-
super(
|
|
235
|
-
|
|
236
|
-
this.
|
|
231
|
+
constructor(infinity_client, serialNumber, log) {
|
|
232
|
+
super(infinity_client, serialNumber, log);
|
|
233
|
+
this.infinity_client = infinity_client;
|
|
234
|
+
this.serialNumber = serialNumber;
|
|
235
|
+
this.log = log;
|
|
236
|
+
// This will always hold the 'dirty' version of the config. This is what is
|
|
237
|
+
// changed by set methods.
|
|
238
|
+
this.dirty_data$ = new rxjs_1.ReplaySubject(1);
|
|
239
|
+
// This combines the clean and dirty data and is what is used by api observers.
|
|
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...'));
|
|
237
262
|
}
|
|
238
263
|
getPath() {
|
|
239
264
|
return `/systems/${this.serialNumber}/config`;
|
|
240
265
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return this.
|
|
244
|
-
}
|
|
245
|
-
async getTempBounds() {
|
|
246
|
-
await this.fetch();
|
|
247
|
-
const utility_events = this.data_object.config.utilityEvent[0];
|
|
248
|
-
return [Number(utility_events.minLimit[0]), Number(utility_events.maxLimit[0])];
|
|
249
|
-
}
|
|
250
|
-
async getMode() {
|
|
251
|
-
await this.fetch();
|
|
252
|
-
return this.data_object.config.mode[0];
|
|
266
|
+
getZone(zone) {
|
|
267
|
+
// TODO save SystemConfigZoneModel to dedup
|
|
268
|
+
return new SystemConfigZoneModel(this.raw_zone_data$.pipe((0, rxjs_1.map)(data => (0, helpers_2.findZoneByID)(data, zone))), this.temp_units);
|
|
253
269
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
async getZoneName(zone) {
|
|
259
|
-
const zone_obj = await this.getZone(zone);
|
|
260
|
-
return zone_obj['name'][0];
|
|
261
|
-
}
|
|
262
|
-
async getZoneHoldStatus(zone) {
|
|
263
|
-
const zone_obj = await this.getZone(zone);
|
|
264
|
-
return [zone_obj['hold'][0], zone_obj['otmr'][0]];
|
|
265
|
-
}
|
|
266
|
-
async getZoneActivity(zone) {
|
|
267
|
-
const zone_obj = await this.getZone(zone);
|
|
268
|
-
if (zone_obj.hold[0] === constants_1.STATUS.ON) {
|
|
269
|
-
return zone_obj.holdActivity[0];
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
const now = new Date();
|
|
273
|
-
const program_obj = (await this.getZone(zone)).program[0];
|
|
274
|
-
const today_schedule = program_obj.day[now.getDay()].period.filter(period => period.enabled[0] === constants_1.STATUS.ON).reverse();
|
|
275
|
-
for (const i in today_schedule) {
|
|
276
|
-
const time = today_schedule[i].time[0];
|
|
277
|
-
const split = time.split(':');
|
|
278
|
-
if (
|
|
279
|
-
// The hour is past
|
|
280
|
-
Number(split[0]) < now.getHours() ||
|
|
281
|
-
// The hour is now, the minute is past
|
|
282
|
-
(Number(split[0]) === now.getHours() && Number(split[1]) < now.getMinutes())) {
|
|
283
|
-
return today_schedule[i].activity[0];
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
// If we got to the end without finding the next activity, it means the activity is the last from yesterday
|
|
287
|
-
const yesterday_schedule = program_obj['day'][(now.getDay() + 8) % 7].period.filter(period => period.enabled[0] === constants_1.STATUS.ON).reverse();
|
|
288
|
-
return yesterday_schedule[0].activity[0];
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
async getZoneActivityConfig(zone, activity_name) {
|
|
292
|
-
await this.fetch();
|
|
293
|
-
// Vacation is stored somewhere else...
|
|
294
|
-
if (activity_name === constants_1.ACTIVITY.VACATION) {
|
|
295
|
-
return {
|
|
296
|
-
'$': { id: constants_1.ACTIVITY.VACATION },
|
|
297
|
-
clsp: this.data_object.config.vacmaxt,
|
|
298
|
-
htsp: this.data_object.config.vacmint,
|
|
299
|
-
fan: this.data_object.config.vacfan,
|
|
300
|
-
previousFan: [],
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
const activities_obj = (await this.getZone(zone)).activities[0];
|
|
304
|
-
return activities_obj['activity'].find((activity) => activity['$'].id === activity_name);
|
|
305
|
-
}
|
|
306
|
-
async getZoneActivityFan(zone, activity) {
|
|
307
|
-
const activity_obj = await this.getZoneActivityConfig(zone, activity);
|
|
308
|
-
return activity_obj.fan[0];
|
|
309
|
-
}
|
|
310
|
-
async getZoneActivityCoolSetpoint(zone, activity) {
|
|
311
|
-
const activity_obj = await this.getZoneActivityConfig(zone, activity);
|
|
312
|
-
return Number(activity_obj.clsp[0]);
|
|
313
|
-
}
|
|
314
|
-
async getZoneActivityHeatSetpoint(zone, activity) {
|
|
315
|
-
const activity_obj = await this.getZoneActivityConfig(zone, activity);
|
|
316
|
-
return Number(activity_obj.htsp[0]);
|
|
317
|
-
}
|
|
318
|
-
async getZoneNextActivityTime(zone) {
|
|
270
|
+
/* Write APIs */
|
|
271
|
+
async push(data) {
|
|
272
|
+
this.log.info('Start pushing changes to carrier api...');
|
|
273
|
+
// Pause clean data use until we see an update from after now.
|
|
319
274
|
const now = new Date();
|
|
320
|
-
|
|
321
|
-
const day_obj = program_obj['day'][now.getDay()];
|
|
322
|
-
for (const i in day_obj['period']) {
|
|
323
|
-
const time = day_obj['period'][i].time[0];
|
|
324
|
-
const split = time.split(':');
|
|
325
|
-
if (
|
|
326
|
-
// The hour is nigh
|
|
327
|
-
Number(split[0]) > now.getHours() ||
|
|
328
|
-
// The hour is now, the minute is nigh
|
|
329
|
-
(Number(split[0]) === now.getHours() && Number(split[1]) > now.getMinutes())) {
|
|
330
|
-
return time;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
// If we got to the end without finding the next activity, it means the next activity is the first from tomorrow
|
|
334
|
-
const tomorrow_obj = program_obj['day'][(now.getDay() + 1) % 7];
|
|
335
|
-
return tomorrow_obj['period'][0].time[0];
|
|
336
|
-
}
|
|
337
|
-
async push() {
|
|
338
|
-
// Wait a bit so we can catch other mutations that came in around the
|
|
339
|
-
// same time.
|
|
340
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
341
|
-
// We only ever need 2 pushes ongoing at a time. One active, and one pending.
|
|
342
|
-
// The first one will handle mutations available at its start, and the next
|
|
343
|
-
// one will cover mutations that arrived during the previous's run.
|
|
344
|
-
// First, to make sure we only ever have one 'pending' push, cancel any other
|
|
345
|
-
// possible 'pending' pushes, and make this one become the 'pending' push.
|
|
346
|
-
this.write_lock.cancel();
|
|
347
|
-
// Then, grab the lock. so this push can move from 'pending' to 'active'.
|
|
348
|
-
try {
|
|
349
|
-
await this.write_lock.runExclusive(async () => {
|
|
350
|
-
// 1. Do mutations
|
|
351
|
-
const mutated_hash = await this.mutate();
|
|
352
|
-
if (mutated_hash === null) {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
// 2. Push
|
|
356
|
-
await this.forcePush();
|
|
357
|
-
this.log.info('... pushing changes complete.');
|
|
358
|
-
// 3. Confirm
|
|
359
|
-
await new Promise(r => setTimeout(r, 5000));
|
|
360
|
-
await this.forceFetch();
|
|
361
|
-
if (mutated_hash === this.data_object_hash) {
|
|
362
|
-
this.log.debug('Successful propagation to carrier api is confirmed.');
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
this.log.warn('Changes do not (yet?) appear to have propagated to the carrier api.');
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
catch (e) {
|
|
370
|
-
if (e === async_mutex_1.E_CANCELED) {
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
else if (e === async_mutex_1.E_TIMEOUT || e === async_mutex_1.E_ALREADY_LOCKED) {
|
|
374
|
-
this.log.error(`Deadlock on push ${e}. Report bug: https://bit.ly/3igbU7D`);
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
this.log.error('Failed to push updates: ', axios_1.default.isAxiosError(e) ? e.message : e);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
async mutate() {
|
|
382
|
-
// short circuit if no mutations in queue
|
|
383
|
-
if (this.mutations.length === 0) {
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
// Refresh config.
|
|
387
|
-
const old_hash = this.data_object_hash;
|
|
388
|
-
await this.forceFetch();
|
|
389
|
-
if (old_hash !== this.data_object_hash) {
|
|
390
|
-
this.log.warn('Cached config was stale before mutation and push.');
|
|
391
|
-
}
|
|
392
|
-
// Take config mutations of the queue and run them.
|
|
393
|
-
// TODO make mutations non-async. these need to happen in order. and async
|
|
394
|
-
// in a loop is an anti-pattern.
|
|
395
|
-
while (this.mutations.length > 0) {
|
|
396
|
-
const m = this.mutations.shift();
|
|
397
|
-
if (m) {
|
|
398
|
-
await m();
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
const mutated_hash = this.hashDataObject();
|
|
275
|
+
this.last_pushed_ts = now.valueOf();
|
|
402
276
|
// If nothing actually changed, no need to push.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
277
|
+
const dirty_hash = this.hash(data);
|
|
278
|
+
// TODO add back in this check
|
|
279
|
+
// if (this.last_fetched_hash === dirty_hash) {
|
|
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;
|
|
406
298
|
}
|
|
407
|
-
|
|
299
|
+
// Send the update
|
|
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');
|
|
408
320
|
}
|
|
409
|
-
async forcePush() {
|
|
410
|
-
this.log.info('
|
|
321
|
+
async forcePush(data) {
|
|
322
|
+
this.log.info('... sending changes to carrier api...');
|
|
411
323
|
const builder = new xml2js.Builder();
|
|
412
|
-
const new_xml = builder.buildObject(
|
|
413
|
-
const
|
|
414
|
-
await this.infinity_client.axios.post(this.getPath(),
|
|
324
|
+
const new_xml = builder.buildObject(data);
|
|
325
|
+
const post_data = `data=${encodeURIComponent(new_xml)}`;
|
|
326
|
+
await this.infinity_client.axios.post(this.getPath(), post_data, {
|
|
415
327
|
headers: {
|
|
416
328
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
417
329
|
},
|
|
418
330
|
});
|
|
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.');
|
|
419
334
|
}
|
|
420
335
|
async setMode(mode) {
|
|
421
|
-
this.mutations.push(async () => {
|
|
422
|
-
this.mutateMode(mode);
|
|
423
|
-
});
|
|
424
|
-
// Schedule the push event, but don't wait for it to return.
|
|
425
|
-
this.push();
|
|
426
|
-
}
|
|
427
|
-
mutateMode(mode) {
|
|
428
336
|
this.log.debug('Setting mode to ' + mode);
|
|
429
|
-
|
|
337
|
+
const data = await (0, rxjs_1.firstValueFrom)(this.data$);
|
|
338
|
+
data.config.mode[0] = mode;
|
|
339
|
+
this.dirty_data$.next(data);
|
|
430
340
|
}
|
|
431
341
|
async setZoneActivityHold(zone, activity, hold_until) {
|
|
432
|
-
this.mutations.push(async () => {
|
|
433
|
-
await this.mutateZoneActivityHold(zone, activity, hold_until);
|
|
434
|
-
});
|
|
435
|
-
// Schedule the push event, but don't wait for it to return.
|
|
436
|
-
this.push();
|
|
437
|
-
}
|
|
438
|
-
async mutateZoneActivityHold(zone, activity, hold_until) {
|
|
439
342
|
this.log.debug(`Setting zone ${zone} activity to ${activity} until ${hold_until}`);
|
|
440
|
-
|
|
343
|
+
// Get data from zone object and make changes
|
|
344
|
+
const data = await (0, rxjs_1.firstValueFrom)(this.data$);
|
|
345
|
+
const zone_obj = (0, helpers_2.findZoneByID)(data.config.zones[0].zone, zone);
|
|
441
346
|
zone_obj['holdActivity'][0] = activity;
|
|
442
347
|
zone_obj['hold'][0] = activity ? constants_1.STATUS.ON : constants_1.STATUS.OFF;
|
|
443
348
|
zone_obj['otmr'][0] = activity ? hold_until || '' : '';
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const manual_activity_obj = await this.getZoneActivityConfig(zone, constants_1.ACTIVITY.MANUAL);
|
|
462
|
-
if (zone_obj['holdActivity'][0] !== constants_1.ACTIVITY.MANUAL) {
|
|
463
|
-
const prev_activity_obj = await this.getZoneActivityConfig(zone, await this.getZoneActivity(zone));
|
|
349
|
+
// Push changes
|
|
350
|
+
this.dirty_data$.next(data);
|
|
351
|
+
}
|
|
352
|
+
// This makes the manual activity match another named activity. This is useful
|
|
353
|
+
// before switching to the manual activity to make sure only the setpoint you
|
|
354
|
+
// intend to change is changed.
|
|
355
|
+
async setZoneActivityManualSync(zone, sync_from_activity_name) {
|
|
356
|
+
// Get data from zone / activity
|
|
357
|
+
const data = await (0, rxjs_1.firstValueFrom)(this.data$);
|
|
358
|
+
const zone_obj = (0, helpers_2.findZoneByID)(data.config.zones[0].zone, zone);
|
|
359
|
+
const manual_activity_obj = (0, helpers_2.getZoneActivityConfig)(data, zone, constants_1.ACTIVITY.MANUAL);
|
|
360
|
+
// Modify MANUAL activity to match current activity, but only if we have
|
|
361
|
+
// not already made the switch to manual.
|
|
362
|
+
if (sync_from_activity_name &&
|
|
363
|
+
sync_from_activity_name !== constants_1.ACTIVITY.MANUAL &&
|
|
364
|
+
zone_obj.holdActivity[0] !== constants_1.ACTIVITY.MANUAL) {
|
|
365
|
+
const prev_activity_obj = (0, helpers_2.getZoneActivityConfig)(data, zone, sync_from_activity_name);
|
|
464
366
|
manual_activity_obj['clsp'][0] = prev_activity_obj['clsp'][0];
|
|
465
367
|
manual_activity_obj['htsp'][0] = prev_activity_obj['htsp'][0];
|
|
466
368
|
manual_activity_obj['fan'][0] = prev_activity_obj['fan'][0];
|
|
369
|
+
// Push changes
|
|
370
|
+
this.dirty_data$.next(data);
|
|
467
371
|
}
|
|
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);
|
|
468
378
|
// Set setpoints on manual activity
|
|
469
|
-
[htsp, clsp] = (0, helpers_1.processSetpointDeadband)(htsp || parseFloat(manual_activity_obj['htsp'][0]), clsp || parseFloat(manual_activity_obj['clsp'][0]),
|
|
379
|
+
[htsp, clsp] = (0, helpers_1.processSetpointDeadband)(htsp || parseFloat(manual_activity_obj['htsp'][0]), clsp || parseFloat(manual_activity_obj['clsp'][0]), data.config.cfgem[0],
|
|
380
|
+
// TODO: rethink setpoint deadband
|
|
470
381
|
// when setpoints are too close, make clsp sticky when no change made to htsp
|
|
471
382
|
htsp === null);
|
|
472
383
|
manual_activity_obj['htsp'][0] = htsp.toFixed(1);
|
|
473
384
|
manual_activity_obj['clsp'][0] = clsp.toFixed(1);
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
385
|
+
// Push changes
|
|
386
|
+
this.dirty_data$.next(data);
|
|
387
|
+
}
|
|
388
|
+
async setZoneActivityManualFan(zone, fan) {
|
|
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);
|
|
478
396
|
}
|
|
479
397
|
}
|
|
480
398
|
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;
|
|
481
464
|
class SystemModel {
|
|
482
465
|
constructor(infinity_client, serialNumber) {
|
|
483
466
|
this.infinity_client = infinity_client;
|
|
@@ -488,6 +471,19 @@ class SystemModel {
|
|
|
488
471
|
this.config = new SystemConfigModel(infinity_client, serialNumber, api_logger);
|
|
489
472
|
this.profile = new SystemProfileModel(infinity_client, serialNumber, api_logger);
|
|
490
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)());
|
|
486
|
+
}
|
|
491
487
|
}
|
|
492
488
|
exports.SystemModel = SystemModel;
|
|
493
489
|
//# sourceMappingURL=models.js.map
|