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.
Files changed (48) hide show
  1. package/dist/accessory_comfort_activity.d.ts.map +1 -1
  2. package/dist/accessory_comfort_activity.js +6 -9
  3. package/dist/accessory_comfort_activity.js.map +1 -1
  4. package/dist/accessory_oat.d.ts +1 -1
  5. package/dist/accessory_oat.d.ts.map +1 -1
  6. package/dist/accessory_oat.js +4 -3
  7. package/dist/accessory_oat.js.map +1 -1
  8. package/dist/accessory_thermostat.d.ts.map +1 -1
  9. package/dist/accessory_thermostat.js +6 -27
  10. package/dist/accessory_thermostat.js.map +1 -1
  11. package/dist/api/helpers.d.ts +5 -0
  12. package/dist/api/helpers.d.ts.map +1 -0
  13. package/dist/api/helpers.js +31 -0
  14. package/dist/api/helpers.js.map +1 -0
  15. package/dist/api/helpers_rxjs.d.ts +3 -0
  16. package/dist/api/helpers_rxjs.d.ts.map +1 -0
  17. package/dist/api/helpers_rxjs.js +16 -0
  18. package/dist/api/helpers_rxjs.js.map +1 -0
  19. package/dist/api/models.d.ts +78 -56
  20. package/dist/api/models.d.ts.map +1 -1
  21. package/dist/api/models.js +352 -356
  22. package/dist/api/models.js.map +1 -1
  23. package/dist/api/oauth.js +1 -1
  24. package/dist/api/oauth.js.map +1 -1
  25. package/dist/characteristics_ac.d.ts +1 -1
  26. package/dist/characteristics_ac.d.ts.map +1 -1
  27. package/dist/characteristics_ac.js +70 -42
  28. package/dist/characteristics_ac.js.map +1 -1
  29. package/dist/characteristics_base.d.ts +9 -6
  30. package/dist/characteristics_base.d.ts.map +1 -1
  31. package/dist/characteristics_base.js +56 -37
  32. package/dist/characteristics_base.js.map +1 -1
  33. package/dist/characteristics_fan.d.ts +6 -6
  34. package/dist/characteristics_fan.d.ts.map +1 -1
  35. package/dist/characteristics_fan.js +75 -63
  36. package/dist/characteristics_fan.js.map +1 -1
  37. package/dist/characteristics_filter.d.ts +1 -1
  38. package/dist/characteristics_filter.d.ts.map +1 -1
  39. package/dist/characteristics_filter.js +5 -8
  40. package/dist/characteristics_filter.js.map +1 -1
  41. package/dist/characteristics_humidity.d.ts +2 -5
  42. package/dist/characteristics_humidity.d.ts.map +1 -1
  43. package/dist/characteristics_humidity.js +9 -17
  44. package/dist/characteristics_humidity.js.map +1 -1
  45. package/dist/platform.d.ts.map +1 -1
  46. package/dist/platform.js +12 -6
  47. package/dist/platform.js.map +1 -1
  48. package/package.json +28 -23
@@ -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
- hashDataObject() {
52
- return (0, object_hash_1.default)(this.data_object, { excludeKeys: (key) => {
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
- async fetch() {
57
- // If push is ongoing, skip this update fetch. The push will do a fetch.
58
- try {
59
- await (0, async_mutex_1.tryAcquire)(this.write_lock).runExclusive(async () => {
60
- await this.forceFetch();
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
- this.data_object = await xml2js.parseStringPromise(response.data);
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
- this.last_updated = 0; // TODO use this
116
- this.HASH_IGNORE_KEYS = new Set(['timestamp', 'localTime']);
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(this.data_object)[0];
121
- const ts = this.data_object[top_level_key].timestamp[0];
122
- this.last_updated = Date.parse(ts);
123
- this.log.debug(`TIMESTAMP ${this.getPath()} reports ${ts} (${this.last_updated})`);
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
- async getUnits() {
157
- await this.fetch();
158
- return this.data_object.status.cfgem[0];
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
- async getMode() {
169
- await this.fetch();
170
- const raw_mode = this.data_object.status.mode[0];
171
- switch (raw_mode) {
172
- case 'gasheat':
173
- case 'electric':
174
- case 'hpheat':
175
- return constants_1.SYSTEM_MODE.HEAT;
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
- default:
200
- return raw_mode;
201
- }
202
- }
203
- async getZoneFan(zone) {
204
- const zone_obj = await this.getZone(zone);
205
- if (zone_obj.damperposition[0] === '0') {
206
- return constants_1.FAN_MODE.OFF;
207
- }
208
- else {
209
- return zone_obj.fan[0];
210
- }
211
- }
212
- async getZoneOpen(zone) {
213
- return (await this.getZone(zone)).damperposition[0] !== '0';
214
- }
215
- async getZoneTemp(zone) {
216
- return Number((await this.getZone(zone)).rt[0]);
217
- }
218
- async getZoneHumidity(zone) {
219
- return Number((await this.getZone(zone)).rh[0]);
220
- }
221
- async getZoneActivity(zone) {
222
- return (await this.getZone(zone)).currentActivity[0];
223
- }
224
- async getZoneCoolSetpoint(zone) {
225
- return Number((await this.getZone(zone)).clsp[0]);
226
- }
227
- async getZoneHeatSetpoint(zone) {
228
- return Number((await this.getZone(zone)).htsp[0]);
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.SystemStatusModel = SystemStatusModel;
229
+ exports.SystemStatusZoneModel = SystemStatusZoneModel;
232
230
  class SystemConfigModel extends BaseSystemModel {
233
- constructor() {
234
- super(...arguments);
235
- /* Write APIs */
236
- this.mutations = [];
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
- async getUnits() {
242
- await this.fetch();
243
- return this.data_object.config.cfgem[0];
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
- async getZone(zone) {
255
- await this.fetch();
256
- return this.data_object.config.zones[0].zone.find((z) => z['$'].id === zone.toString());
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
- const program_obj = (await this.getZone(zone)).program[0];
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
- if (old_hash === mutated_hash) {
404
- this.log.warn('Config doesn\'t appear to have changed. No changes sent.');
405
- return null;
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
- return mutated_hash;
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('Pushing changes to carrier api...');
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(this.data_object);
413
- const data = `data=${encodeURIComponent(new_xml)}`;
414
- await this.infinity_client.axios.post(this.getPath(), data, {
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
- this.data_object.config.mode[0] = mode;
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
- const zone_obj = await this.getZone(zone);
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
- async setZoneActivityManualHold(zone, clsp, htsp, hold_until, fan = null) {
446
- // Modify MANUAL activity to the requested setpoints
447
- this.mutations.push(async () => {
448
- await this.mutateZoneActivityManualHold(zone, clsp, htsp, fan);
449
- });
450
- // Set hold to MANUAL activity
451
- this.mutations.push(async () => {
452
- await this.mutateZoneActivityHold(zone, constants_1.ACTIVITY.MANUAL, hold_until);
453
- });
454
- // Schedule the push event, but don't wait for it to return.
455
- this.push();
456
- }
457
- async mutateZoneActivityManualHold(zone, clsp, htsp, fan = null) {
458
- this.log.debug(`Setting zone ${zone} to`, clsp ? `clsp=${clsp}` : '', htsp ? `htsp=${htsp}` : '', fan ? `fan=${fan}` : '', '.');
459
- const zone_obj = await this.getZone(zone);
460
- // When moving to manual activity, default to prev activity settings.
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]), await this.getUnits(),
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
- // Set fan on manual activity
475
- if (fan) {
476
- manual_activity_obj['fan'][0] = fan;
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