hoffmation-base 3.5.0 → 3.6.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.
@@ -149,12 +149,22 @@ class HeatGroup extends base_group_1.BaseGroup {
149
149
  const devs = this.getOwnAcDevices();
150
150
  this.log(enums_1.LogLevel.Debug, `set ${devs.length} Ac's to new State: ${newDesiredState}`);
151
151
  for (const dev of devs) {
152
- dev.setAcState(new command_1.AcSetStateCommand(source,
152
+ const command = new command_1.AcSetStateCommand(source,
153
153
  // Undefined mode means "just switch it on"; the device resolves which mode that is.
154
- newDesiredState ? undefined : enums_1.AcMode.Off, undefined, 'HeatGroup setAc',
155
- // The block travels inside the command so setAcState applies it centrally. As
156
- // before, only switching off pins the automatic.
157
- newDesiredState ? undefined : new command_1.BlockAutomaticCommand(source, 60 * 60 * 1000)));
154
+ newDesiredState ? undefined : enums_1.AcMode.Off, undefined, 'HeatGroup setAc');
155
+ // As before, only switching off pins the automatic - and the block travels inside the command
156
+ // so setAcState applies it centrally.
157
+ //
158
+ // The duration now comes from the device's own settings, built the way every other device
159
+ // builds its block (see ShutterUtils.setLevel). It used to be a hard-coded hour here, which
160
+ // is exactly what the configured default resolves to - so nothing changes today; what changes
161
+ // is that a single unit can be given a shorter block without touching group code. Building it
162
+ // from the command rather than from `source` also puts the block into the reason chain, so the
163
+ // log says which switch-off set it.
164
+ if (!newDesiredState) {
165
+ command.disableAutomaticCommand = dev.settings.buildBlockAutomaticCommand(command);
166
+ }
167
+ dev.setAcState(command);
158
168
  }
159
169
  }
160
170
  deleteAutomaticPoint(name) {
@@ -62,16 +62,21 @@ class ShutterUtils {
62
62
  }
63
63
  device.logCommand(c);
64
64
  if (device.window !== undefined) {
65
+ // Only a manual command deserves an alert, because only then is someone waiting for a shutter
66
+ // that will not move. The automatic run meets an open window every evening, and an Alert is
67
+ // what reaches Telegram - a nightly push for an expected skip. Info keeps the decision
68
+ // reconstructable without waking anybody.
69
+ const warningLevel = c.isManual ? enums_1.LogLevel.Alert : enums_1.LogLevel.Info;
65
70
  if (device.window.griffeInPosition(enums_1.WindowPosition.open) > 0 && pPosition < 100) {
66
71
  if (!c.skipOpenWarning) {
67
- device.log(enums_1.LogLevel.Alert, 'Not closing the shutter, as the window is open!');
72
+ device.log(warningLevel, 'Not closing the shutter, as the window is open!');
68
73
  }
69
74
  return;
70
75
  }
71
76
  if (device.window.griffeInPosition(enums_1.WindowPosition.tilted) > 0 && pPosition < 50) {
72
77
  pPosition = 50;
73
78
  if (!c.skipOpenWarning) {
74
- device.log(enums_1.LogLevel.Alert, 'Not closing the shutter, as the window is half open!');
79
+ device.log(warningLevel, 'Not closing the shutter, as the window is half open!');
75
80
  }
76
81
  }
77
82
  }
@@ -11,6 +11,10 @@ export interface iBlockAutomaticHandler {
11
11
  *
12
12
  */
13
13
  readonly automaticBlockActive: boolean;
14
+ /**
15
+ * Whether the active block was set by a person (Manual, API or Force) rather than by a rule.
16
+ */
17
+ readonly automaticBlockedByUser: boolean;
14
18
  /**
15
19
  *
16
20
  */
@@ -93,6 +93,24 @@ export declare abstract class AcDevice extends RoomBaseDevice implements iExcess
93
93
  */
94
94
  protected automaticCheck(c: AcPerformAutomaticCheckCommand): void;
95
95
  private onRoomAnyMovement;
96
+ /**
97
+ * Keeps the unit off for as long as the room counts as occupied - renewed by every movement.
98
+ *
99
+ * Without a block, staying off depended on the five-minute interval check noticing that somebody
100
+ * is present, and coming back on depended on the last-leave callback arriving. The block replaces
101
+ * both with one timer: every movement pushes it out, and its expiry reverts to automatic by
102
+ * itself, so the room cools again shortly after it empties rather than at the next check.
103
+ *
104
+ * The duration is the room's own movementResetTimer - the same span that decides when the room
105
+ * counts as empty. Anything shorter would let the unit start up with somebody still sitting there;
106
+ * a separate constant would be a second answer to a question the room already answers.
107
+ *
108
+ * The collision strategy is deliberately left at its default (overrideIfGreater): a long press on
109
+ * the wall button sets a much longer block, and walking past the unit must not cut that short.
110
+ * @param c - The command the block chains from, so the log shows what triggered it
111
+ * @returns The block, or undefined when the device has no room to take the duration from
112
+ */
113
+ private buildMovementBlock;
96
114
  private onRoomLastLeave;
97
115
  /** @inheritDoc */
98
116
  toJSON(): Partial<AcDevice>;
@@ -100,7 +100,6 @@ class AcDevice extends devices_1.RoomBaseDevice {
100
100
  }
101
101
  utils_1.Utils.guardedFunction(() => {
102
102
  var _a, _b;
103
- // TODO: Maybe change to any Movement
104
103
  (_a = this.room.PraesenzGroup) === null || _a === void 0 ? void 0 : _a.addAnyMovementCallback(this.onRoomAnyMovement.bind(this));
105
104
  (_b = this.room.PraesenzGroup) === null || _b === void 0 ? void 0 : _b.addLastLeftCallback(this.onRoomLastLeave.bind(this));
106
105
  this._movementCallbackAdded = true;
@@ -341,7 +340,46 @@ class AcDevice extends devices_1.RoomBaseDevice {
341
340
  if (!this.settings.noCoolingOnMovement || !this.on || this.mode === enums_1.AcMode.Heating) {
342
341
  return;
343
342
  }
344
- this.setAcState(new command_1.AcSetStateCommand(action, enums_1.AcMode.Off, undefined, 'Something moved in the room and noCoolingOnMovement is set.'));
343
+ // Somebody asked for this unit to run and pinned it - a configured rule does not overrule that.
344
+ // This is the case the report was about: switching the AC on from the phone pins it for an hour,
345
+ // the next movement a minute later switched it off anyway, and the pin then kept the automatic
346
+ // from ever switching it back on. The unit stayed off for the full hour.
347
+ //
348
+ // Only a person's pin counts. The block this path sets itself is an automatic one, so a later
349
+ // movement can still renew it.
350
+ if (this.blockAutomationHandler.automaticBlockedByUser) {
351
+ this.log(enums_1.LogLevel.Debug, 'Movement detected, but the AC was pinned by hand - leaving it alone.');
352
+ return;
353
+ }
354
+ this.setAcState(new command_1.AcSetStateCommand(action, enums_1.AcMode.Off, undefined, 'Something moved in the room and noCoolingOnMovement is set.', this.buildMovementBlock(action)));
355
+ }
356
+ /**
357
+ * Keeps the unit off for as long as the room counts as occupied - renewed by every movement.
358
+ *
359
+ * Without a block, staying off depended on the five-minute interval check noticing that somebody
360
+ * is present, and coming back on depended on the last-leave callback arriving. The block replaces
361
+ * both with one timer: every movement pushes it out, and its expiry reverts to automatic by
362
+ * itself, so the room cools again shortly after it empties rather than at the next check.
363
+ *
364
+ * The duration is the room's own movementResetTimer - the same span that decides when the room
365
+ * counts as empty. Anything shorter would let the unit start up with somebody still sitting there;
366
+ * a separate constant would be a second answer to a question the room already answers.
367
+ *
368
+ * The collision strategy is deliberately left at its default (overrideIfGreater): a long press on
369
+ * the wall button sets a much longer block, and walking past the unit must not cut that short.
370
+ * @param c - The command the block chains from, so the log shows what triggered it
371
+ * @returns The block, or undefined when the device has no room to take the duration from
372
+ */
373
+ buildMovementBlock(c) {
374
+ var _a, _b;
375
+ const occupiedForSeconds = (_b = (_a = this.room) === null || _a === void 0 ? void 0 : _a.settings) === null || _b === void 0 ? void 0 : _b.movementResetTimer;
376
+ // No room means no answer to "how long does this count as occupied". Switching off without a
377
+ // block is what happened before this existed, and it is the honest fallback - a made-up
378
+ // duration would block the automatic for a span nothing chose.
379
+ if (occupiedForSeconds === undefined || occupiedForSeconds <= 0) {
380
+ return undefined;
381
+ }
382
+ return new command_1.BlockAutomaticCommand(c, occupiedForSeconds * 1000, 'noCoolingOnMovement', undefined, true);
345
383
  }
346
384
  onRoomLastLeave(action) {
347
385
  if (!this.settings.noCoolingOnMovement) {
@@ -9,11 +9,22 @@ export declare class BlockAutomaticHandler implements iBlockAutomaticHandler {
9
9
  private readonly _logger;
10
10
  private readonly _restoreAutomatic;
11
11
  private _automaticBlockedUntil;
12
+ private _blockSetByUser;
12
13
  private _restoreAutomaticStateTimeout;
13
14
  constructor(restoreAutomaticCb: (c: RestoreTargetAutomaticValueCommand) => void, _logger: (level: LogLevel, message: string, logDebugType?: LogDebugType) => void);
14
15
  get automaticBlockedUntil(): Date;
15
16
  private set automaticBlockedUntil(value);
16
17
  get automaticBlockActive(): boolean;
18
+ /**
19
+ * Whether the active block was set by a person rather than by another rule.
20
+ *
21
+ * The expiry date alone cannot answer that, and a rule that has to decide whether it may overrule
22
+ * the current state needs to: at the end of the day the user's choice outranks a configured
23
+ * automatism. Manual, API and Force all count - they are the same decision reaching the house
24
+ * through different doors.
25
+ * @returns True while a block set by a person is still running.
26
+ */
27
+ get automaticBlockedByUser(): boolean;
17
28
  disableAutomatic(c: BlockAutomaticCommand): void;
18
29
  disableAutomaticUntil(c: BlockAutomaticUntilCommand): void;
19
30
  liftAutomaticBlock(c: BlockAutomaticLiftBlockCommand): void;
@@ -16,6 +16,7 @@ class BlockAutomaticHandler {
16
16
  constructor(restoreAutomaticCb, _logger) {
17
17
  this._logger = _logger;
18
18
  this._automaticBlockedUntil = new Date(0);
19
+ this._blockSetByUser = false;
19
20
  this._restoreAutomaticStateTimeout = null;
20
21
  this._restoreAutomatic = restoreAutomaticCb;
21
22
  }
@@ -28,6 +29,18 @@ class BlockAutomaticHandler {
28
29
  get automaticBlockActive() {
29
30
  return this._automaticBlockedUntil > new Date();
30
31
  }
32
+ /**
33
+ * Whether the active block was set by a person rather than by another rule.
34
+ *
35
+ * The expiry date alone cannot answer that, and a rule that has to decide whether it may overrule
36
+ * the current state needs to: at the end of the day the user's choice outranks a configured
37
+ * automatism. Manual, API and Force all count - they are the same decision reaching the house
38
+ * through different doors.
39
+ * @returns True while a block set by a person is still running.
40
+ */
41
+ get automaticBlockedByUser() {
42
+ return this.automaticBlockActive && this._blockSetByUser;
43
+ }
31
44
  disableAutomatic(c) {
32
45
  this.disableAutomaticUntil(new command_1.BlockAutomaticUntilCommand(c, new Date(utils_1.Utils.nowMS() + c.durationMS), '', c.onCollideAction, c.revertToAutomaticAtBlockLift));
33
46
  }
@@ -41,6 +54,9 @@ class BlockAutomaticHandler {
41
54
  }
42
55
  this._logger(enums_1.LogLevel.Info, c.logMessage);
43
56
  this.automaticBlockedUntil = c.targetDate;
57
+ // Recorded here rather than derived later: the command is gone once this returns, and the level
58
+ // that pinned the device is exactly what a rule needs to know before overruling it.
59
+ this._blockSetByUser = c.isForceAction;
44
60
  if (c.revertToAutomaticAtBlockLift) {
45
61
  const revertCommand = new command_1.RestoreTargetAutomaticValueCommand(c, 'Restore to automatic state after block.');
46
62
  revertCommand.overrideCommandSource = enums_1.CommandSource.Automatic;
@@ -53,6 +69,7 @@ class BlockAutomaticHandler {
53
69
  liftAutomaticBlock(c) {
54
70
  this.removeRestoreTimeout();
55
71
  this.automaticBlockedUntil = new Date(0);
72
+ this._blockSetByUser = false;
56
73
  if (c.revertToAutomatic) {
57
74
  this._restoreAutomatic(new command_1.RestoreTargetAutomaticValueCommand(c));
58
75
  }