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.
Files changed (64) hide show
  1. package/dist/accessory_base.js.map +1 -1
  2. package/dist/accessory_comfort_activity.d.ts.map +1 -1
  3. package/dist/accessory_comfort_activity.js +12 -8
  4. package/dist/accessory_comfort_activity.js.map +1 -1
  5. package/dist/accessory_oat.d.ts +1 -1
  6. package/dist/accessory_oat.d.ts.map +1 -1
  7. package/dist/accessory_oat.js +3 -4
  8. package/dist/accessory_oat.js.map +1 -1
  9. package/dist/accessory_thermostat.d.ts.map +1 -1
  10. package/dist/accessory_thermostat.js +27 -6
  11. package/dist/accessory_thermostat.js.map +1 -1
  12. package/dist/api/constants.d.ts +6 -0
  13. package/dist/api/constants.d.ts.map +1 -1
  14. package/dist/api/constants.js +7 -1
  15. package/dist/api/constants.js.map +1 -1
  16. package/dist/api/models.d.ts +71 -79
  17. package/dist/api/models.d.ts.map +1 -1
  18. package/dist/api/models.js +438 -363
  19. package/dist/api/models.js.map +1 -1
  20. package/dist/api/oauth.d.ts +2 -2
  21. package/dist/api/oauth.d.ts.map +1 -1
  22. package/dist/api/oauth.js +0 -3
  23. package/dist/api/oauth.js.map +1 -1
  24. package/dist/api/rest_client.js +1 -1
  25. package/dist/api/rest_client.js.map +1 -1
  26. package/dist/characteristics_ac.d.ts +1 -1
  27. package/dist/characteristics_ac.d.ts.map +1 -1
  28. package/dist/characteristics_ac.js +42 -70
  29. package/dist/characteristics_ac.js.map +1 -1
  30. package/dist/characteristics_base.d.ts +6 -9
  31. package/dist/characteristics_base.d.ts.map +1 -1
  32. package/dist/characteristics_base.js +48 -57
  33. package/dist/characteristics_base.js.map +1 -1
  34. package/dist/characteristics_fan.d.ts +6 -6
  35. package/dist/characteristics_fan.d.ts.map +1 -1
  36. package/dist/characteristics_fan.js +63 -75
  37. package/dist/characteristics_fan.js.map +1 -1
  38. package/dist/characteristics_filter.d.ts +1 -1
  39. package/dist/characteristics_filter.d.ts.map +1 -1
  40. package/dist/characteristics_filter.js +8 -5
  41. package/dist/characteristics_filter.js.map +1 -1
  42. package/dist/characteristics_humidity.d.ts +5 -2
  43. package/dist/characteristics_humidity.d.ts.map +1 -1
  44. package/dist/characteristics_humidity.js +17 -9
  45. package/dist/characteristics_humidity.js.map +1 -1
  46. package/dist/helper_logging.d.ts +1 -0
  47. package/dist/helper_logging.d.ts.map +1 -1
  48. package/dist/helper_logging.js +3 -0
  49. package/dist/helper_logging.js.map +1 -1
  50. package/dist/helpers.js +7 -8
  51. package/dist/helpers.js.map +1 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/platform.d.ts.map +1 -1
  54. package/dist/platform.js +6 -12
  55. package/dist/platform.js.map +1 -1
  56. package/package.json +28 -28
  57. package/dist/api/helpers.d.ts +0 -5
  58. package/dist/api/helpers.d.ts.map +0 -1
  59. package/dist/api/helpers.js +0 -31
  60. package/dist/api/helpers.js.map +0 -1
  61. package/dist/api/helpers_rxjs.d.ts +0 -3
  62. package/dist/api/helpers_rxjs.d.ts.map +0 -1
  63. package/dist/api/helpers_rxjs.js +0 -16
  64. package/dist/api/helpers_rxjs.js.map +0 -1
@@ -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 __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
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.SystemConfigZoneModel = exports.SystemConfigModel = exports.SystemStatusZoneModel = exports.SystemStatusModel = exports.SystemProfileModel = exports.LocationsModel = void 0;
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 helpers_2 = require("./helpers");
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
- // 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) => {
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
- 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();
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
- return await xml2js.parseStringPromise(response.data);
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
- // 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']);
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 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;
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
- 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);
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 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') {
192
- return constants_1.SYSTEM_MODE.OFF;
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;
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
- else {
215
- return zone.fan[0];
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
- }), (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)());
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.SystemStatusZoneModel = SystemStatusZoneModel;
230
- class SystemConfigModel extends BaseSystemModel {
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...'));
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
- getPath() {
264
- return `/systems/${this.serialNumber}/config`;
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
- 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);
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
- /* 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.
274
- const now = new Date();
275
- this.last_pushed_ts = now.valueOf();
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
- 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;
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
- // 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');
470
+ return [mutated_hash, mutated_data_object];
320
471
  }
321
- async forcePush(data) {
322
- this.log.info('... sending changes to carrier api...');
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(data);
325
- const post_data = `data=${encodeURIComponent(new_xml)}`;
326
- await this.infinity_client.axios.post(this.getPath(), post_data, {
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 data = await (0, rxjs_1.firstValueFrom)(this.data$);
338
- data.config.mode[0] = mode;
339
- this.dirty_data$.next(data);
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
- // 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);
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
- // 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);
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]), data.config.cfgem[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
- // 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);
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;