machine_access_control 0.2.3__tar.gz → 0.4.0__tar.gz

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 (17) hide show
  1. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/PKG-INFO +8 -6
  2. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/pyproject.toml +7 -7
  3. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/models/machine.py +159 -38
  4. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/slack_handler.py +24 -24
  5. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/views/prometheus.py +19 -36
  6. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/LICENSE +0 -0
  7. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/README.rst +0 -0
  8. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/__init__.py +0 -0
  9. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/cli_utils.py +0 -0
  10. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/models/__init__.py +0 -0
  11. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/models/users.py +0 -0
  12. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/neongetter.py +0 -0
  13. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/py.typed +0 -0
  14. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/utils.py +0 -0
  15. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/views/__init__.py +0 -0
  16. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/views/api.py +0 -0
  17. {machine_access_control-0.2.3 → machine_access_control-0.4.0}/src/dm_mac/views/machine.py +0 -0
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: machine_access_control
3
- Version: 0.2.3
3
+ Version: 0.4.0
4
4
  Summary: Decatur Makers Machine Access Control package
5
- Home-page: https://github.com/jantman/machine_access_control
6
5
  License: MIT
6
+ License-File: LICENSE
7
7
  Author: Jason Antman
8
8
  Author-email: jason@jasonantman.com
9
9
  Requires-Python: >=3.12,<4.0
@@ -12,18 +12,20 @@ Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: aiohttp (>=3.11.4,<4.0.0)
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: aiohttp (>=3.12.14,<4.0.0)
16
17
  Requires-Dist: asyncio (>=3.4.3,<4.0.0)
17
- Requires-Dist: filelock (>=3.15.4,<4.0.0)
18
+ Requires-Dist: filelock (>=3.16.1,<4.0.0)
18
19
  Requires-Dist: humanize (>=4.11.0,<5.0.0)
19
20
  Requires-Dist: jsonschema (>=4.23.0,<5.0.0)
20
21
  Requires-Dist: prometheus-client (>=0.20.0,<0.21.0)
21
22
  Requires-Dist: quart (>=0.19.8,<0.20.0)
22
- Requires-Dist: requests (>=2.32.3,<3.0.0)
23
+ Requires-Dist: requests (>=2.32.4,<3.0.0)
23
24
  Requires-Dist: slack-bolt (>=1.21.3,<2.0.0)
24
25
  Requires-Dist: slack-sdk (>=3.33.5,<4.0.0)
25
26
  Project-URL: Changelog, https://github.com/jantman/machine_access_control/releases
26
27
  Project-URL: Documentation, https://github.com/jantman/machine_access_control
28
+ Project-URL: Homepage, https://github.com/jantman/machine_access_control
27
29
  Project-URL: Repository, https://github.com/jantman/machine_access_control
28
30
  Description-Content-Type: text/x-rst
29
31
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "machine_access_control"
3
- version = "0.2.3"
3
+ version = "0.4.0"
4
4
  description = "Decatur Makers Machine Access Control package"
5
5
  authors = ["Jason Antman <jason@jasonantman.com>"]
6
6
  license = "MIT"
@@ -25,13 +25,13 @@ Changelog = "https://github.com/jantman/machine_access_control/releases"
25
25
  [tool.poetry.dependencies]
26
26
  python = "^3.12"
27
27
  jsonschema = "^4.23.0"
28
- requests = "^2.32.3"
29
- filelock = "^3.15.4"
28
+ requests = "^2.32.4"
29
+ filelock = "^3.16.1"
30
30
  prometheus-client = "^0.20.0"
31
31
  quart = "^0.19.8"
32
32
  asyncio = "^3.4.3"
33
33
  slack-bolt = "^1.21.3"
34
- aiohttp = "^3.11.4"
34
+ aiohttp = "^3.12.14"
35
35
  slack-sdk = "^3.33.5"
36
36
  humanize = "^4.11.0"
37
37
 
@@ -40,7 +40,7 @@ Pygments = ">=2.10.0"
40
40
  black = ">=21.10b0"
41
41
  coverage = {extras = ["toml"], version = ">=6.2"}
42
42
  darglint = ">=1.8.1"
43
- flake8 = ">=4.0.1"
43
+ flake8 = ">=7.1.1"
44
44
  flake8-bandit = ">=2.1.2"
45
45
  flake8-bugbear = ">=21.9.2"
46
46
  flake8-docstrings = ">=1.6.0"
@@ -49,7 +49,7 @@ furo = ">=2021.11.12"
49
49
  isort = ">=5.10.1"
50
50
  mypy = ">=0.930"
51
51
  nox = ">=2024.4.15"
52
- nox-poetry = ">= 1.0.3"
52
+ nox-poetry = ">= 1.2.0"
53
53
  pep8-naming = ">=0.12.1"
54
54
  pre-commit = ">=2.16.0"
55
55
  pre-commit-hooks = ">=4.1.0"
@@ -65,7 +65,7 @@ responses = "^0.25.3"
65
65
  types-requests = "^2.32.0.20240712"
66
66
  faker = "^26.3.0"
67
67
  pytest-html = "^4.1.1"
68
- poetry-plugin-export = "^1.8.0"
68
+ poetry-plugin-export = "^1.9.0"
69
69
  freezegun = "^1.5.1"
70
70
 
71
71
  [tool.isort]
@@ -53,6 +53,18 @@ CONFIG_SCHEMA: Dict[str, Any] = {
53
53
  "but log and display a warning if the "
54
54
  "operator is not authorized.",
55
55
  },
56
+ "always_enabled": {
57
+ "type": "boolean",
58
+ "description": "If set, machine is always enabled and "
59
+ "does not require RFID authentication. "
60
+ "Displays 'Always On' and relay is always "
61
+ "on unless Oopsed or Locked.",
62
+ },
63
+ "alias": {
64
+ "type": "string",
65
+ "description": "Optional human-friendly alias for the machine. "
66
+ "Used in Slack messages and logs instead of the machine name.",
67
+ },
56
68
  },
57
69
  "additionalProperties": False,
58
70
  "description": "Unique machine name, alphanumeric _ and - only.",
@@ -69,6 +81,8 @@ class Machine:
69
81
  name: str,
70
82
  authorizations_or: List[str],
71
83
  unauthorized_warn_only: bool = False,
84
+ always_enabled: bool = False,
85
+ alias: Optional[str] = None,
72
86
  ):
73
87
  """Initialize a new MachineState instance."""
74
88
  #: The name of the machine
@@ -78,6 +92,10 @@ class Machine:
78
92
  #: Whether to allow anyone to operate machine regardless of
79
93
  #: authorization, just logging/displaying a warning if unauthorized
80
94
  self.unauthorized_warn_only: bool = unauthorized_warn_only
95
+ #: Whether machine is always enabled without RFID authentication
96
+ self.always_enabled: bool = always_enabled
97
+ #: Optional human-friendly alias for the machine
98
+ self.alias: Optional[str] = alias
81
99
  #: state of the machine
82
100
  self.state: "MachineState" = MachineState(self)
83
101
 
@@ -135,6 +153,11 @@ class Machine:
135
153
  return
136
154
  await slack.log_unoops(self, source)
137
155
 
156
+ @property
157
+ def display_name(self) -> str:
158
+ """Return the display name for this machine (alias if present, else name)."""
159
+ return self.alias if self.alias else self.name
160
+
138
161
  @property
139
162
  def as_dict(self) -> Dict[str, Any]:
140
163
  """Return a dict representation of this machine."""
@@ -142,6 +165,8 @@ class Machine:
142
165
  "name": self.name,
143
166
  "authorizations_or": self.authorizations_or,
144
167
  "unauthorized_warn_only": self.unauthorized_warn_only,
168
+ "always_enabled": self.always_enabled,
169
+ "alias": self.alias,
145
170
  }
146
171
 
147
172
 
@@ -152,6 +177,7 @@ class MachinesConfig:
152
177
  """Initialize MachinesConfig."""
153
178
  logger.debug("Initializing MachinesConfig")
154
179
  self.machines_by_name: Dict[str, Machine] = {}
180
+ self.machines_by_alias: Dict[str, Machine] = {}
155
181
  self.machines: List[Machine] = []
156
182
  mdict: Dict[str, Any]
157
183
  mname: str
@@ -159,8 +185,16 @@ class MachinesConfig:
159
185
  mach: Machine = Machine(name=mname, **mdict)
160
186
  self.machines.append(mach)
161
187
  self.machines_by_name[mach.name] = mach
188
+ if mach.alias:
189
+ self.machines_by_alias[mach.alias] = mach
162
190
  self.load_time: float = time()
163
191
 
192
+ def get_machine(self, name_or_alias: str) -> Optional[Machine]:
193
+ """Get a machine by name or alias."""
194
+ return self.machines_by_name.get(name_or_alias) or self.machines_by_alias.get(
195
+ name_or_alias
196
+ )
197
+
164
198
  def _load_and_validate_config(self) -> Dict[str, Dict[str, Any]]:
165
199
  """Load and validate the config file."""
166
200
  config: Dict[str, Dict[str, Any]] = cast(
@@ -187,6 +221,8 @@ class MachineState:
187
221
 
188
222
  LOCKOUT_DISPLAY_TEXT: str = "Down for\nmaintenance"
189
223
 
224
+ ALWAYS_ON_DISPLAY_TEXT: str = "Always On"
225
+
190
226
  STATUS_LED_BRIGHTNESS: float = 0.5
191
227
 
192
228
  def __init__(self, machine: Machine, load_state: bool = True):
@@ -290,29 +326,37 @@ class MachineState:
290
326
  async def _handle_reboot(self) -> None:
291
327
  """Handle when the ESP32 (MCU) has rebooted since last checkin.
292
328
 
293
- This logs out the current user if logged in and turns off the relay if
294
- turned on.
329
+ This logs out the current user if logged in and resets the machine state.
330
+ For always-enabled machines, restores the always-on state.
295
331
  """
296
332
  logging.getLogger("AUTH").warning(
297
- "Machine %s rebooted; resetting relay and RFID state", self.machine.name
333
+ "Machine %s rebooted; resetting relay and RFID state",
334
+ self.machine.display_name,
298
335
  )
299
336
  # locking handled in update()
300
- self.relay_desired_state = False
301
337
  self.current_user = None
302
- self.display_text = self.DEFAULT_DISPLAY_TEXT
303
- self.status_led_rgb = (0.0, 0.0, 0.0)
304
- self.status_led_brightness = 0.0
338
+ # Restore always-enabled state if applicable
339
+ if self.machine.always_enabled:
340
+ self.relay_desired_state = True
341
+ self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
342
+ self.status_led_rgb = (0.0, 1.0, 0.0)
343
+ self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
344
+ else:
345
+ self.relay_desired_state = False
346
+ self.display_text = self.DEFAULT_DISPLAY_TEXT
347
+ self.status_led_rgb = (0.0, 0.0, 0.0)
348
+ self.status_led_brightness = 0.0
305
349
  # log to Slack, if enabled
306
350
  slack: Optional["SlackHandler"] = current_app.config.get("SLACK_HANDLER")
307
351
  if not slack:
308
352
  # Slack integration is not enabled
309
353
  return
310
- await slack.admin_log(f"Machine {self.machine.name} has rebooted.")
354
+ await slack.admin_log(f"Machine {self.machine.display_name} has rebooted.")
311
355
 
312
356
  def lockout(self) -> None:
313
357
  """Lock-out the machine."""
314
358
  logging.getLogger("OOPS").warning(
315
- "Machine %s was locked out for maintenance.", self.machine.name
359
+ "Machine %s was locked out for maintenance.", self.machine.display_name
316
360
  )
317
361
  with self._lock:
318
362
  self.is_locked_out = True
@@ -325,19 +369,29 @@ class MachineState:
325
369
  def unlock(self) -> None:
326
370
  """Un-lock-out the machine."""
327
371
  logging.getLogger("OOPS").warning(
328
- "Machine %s was removed from maintenance lock-out.", self.machine.name
372
+ "Machine %s was removed from maintenance lock-out.",
373
+ self.machine.display_name,
329
374
  )
330
375
  with self._lock:
331
376
  self.is_locked_out = False
332
- self.relay_desired_state = False
333
377
  self.current_user = None
334
- self.display_text = self.DEFAULT_DISPLAY_TEXT
335
- self.status_led_rgb = (0.0, 0.0, 0.0)
336
- self.status_led_brightness = 0.0
378
+ # Restore always-enabled state if applicable
379
+ if self.machine.always_enabled:
380
+ self.relay_desired_state = True
381
+ self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
382
+ self.status_led_rgb = (0.0, 1.0, 0.0)
383
+ self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
384
+ else:
385
+ self.relay_desired_state = False
386
+ self.display_text = self.DEFAULT_DISPLAY_TEXT
387
+ self.status_led_rgb = (0.0, 0.0, 0.0)
388
+ self.status_led_brightness = 0.0
337
389
 
338
390
  def oops(self, do_locking: bool = True) -> None:
339
391
  """Oops the machine."""
340
- logging.getLogger("OOPS").warning("Machine %s was Oopsed.", self.machine.name)
392
+ logging.getLogger("OOPS").warning(
393
+ "Machine %s was Oopsed.", self.machine.display_name
394
+ )
341
395
  locker = self._lock if do_locking else nullcontext()
342
396
  with locker:
343
397
  self.is_oopsed = True
@@ -350,16 +404,23 @@ class MachineState:
350
404
  def unoops(self, do_locking: bool = True) -> None:
351
405
  """Un-oops the machine."""
352
406
  logging.getLogger("OOPS").warning(
353
- "Machine %s was un-Oopsed.", self.machine.name
407
+ "Machine %s was un-Oopsed.", self.machine.display_name
354
408
  )
355
409
  locker = self._lock if do_locking else nullcontext()
356
410
  with locker:
357
411
  self.is_oopsed = False
358
- self.relay_desired_state = False
359
412
  self.current_user = None
360
- self.display_text = self.DEFAULT_DISPLAY_TEXT
361
- self.status_led_rgb = (0.0, 0.0, 0.0)
362
- self.status_led_brightness = 0
413
+ # Restore always-enabled state if applicable
414
+ if self.machine.always_enabled:
415
+ self.relay_desired_state = True
416
+ self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
417
+ self.status_led_rgb = (0.0, 1.0, 0.0)
418
+ self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
419
+ else:
420
+ self.relay_desired_state = False
421
+ self.display_text = self.DEFAULT_DISPLAY_TEXT
422
+ self.status_led_rgb = (0.0, 0.0, 0.0)
423
+ self.status_led_brightness = 0
363
424
 
364
425
  async def update(
365
426
  self,
@@ -398,7 +459,21 @@ class MachineState:
398
459
  if oops:
399
460
  await self._handle_oops(users)
400
461
  self.last_update = time()
401
- if rfid_value != self.rfid_value:
462
+ # Handle always-enabled machines - track RFID but maintain always-on state
463
+ if (
464
+ self.machine.always_enabled
465
+ and not self.is_oopsed
466
+ and not self.is_locked_out
467
+ ):
468
+ self.relay_desired_state = True
469
+ self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
470
+ self.status_led_rgb = (0.0, 1.0, 0.0)
471
+ self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
472
+ self.last_update = time()
473
+ # Track RFID changes for logging/auditing purposes
474
+ if rfid_value != self.rfid_value:
475
+ await self._handle_rfid_tracking_always_enabled(users, rfid_value)
476
+ elif rfid_value != self.rfid_value:
402
477
  if rfid_value is None:
403
478
  await self._handle_rfid_remove()
404
479
  else:
@@ -417,7 +492,7 @@ class MachineState:
417
492
  ustr = f" Current user is: {user.full_name}."
418
493
  uname = user.full_name
419
494
  logging.getLogger("OOPS").warning(
420
- "Machine %s was Oopsed.%s", self.machine.name, ustr
495
+ "Machine %s was Oopsed.%s", self.machine.display_name, ustr
421
496
  )
422
497
  # locking handled in update()
423
498
  self.oops(do_locking=False)
@@ -437,12 +512,12 @@ class MachineState:
437
512
  """Handle RFID card removed."""
438
513
  logging.getLogger("AUTH").info(
439
514
  "RFID logout on %s by %s; session duration %d seconds",
440
- self.machine.name,
515
+ self.machine.display_name,
441
516
  self.current_user.full_name if self.current_user else self.rfid_value,
442
517
  time() - cast(float, self.rfid_present_since),
443
518
  )
444
519
  log_str: str = (
445
- f"RFID logout on {self.machine.name} by "
520
+ f"RFID logout on {self.machine.display_name} by "
446
521
  + (self.current_user.full_name if self.current_user else "unknown")
447
522
  + "; session duration "
448
523
  + naturaldelta(time() - cast(float, self.rfid_present_since))
@@ -473,13 +548,13 @@ class MachineState:
473
548
  if not user:
474
549
  logging.getLogger("AUTH").warning(
475
550
  "RFID login attempt on %s by unknown fob %s",
476
- self.machine.name,
551
+ self.machine.display_name,
477
552
  rfid_value,
478
553
  )
479
554
  if self.is_oopsed or self.is_locked_out:
480
555
  if slack:
481
556
  await slack.admin_log(
482
- f"RFID login attempt on {self.machine.name} "
557
+ f"RFID login attempt on {self.machine.display_name} "
483
558
  "by unknown fob when oopsed or locked out."
484
559
  )
485
560
  return
@@ -488,7 +563,7 @@ class MachineState:
488
563
  self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
489
564
  if slack:
490
565
  await slack.admin_log(
491
- f"RFID login attempt on {self.machine.name} by unknown fob"
566
+ f"RFID login attempt on {self.machine.display_name} by unknown fob"
492
567
  )
493
568
  return
494
569
  # ok, we have a known user
@@ -496,26 +571,26 @@ class MachineState:
496
571
  if self.is_oopsed:
497
572
  logging.getLogger("AUTH").warning(
498
573
  "RFID login attempt while oopsed on %s by %s",
499
- self.machine.name,
574
+ self.machine.display_name,
500
575
  logname,
501
576
  )
502
577
  # don't change anything
503
578
  if slack:
504
579
  await slack.admin_log(
505
- f"RFID login attempt on {self.machine.name} by "
580
+ f"RFID login attempt on {self.machine.display_name} by "
506
581
  f"{user.full_name} when oopsed."
507
582
  )
508
583
  return
509
584
  if self.is_locked_out:
510
585
  logging.getLogger("AUTH").warning(
511
586
  "RFID login attempt while locked out on %s by %s",
512
- self.machine.name,
587
+ self.machine.display_name,
513
588
  logname,
514
589
  )
515
590
  # don't change anything
516
591
  if slack:
517
592
  await slack.admin_log(
518
- f"RFID login attempt on {self.machine.name} by "
593
+ f"RFID login attempt on {self.machine.display_name} by "
519
594
  f"{user.full_name} when machine locked-out."
520
595
  )
521
596
  return
@@ -524,7 +599,7 @@ class MachineState:
524
599
  "User %s (%s) authorized for %s; session start",
525
600
  user.full_name,
526
601
  user.account_id,
527
- self.machine.name,
602
+ self.machine.display_name,
528
603
  )
529
604
  self.current_user = user
530
605
  self.relay_desired_state = True
@@ -533,7 +608,7 @@ class MachineState:
533
608
  self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
534
609
  if slack:
535
610
  await slack.admin_log(
536
- f"RFID login on {self.machine.name} by authorized user "
611
+ f"RFID login on {self.machine.display_name} by authorized user "
537
612
  f"{user.full_name}"
538
613
  )
539
614
  else:
@@ -541,7 +616,7 @@ class MachineState:
541
616
  "User %s (%s) UNAUTHORIZED for %s",
542
617
  user.full_name,
543
618
  user.account_id,
544
- self.machine.name,
619
+ self.machine.display_name,
545
620
  )
546
621
  self.relay_desired_state = False
547
622
  self.display_text = "Unauthorized"
@@ -549,10 +624,56 @@ class MachineState:
549
624
  self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
550
625
  if slack:
551
626
  await slack.admin_log(
552
- f"rejected RFID login on {self.machine.name} by "
627
+ f"rejected RFID login on {self.machine.display_name} by "
553
628
  f"UNAUTHORIZED user {user.full_name}"
554
629
  )
555
630
 
631
+ async def _handle_rfid_tracking_always_enabled(
632
+ self, users: UsersConfig, rfid_value: Optional[str]
633
+ ) -> None:
634
+ """Track RFID changes for always-enabled machines without changing state.
635
+
636
+ This method logs RFID insertions and removals for auditing purposes while
637
+ maintaining the always-on state of the machine.
638
+ """
639
+ # locking handled in update()
640
+ if rfid_value is None:
641
+ # RFID removed
642
+ logging.getLogger("AUTH").info(
643
+ "RFID removed on always-enabled machine %s (was %s); session duration %d seconds",
644
+ self.machine.display_name,
645
+ self.current_user.full_name if self.current_user else self.rfid_value,
646
+ (
647
+ time() - cast(float, self.rfid_present_since)
648
+ if self.rfid_present_since
649
+ else 0
650
+ ),
651
+ )
652
+ self.rfid_value = None
653
+ self.rfid_present_since = None
654
+ self.current_user = None
655
+ # State remains always-on (relay/display/LED not changed)
656
+ else:
657
+ # RFID inserted
658
+ self.rfid_present_since = time()
659
+ self.rfid_value = rfid_value
660
+ user: Optional[User] = users.users_by_fob.get(rfid_value)
661
+ if user:
662
+ self.current_user = user
663
+ logging.getLogger("AUTH").info(
664
+ "RFID inserted on always-enabled machine %s by %s (%s)",
665
+ self.machine.display_name,
666
+ user.full_name,
667
+ rfid_value,
668
+ )
669
+ else:
670
+ logging.getLogger("AUTH").warning(
671
+ "RFID inserted on always-enabled machine %s by unknown fob %s",
672
+ self.machine.display_name,
673
+ rfid_value,
674
+ )
675
+ # State remains always-on (relay/display/LED not changed)
676
+
556
677
  async def _user_is_authorized(
557
678
  self, user: User, slack: Optional["SlackHandler"] = None
558
679
  ) -> bool:
@@ -563,7 +684,7 @@ class MachineState:
563
684
  "User %s (%s) authorized for %s based on %s",
564
685
  user.full_name,
565
686
  user.account_id,
566
- self.machine.name,
687
+ self.machine.display_name,
567
688
  auth,
568
689
  )
569
690
  return True
@@ -573,12 +694,12 @@ class MachineState:
573
694
  "unauthorized_warn_only==True",
574
695
  user.full_name,
575
696
  user.account_id,
576
- self.machine.name,
697
+ self.machine.display_name,
577
698
  )
578
699
  if slack:
579
700
  await slack.admin_log(
580
701
  f"WARNING - Authorizing user {user.full_name} for "
581
- f"{self.machine.name} based on unauthorized_warn_only "
702
+ f"{self.machine.display_name} based on unauthorized_warn_only "
582
703
  "setting for machine. User is NOT authorized for this "
583
704
  "machine."
584
705
  )
@@ -160,11 +160,11 @@ class SlackHandler:
160
160
  msg.channel_id,
161
161
  )
162
162
  return None
163
- if msg.command[0] == "oops" and len(msg.command) == 2:
163
+ if msg.command[0] == "oops" and len(msg.command) >= 2:
164
164
  return await self.oops(msg, say)
165
- elif msg.command[0] == "lock" and len(msg.command) == 2:
165
+ elif msg.command[0] == "lock" and len(msg.command) >= 2:
166
166
  return await self.lock(msg, say)
167
- elif msg.command[0] == "clear" and len(msg.command) == 2:
167
+ elif msg.command[0] == "clear" and len(msg.command) >= 2:
168
168
  return await self.clear(msg, say)
169
169
  await say(self.HELP_RESPONSE)
170
170
 
@@ -175,7 +175,7 @@ class SlackHandler:
175
175
  mname: str
176
176
  mach: Machine
177
177
  for mname, mach in sorted(mconf.machines_by_name.items()):
178
- resp += mname + ": "
178
+ resp += mach.display_name + ": "
179
179
  if mach.state.is_oopsed or mach.state.is_locked_out:
180
180
  if mach.state.is_oopsed:
181
181
  resp += "Oopsed "
@@ -199,44 +199,44 @@ class SlackHandler:
199
199
 
200
200
  async def oops(self, msg: Message, say: AsyncSay) -> None:
201
201
  """Set oops status on a machine."""
202
- mname: str = msg.command[1]
202
+ mname: str = " ".join(msg.command[1:])
203
203
  mconf: MachinesConfig = self.quart.config["MACHINES"]
204
- mach: Optional[Machine] = mconf.machines_by_name.get(mname)
204
+ mach: Optional[Machine] = mconf.get_machine(mname)
205
205
  if not mach:
206
206
  await say(
207
- f"Invalid machine name '{mname}'. Use status command to "
207
+ f"Invalid machine name or alias '{mname}'. Use status command to "
208
208
  f"list all machines."
209
209
  )
210
210
  return
211
211
  if mach.state.is_oopsed:
212
- await say(f"Machine {mname} is already oopsed.")
212
+ await say(f"Machine {mach.display_name} is already oopsed.")
213
213
  return
214
214
  await mach.oops(slack=self)
215
215
 
216
216
  async def lock(self, msg: Message, say: AsyncSay) -> None:
217
217
  """Set lock status on a machine."""
218
- mname: str = msg.command[1]
218
+ mname: str = " ".join(msg.command[1:])
219
219
  mconf: MachinesConfig = self.quart.config["MACHINES"]
220
- mach: Optional[Machine] = mconf.machines_by_name.get(mname)
220
+ mach: Optional[Machine] = mconf.get_machine(mname)
221
221
  if not mach:
222
222
  await say(
223
- f"Invalid machine name '{mname}'. Use status command to "
223
+ f"Invalid machine name or alias '{mname}'. Use status command to "
224
224
  f"list all machines."
225
225
  )
226
226
  return
227
227
  if mach.state.is_locked_out:
228
- await say(f"Machine {mname} is already locked-out.")
228
+ await say(f"Machine {mach.display_name} is already locked-out.")
229
229
  return
230
230
  await mach.lockout(slack=self)
231
231
 
232
232
  async def clear(self, msg: Message, say: AsyncSay) -> None:
233
233
  """Clear oops and lock status on a machine."""
234
- mname: str = msg.command[1]
234
+ mname: str = " ".join(msg.command[1:])
235
235
  mconf: MachinesConfig = self.quart.config["MACHINES"]
236
- mach: Optional[Machine] = mconf.machines_by_name.get(mname)
236
+ mach: Optional[Machine] = mconf.get_machine(mname)
237
237
  if not mach:
238
238
  await say(
239
- f"Invalid machine name '{mname}'. Use status command to "
239
+ f"Invalid machine name or alias '{mname}'. Use status command to "
240
240
  f"list all machines."
241
241
  )
242
242
  return
@@ -248,7 +248,7 @@ class SlackHandler:
248
248
  await mach.unlock(slack=self)
249
249
  acted = True
250
250
  if not acted:
251
- await say(f"Machine {mname} is not oopsed or locked-out.")
251
+ await say(f"Machine {mach.display_name} is not oopsed or locked-out.")
252
252
 
253
253
  async def log_unoops(self, machine: Machine, source: str) -> None:
254
254
  """
@@ -262,13 +262,13 @@ class SlackHandler:
262
262
  create_task(
263
263
  self.app.client.chat_postMessage(
264
264
  channel=self.control_channel_id,
265
- text=f"Machine {machine.name} un-oopsed via {source}.",
265
+ text=f"Machine {machine.display_name} un-oopsed via {source}.",
266
266
  )
267
267
  )
268
268
  create_task(
269
269
  self.app.client.chat_postMessage(
270
270
  channel=self.oops_channel_id,
271
- text=f"Machine {machine.name} oops has been cleared.",
271
+ text=f"Machine {machine.display_name} oops has been cleared.",
272
272
  )
273
273
  )
274
274
 
@@ -286,13 +286,13 @@ class SlackHandler:
286
286
  create_task(
287
287
  self.app.client.chat_postMessage(
288
288
  channel=self.control_channel_id,
289
- text=f"Machine {machine.name} oopsed via {source} by {user_name}.",
289
+ text=f"Machine {machine.display_name} oopsed via {source} by {user_name}.",
290
290
  )
291
291
  )
292
292
  create_task(
293
293
  self.app.client.chat_postMessage(
294
294
  channel=self.oops_channel_id,
295
- text=f"Machine {machine.name} has been Oops'ed!",
295
+ text=f"Machine {machine.display_name} has been Oops'ed!",
296
296
  )
297
297
  )
298
298
 
@@ -308,13 +308,13 @@ class SlackHandler:
308
308
  create_task(
309
309
  self.app.client.chat_postMessage(
310
310
  channel=self.control_channel_id,
311
- text=f"Machine {machine.name} locked-out cleared via {source}.",
311
+ text=f"Machine {machine.display_name} locked-out cleared via {source}.",
312
312
  )
313
313
  )
314
314
  create_task(
315
315
  self.app.client.chat_postMessage(
316
316
  channel=self.oops_channel_id,
317
- text=f"Machine {machine.name} is no longer locked-out for "
317
+ text=f"Machine {machine.display_name} is no longer locked-out for "
318
318
  f"maintenance.",
319
319
  )
320
320
  )
@@ -331,13 +331,13 @@ class SlackHandler:
331
331
  create_task(
332
332
  self.app.client.chat_postMessage(
333
333
  channel=self.control_channel_id,
334
- text=f"Machine {machine.name} locked-out via {source}.",
334
+ text=f"Machine {machine.display_name} locked-out via {source}.",
335
335
  )
336
336
  )
337
337
  create_task(
338
338
  self.app.client.chat_postMessage(
339
339
  channel=self.oops_channel_id,
340
- text=f"Machine {machine.name} is locked-out for maintenance.",
340
+ text=f"Machine {machine.display_name} is locked-out for maintenance.",
341
341
  )
342
342
  )
343
343
 
@@ -134,52 +134,35 @@ class PromCustomCollector:
134
134
  )
135
135
  m: Machine
136
136
  for m in mconf.machines:
137
- relay_state.add_metric(
138
- {"machine_name": m.name}, 1 if m.state.relay_desired_state else 0
139
- )
140
- oops_state.add_metric(
141
- {"machine_name": m.name}, 1 if m.state.is_oopsed else 0
142
- )
143
- lockout_state.add_metric(
144
- {"machine_name": m.name}, 1 if m.state.is_locked_out else 0
145
- )
146
- unauth_state.add_metric(
147
- {"machine_name": m.name}, 1 if m.unauthorized_warn_only else 0
148
- )
149
- m_checkin.add_metric({"machine_name": m.name}, m.state.last_checkin or 0)
150
- m_update.add_metric({"machine_name": m.name}, m.state.last_update or 0)
151
- m_rfid_present.add_metric(
152
- {"machine_name": m.name}, 1 if m.state.rfid_value else 0
153
- )
154
- m_rfid_present_since.add_metric(
155
- {"machine_name": m.name}, m.state.rfid_present_since or 0
156
- )
157
- current_amps.add_metric({"machine_name": m.name}, m.state.current_amps)
158
- m_user.add_metric(
159
- {"machine_name": m.name}, 1 if m.state.current_user else 0
160
- )
161
- uptime.add_metric({"machine_name": m.name}, m.state.uptime)
162
- wifi_db.add_metric({"machine_name": m.name}, m.state.wifi_signal_db or 0)
163
- wifi_percent.add_metric(
164
- {"machine_name": m.name}, m.state.wifi_signal_percent or 0
165
- )
166
- temp_c.add_metric(
167
- {"machine_name": m.name}, m.state.internal_temperature_c or 0
168
- )
137
+ labels = {"machine_name": m.name, "display_name": m.display_name}
138
+ relay_state.add_metric(labels, 1 if m.state.relay_desired_state else 0)
139
+ oops_state.add_metric(labels, 1 if m.state.is_oopsed else 0)
140
+ lockout_state.add_metric(labels, 1 if m.state.is_locked_out else 0)
141
+ unauth_state.add_metric(labels, 1 if m.unauthorized_warn_only else 0)
142
+ m_checkin.add_metric(labels, m.state.last_checkin or 0)
143
+ m_update.add_metric(labels, m.state.last_update or 0)
144
+ m_rfid_present.add_metric(labels, 1 if m.state.rfid_value else 0)
145
+ m_rfid_present_since.add_metric(labels, m.state.rfid_present_since or 0)
146
+ current_amps.add_metric(labels, m.state.current_amps)
147
+ m_user.add_metric(labels, 1 if m.state.current_user else 0)
148
+ uptime.add_metric(labels, m.state.uptime)
149
+ wifi_db.add_metric(labels, m.state.wifi_signal_db or 0)
150
+ wifi_percent.add_metric(labels, m.state.wifi_signal_percent or 0)
151
+ temp_c.add_metric(labels, m.state.internal_temperature_c or 0)
169
152
  led.add_metric(
170
- {"machine_name": m.name, "led_attribute": "red"},
153
+ {**labels, "led_attribute": "red"},
171
154
  m.state.status_led_rgb[0],
172
155
  )
173
156
  led.add_metric(
174
- {"machine_name": m.name, "led_attribute": "green"},
157
+ {**labels, "led_attribute": "green"},
175
158
  m.state.status_led_rgb[1],
176
159
  )
177
160
  led.add_metric(
178
- {"machine_name": m.name, "led_attribute": "blue"},
161
+ {**labels, "led_attribute": "blue"},
179
162
  m.state.status_led_rgb[2],
180
163
  )
181
164
  led.add_metric(
182
- {"machine_name": m.name, "led_attribute": "brightness"},
165
+ {**labels, "led_attribute": "brightness"},
183
166
  m.state.status_led_brightness,
184
167
  )
185
168
  yield mconf_load