Subject: [PATCH v4] services: Add power.

From: Tomas Volf <[email-masked]>
Date: Sun, 2 Mar 2025 14:44:34 +0100

* gnu/services/power.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
* doc/guix.texi (Power Management Services): Document service and data types.

Change-Id: If205d19bea1d20a99309626e28521a2d6fe6702f
---
 doc/guix.texi          | 382 ++++++++++++++++++++++-
 gnu/local.mk           |   1 +
 gnu/services/power.scm | 669 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 1049 insertions(+), 3 deletions(-)
 create mode 100644 gnu/services/power.scm

diff --git a/doc/guix.texi b/doc/guix.texi
index a036c85c31..095e067ea1 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -123,7 +123,7 @@
 Copyright @copyright{} 2023 Thomas Ieong@*
 Copyright @copyright{} 2023 Saku Laesvuori@*
 Copyright @copyright{} 2023 Graham James Addis@*
-Copyright @copyright{} 2023, 2024 Tomas Volf@*
+Copyright @copyright{} 2023-2025 Tomas Volf@*
 Copyright @copyright{} 2024, 2025 Herman Rimm@*
 Copyright @copyright{} 2024 Matthew Trzcinski@*
 Copyright @copyright{} 2024 Richard Sent@*
@@ -422,7 +422,7 @@ Top
 * Network File System::         NFS related services.
 * Samba Services::              Samba services.
 * Continuous Integration::      Cuirass and Laminar services.
-* Power Management Services::   Extending battery life.
+* Power Management Services::   Extending battery life, etc.
 * Audio Services::              The MPD.
 * Virtualization Services::     Virtualization services.
 * Version Control Services::    Providing remote access to Git repositories.
@@ -19270,7 +19270,7 @@ Services
 * Network File System::         NFS related services.
 * Samba Services::              Samba services.
 * Continuous Integration::      Cuirass and Laminar services.
-* Power Management Services::   Extending battery life.
+* Power Management Services::   Extending battery life, etc.
 * Audio Services::              The MPD.
 * Virtualization Services::     Virtualization services.
 * Version Control Services::    Providing remote access to Git repositories.
@@ -36868,6 +36868,382 @@ Power Management Services
 @end table
 @end deftp
 
+The @code{(gnu services power)} module provides a service definition for
+@uref{http://www.apcupsd.org/, apcupsd}, a utility to interact with
+@acronym{APC, APC by Schneider Electric or formerly American Power
+Conversion Corporation} @acronym{UPS, Uninterruptible Power Supply}
+devices.  Apcupsd also works with some @acronym{OEM, Original Equipment
+Manufacturer}-branded products manufactured by APC.
+
+@defvar apcupsd-service-type
+The service type for apcupsd.  For USB UPSes no configuration is
+necessary, however tweaking some fields to better suit your needs might
+be desirable.  The defaults are taken from the upstream configuration
+and they are not very conservative (for example, the default
+@code{battery-level} of 5% may be considered too low by some).
+
+The default event handlers do send emails, read more in
+@ref{apcupsd-event-handlers}.
+
+@lisp
+(service apcupsd-service-type)
+@end lisp
+@end defvar
+
+@deftp {Data Type} apcupsd-configuration
+
+Available @code{apcupsd-configuration} fields are:
+
+@table @asis
+@item @code{apcupsd} (default: @code{apcupsd}) (type: package)
+The @code{apcupsd} package to use.
+
+@item @code{shepherd-service-name} (default: @code{apcupsd}) (type: symbol)
+The name of the shepherd service.  You can add the service multiple
+times with different names to manage multiple UPSes.
+
+@item @code{auto-start?} (default: @code{#t}) (type: boolean)
+Should the shepherd service auto-start?
+
+@item @code{pid-file} (default: @code{"/var/run/apcupsd.pid"}) (type: string)
+The file name of the pid file.
+
+@item @code{debug-level} (default: @code{0}) (type: integer)
+The logging verbosity.  Bigger number means more logs.  The source code
+uses up to @code{300} as debug level value, so a value of @code{999}
+seems reasonable to enable all the logs.
+
+@item @code{run-dir} (default: @code{"/var/run/apcupsd"}) (type: string)
+The directory containing runtime information.  You need to change this
+if you desire to run multiple instances of the daemon.
+
+@item @code{name} (type: maybe-string)
+Use this to give your UPS a name in log files and such.  This is
+particularly useful if you have multiple UPSes.  This does not set the
+EEPROM.  It should be 8 characters or less.
+
+@item @code{cable} (default: @code{usb}) (type: enum-cable)
+The type of a cable connecting the UPS to your computer.  Possible
+generic choices are @code{'simple}, @code{'smart}, @code{'ether} and
+@code{'usb}.
+
+Alternatively, a specific cable model number may be used:
+@code{'940-0119A}, @code{'940-0127A}, @code{'940-0128A},
+@code{'940-0020B}, @code{'940-0020C}, @code{'940-0023A},
+@code{'940-0024B}, @code{'940-0024C}, @code{'940-1524C},
+@code{'940-0024G}, @code{'940-0095A}, @code{'940-0095B},
+@code{'940-0095C}, @code{'940-0625A}, @code{'M-04-02-2000}.
+
+@item @code{type} (default: @code{usb}) (type: enum-type)
+The type of the UPS you have.
+
+@table @code
+@item apcsmart
+Newer serial character device, appropriate for SmartUPS models using a
+serial cable (not an USB).
+
+@item usb
+Most new UPSes are an USB.
+
+@item net
+Network link to a master apcupsd through apcupsd's Network Information
+Server.  This is used if the UPS powering your computer is connected to
+a different computer for monitoring.
+
+@item snmp
+SNMP network link to an SNMP-enabled UPS device.
+
+@item netsnmp
+Same as the SNMP above but requires use of the net-snmp library.  Unless
+you have a specific need for this old driver, you should use the
+@code{'snmp} instead.
+
+@item dumb
+An old serial character device for use with simple-signaling UPSes.
+
+@item pcnet
+A PowerChute Network Shutdown protocol which can be used as an
+alternative to an SNMP with the AP9617 family of smart slot cards.
+
+@item modbus
+A serial device for use with newest SmartUPS models supporting the
+MODBUS protocol.
+
+@end table
+
+@item @code{device} (default: @code{""}) (type: string)
+For USB UPSes, usually you want to set this to an empty string (the
+default).  For other UPS types, you must specify an appropriate port or
+address.
+
+@table @code
+@item apcsmart
+Set to the appropriate @file{/dev/tty**} device.
+
+@item usb
+A null string setting enables auto-detection, which is the best choice
+for most installations.
+
+@item net
+Set to @code{@var{hostname}:@var{port}}.
+
+@item snmp
+Set to @code{@var{hostname}:@var{port}:@var{vendor}:@var{community}}.
+The @var{hostname} is the ip address or hostname of the UPS on the
+network.  The @var{vendor} can be can be "APC" or "APC_NOTRAP".
+"APC_NOTRAP" will disable SNMP trap catching; you usually want "APC".
+The @var{port} is usually 161.  The @var{community} is usually
+"private".
+
+@item netsnmp
+Same as the @code{'snmp}.
+
+@item dumb
+Set to the appropriate @file{/dev/tty**} device.
+
+@item pcnet
+Set to @code{@var{ipaddr}:@var{username}:@var{passphrase}:@var{port}}.
+The @var{ipaddr} is the IP address of the UPS management card.  The
+@var{username} and the @var{passphrase} are the credentials for which
+the card has been configured.  The @var{port} is the port number on
+which to listen for messages from the UPS, normally 3052.  If this
+parameter is empty or missing, the default of 3052 will be used.
+
+@item modbus
+Set to the appropriate @file{/dev/tty**} device.  You can also leave it
+empty for MODBUS over USB or set to the serial number of the UPS.
+
+@end table
+
+@item @code{poll-time} (default: @code{60}) (type: integer)
+The interval (in seconds) at which apcupsd polls the UPS for status.
+This setting applies both to directly-attached UPSes (apcsmart, usb,
+dumb) and networked UPSes (net, snmp).  Lowering this setting will
+improve the apcupsd's responsiveness to certain events at the cost of
+higher CPU utilization.
+
+@item @code{on-batery-delay} (default: @code{6}) (type: integer)
+The time in seconds from when a power failure is detected until we react
+to it with an onbattery event.  The @code{'powerout} event will be
+triggered immediately when a power failure is detected.  However, the
+@code{'onbattery} event will be trigger only after this delay.
+
+@item @code{battery-level} (default: @code{5}) (type: integer)
+If during a power failure, the remaining battery percentage (as reported
+by the UPS) is below or equal to this value, the apcupsd will initiate a
+system shutdown.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation
+
+@item @code{remaining-minutes} (default: @code{3}) (type: integer)
+If during a power failure, the remaining runtime in minutes (as
+calculated internally by the UPS) is below or equal to this value,
+apcupsd will initiate a system shutdown.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation
+
+@item @code{timeout} (default: @code{0}) (type: integer)
+If during a power failure, the UPS has run on batteries for this many
+seconds or longer, apcupsd will initiate a system shutdown.  The value
+of 0 disables this timer.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation
+
+@item @code{annoy-interval} (default: @code{300}) (type: integer)
+The time in seconds between annoying users (via the @code{'annoyme}
+event) to sign off prior to system shutdown.  0 disables.
+
+@item @code{annoy-delay} (default: @code{60}) (type: integer)
+The initial delay in seconds after a power failure before warning users
+to get off the system.
+
+@item @code{no-logon} (default: @code{disable}) (type: enum-no-logon)
+The condition which determines when users are prevented from logging in
+during a power failure.
+
+@item @code{kill-delay} (default: @code{0}) (type: integer)
+If this is non-zero, the apcupsd will continue running after a shutdown
+has been requested, and after the specified time in seconds attempt to
+kill the power.  This is for use on systems where apcupsd cannot regain
+control after a shutdown.
+
+@item @code{net-server} (default: @code{#f}) (type: boolean)
+If enabled, a network information server process will be started.
+
+@item @code{net-server-ip} (default: @code{"127.0.0.1"}) (type: string)
+An IP address on which the NIS server will listen for incoming
+connections.
+
+@item @code{net-server-port} (default: @code{3551}) (type: integer)
+An IP port on which the NIS server will listen for incoming connections.
+
+@item @code{net-server-events-file} (type: maybe-string)
+If you want the last few EVENTS to be available over the network by the
+network information server, you must set this to a file name.
+
+@item @code{net-server-events-file-max-size} (default: @code{10}) (type: integer)
+The maximum size of the events file in kilobytes.
+
+@item @code{class} (default: @code{standalone}) (type: enum-class)
+Normally standalone unless you share an UPS using an APC ShareUPS card.
+
+@item @code{mode} (default: @code{disable}) (type: enum-mode)
+Normally disable unless you share an UPS using an APC ShareUPS card.
+
+@item @code{stat-time} (default: @code{0}) (type: integer)
+The time interval in seconds between writing the status file, 0
+disables.
+
+@item @code{log-stats} (default: @code{#f}) (type: boolean)
+Also write the stats as a logs.  This generates a lot of output.
+
+@item @code{data-time} (default: @code{0}) (type: integer)
+The time interval in seconds between writing the data records to the log
+file, 0 disables.
+
+@item @code{facility} (type: maybe-string)
+The logging facility for the syslog.
+
+@item @code{event-handlers} (type: apcupsd-event-handlers)
+Handlers for events produced by apcupsd.
+
+@end table
+@end deftp
+
+@anchor{apcupsd-event-handlers}
+@deftp {Data Type} apcupsd-event-handlers
+
+For a description of the events please refer to @samp{man 8 apccontrol}.
+
+Each handler shall be a gexp.  It is spliced into the control script for
+the daemon.  In addition to the standard Guile programming environment,
+the following procedures and variables are also available:
+
+@table @code
+@item conf
+Variable containing the file name of the configuration file.
+
+@item powerfail-file
+Variable containing the file name of the powerfail file.
+
+@item cmd
+The event currently being handled.
+
+@item name
+The name of the UPS as specified in the configuration file.
+
+@item connected?
+Is @code{#t} if @command{apcupsd} is connected to the UPS via a serial
+port (or a USB port).  In most configurations, this will be the case.
+In the case of a Slave machine where apcupsd is not directly connected
+to the UPS, this value will be @code{#f}.
+
+@item powered?
+Is @code{#t} if the computer on which @command{apcupsd} is running is
+powered by the UPS and @code{#f} if not.  At the moment, this value is
+unimplemented and always @code{#f}.
+
+@item (err @var{fmt} @var{args...})
+Wrapper around @code{format} outputting to @code{(current-error-port)}.
+
+@item (wall @var{fmt} @var{args...})
+Wrapper around @code{format} outputting via @command{wall}.
+
+@item (apcupsd @var{args...})
+Call @command{apcupsd} while passing the correct configuration file and
+all the arguments.
+
+@item (mail-to-root @var{subject} @var{body})
+Send an email to the local administrator.  This procedure assumes the
+@command{sendmail} is located at @command{/run/privileged/bin/sendmail}
+(as would be the case with @code{opensmtpd-service-type}).
+
+@end table
+
+Available @code{apcupsd-event-handlers} fields are:
+
+@table @asis
+@item @code{modules} (type: gexp)
+Additional modules to import into the generated handler script.
+
+@item @code{killpower} (type: gexp)
+The handler for the killpower event.
+
+@item @code{commfailure} (type: gexp)
+The handler for the commfailure event.
+
+@item @code{commok} (type: gexp)
+The handler for the commfailure event.
+
+@item @code{powerout} (type: gexp)
+The handler for the powerout event.
+
+@item @code{onbattery} (type: gexp)
+The handler for the onbattery event.
+
+@item @code{offbattery} (type: gexp)
+The handler for the offbattery event.
+
+@item @code{mainsback} (type: gexp)
+The handler for the mainsback event.
+
+@item @code{failing} (type: gexp)
+The handler for the failing event.
+
+@item @code{timeout} (type: gexp)
+The handler for the timeout event.
+
+@item @code{loadlimit} (type: gexp)
+The handler for the loadlimit event.
+
+@item @code{runlimit} (type: gexp)
+The handler for the runlimit event.
+
+@item @code{doreboot} (type: gexp)
+The handler for the doreboot event.
+
+@item @code{doshutdown} (type: gexp)
+The handler for the doshutdown event.
+
+@item @code{annoyme} (type: gexp)
+The handler for the annoyme event.
+
+@item @code{emergency} (type: gexp)
+The handler for the emergency event.
+
+@item @code{changeme} (type: gexp)
+The handler for the changeme event.
+
+@item @code{remotedown} (type: gexp)
+The handler for the remotedown event.
+
+@item @code{startselftest} (type: gexp)
+The handler for the startselftest event.
+
+@item @code{endselftest} (type: gexp)
+The handler for the endselftest event.
+
+@item @code{battdetach} (type: gexp)
+The handler for the battdetach event.
+
+@item @code{battattach} (type: gexp)
+The handler for the battattach event.
+
+@end table
+@end deftp
+
 @node Audio Services
 @subsection Audio Services
 
diff --git a/gnu/local.mk b/gnu/local.mk
index c8a29bf98b..0048e09ede 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -764,6 +764,7 @@ GNU_SYSTEM_MODULES =				\
   %D%/services/nix.scm				\
   %D%/services/nfs.scm			\
   %D%/services/pam-mount.scm			\
+  %D%/services/power.scm			\
   %D%/services/science.scm			\
   %D%/services/security.scm			\
   %D%/services/security-token.scm		\
diff --git a/gnu/services/power.scm b/gnu/services/power.scm
new file mode 100644
index 0000000000..68a67e63e8
--- /dev/null
+++ b/gnu/services/power.scm
@@ -0,0 +1,669 @@
+;;; Copyright © 2025 Tomas Volf <[email-masked]>
+
+;;;; Commentary:
+
+;;; Power-related services.
+
+;;;; Code:
+
+(define-module (gnu services power)
+  #:use-module (gnu)
+  #:use-module (gnu packages admin)
+  #:use-module (gnu packages linux)
+  #:use-module (gnu packages power)
+  #:use-module (gnu services configuration)
+  #:use-module (gnu services shepherd)
+  #:use-module (gnu services)
+  #:use-module (guix packages)
+  #:use-module (guix records)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
+  #:export (apcupsd-service-type
+
+            apcupsd-configuration
+            apcupsd-configuration-apcupsd
+            apcupsd-configuration-shepherd-service-name
+            apcupsd-configuration-auto-start?
+            apcupsd-configuration-pid-file
+            apcupsd-configuration-debug-level
+            apcupsd-configuration-run-dir
+            apcupsd-configuration-name
+            apcupsd-configuration-cable
+            apcupsd-configuration-type
+            apcupsd-configuration-device
+            apcupsd-configuration-poll-time
+            apcupsd-configuration-on-batery-delay
+            apcupsd-configuration-battery-level
+            apcupsd-configuration-remaining-minutes
+            apcupsd-configuration-timeout
+            apcupsd-configuration-annoy-interval
+            apcupsd-configuration-annoy-delay
+            apcupsd-configuration-no-logon
+            apcupsd-configuration-kill-delay
+            apcupsd-configuration-net-server
+            apcupsd-configuration-net-server-ip
+            apcupsd-configuration-net-server-port
+            apcupsd-configuration-net-server-events-file
+            apcupsd-configuration-net-server-events-file-max-size
+            apcupsd-configuration-class
+            apcupsd-configuration-mode
+            apcupsd-configuration-stat-time
+            apcupsd-configuration-log-stats
+            apcupsd-configuration-data-time
+            apcupsd-configuration-facility
+            apcupsd-configuration-event-handlers
+
+            apcupsd-event-handlers
+            apcupsd-event-handlers-modules
+            apcupsd-event-handlers-annoyme
+            apcupsd-event-handlers-battattach
+            apcupsd-event-handlers-battdetach
+            apcupsd-event-handlers-changeme
+            apcupsd-event-handlers-commfailure
+            apcupsd-event-handlers-commok
+            apcupsd-event-handlers-doreboot
+            apcupsd-event-handlers-doshutdown
+            apcupsd-event-handlers-emergency
+            apcupsd-event-handlers-endselftest
+            apcupsd-event-handlers-failing
+            apcupsd-event-handlers-killpower
+            apcupsd-event-handlers-loadlimit
+            apcupsd-event-handlers-mainsback
+            apcupsd-event-handlers-offbattery
+            apcupsd-event-handlers-onbattery
+            apcupsd-event-handlers-powerout
+            apcupsd-event-handlers-remotedown
+            apcupsd-event-handlers-runlimit
+            apcupsd-event-handlers-startselftest
+            apcupsd-event-handlers-timeout))
+
+(define-configuration/no-serialization apcupsd-event-handlers
+  (modules
+   (gexp #~())
+   "Additional modules to import into the generated handler script.")
+  (killpower
+   (gexp
+    #~((wall "Apccontrol doing: apcupsd --killpower on UPS ~a" name)
+       (sleep 10)
+       (apcupsd "--killpower")
+       (wall "Apccontrol has done: apcupsd --killpower on UPS ~a" name)))
+   "The handler for the killpower event.")
+  (commfailure
+   (gexp
+    #~((let ((msg (format #f "~a Communications with UPS ~a lost."
+                          (gethostname) name)))
+         (mail-to-root msg msg))
+       (wall "Warning: communications lost with UPS ~a" name)))
+   "The handler for the commfailure event.")
+  (commok
+   (gexp
+    #~((let ((msg (format #f "~a Communications with UPS ~a restored."
+                          (gethostname) name)))
+         (mail-to-root msg msg))
+       (wall "Communications restored with UPS ~a" name)))
+   "The handler for the commfailure event.")
+  (powerout
+   (gexp
+    #~(#t))
+   "The handler for the powerout event.")
+  (onbattery
+   (gexp
+    #~((let ((msg (format #f "~a UPS ~a Power Failure !!!"
+                          (gethostname) name)))
+         (mail-to-root msg msg))
+       (wall "Power failure on UPS ~a.  Running on batteries." name)))
+   "The handler for the onbattery event.")
+  (offbattery
+   (gexp
+    #~((let ((msg (format #f "~a UPS ~a Power has returned."
+                          (gethostname) name)))
+         (mail-to-root msg msg))
+       (wall "Power has returned on UPS ~a..." name)))
+   "The handler for the offbattery event.")
+  (mainsback
+   (gexp
+    #~((when (file-exists? powerfail-file)
+         (wall "Continuing with shutdown."))))
+   "The handler for the mainsback event.")
+  (failing
+   (gexp
+    #~((wall "Battery power exhausted on UPS ~a.  Doing shutdown." name)))
+   "The handler for the failing event.")
+  (timeout
+   (gexp
+    #~((wall "Battery time limit exceeded on UPS ~a.  Doing shutdown." name)))
+   "The handler for the timeout event.")
+  (loadlimit
+   (gexp
+    #~((wall "Remaining battery charge below limit on UPS ~a.  Doing shutdown." name)))
+   "The handler for the loadlimit event.")
+  (runlimit
+   (gexp
+    #~((wall "Remaining battery runtime below limit on UPS ~a.  Doing shutdown." name)))
+   "The handler for the runlimit event.")
+  (doreboot
+   (gexp
+    #~((wall "UPS ~a initiating Reboot Sequence" name)
+       (system* #$(file-append shepherd "/sbin/reboot"))))
+   "The handler for the doreboot event.")
+  (doshutdown
+   (gexp
+    #~((wall "UPS ~a initiated Shutdown Sequence" name)
+       (system* #$(file-append shepherd "/sbin/halt"))))
+   "The handler for the doshutdown event.")
+  (annoyme
+   (gexp
+    #~((wall "Power problems with UPS ~a.  Please logoff." name)))
+   "The handler for the annoyme event.")
+  (emergency
+   (gexp
+    #~((wall "Emergency Shutdown.  Possible battery failure on UPS ~a." name)))
+   "The handler for the emergency event.")
+  (changeme
+   (gexp
+    #~((let ((msg (format #f "~a UPS ~a battery needs changing NOW."
+                          (gethostname) name)))
+         (mail-to-root msg msg))
+       (wall "Emergency!  Batteries have failed on UPS ~a.  Change them NOW." name)))
+   "The handler for the changeme event.")
+  (remotedown
+   (gexp
+    #~((wall "Remote Shutdown.  Beginning Shutdown Sequence.")))
+   "The handler for the remotedown event.")
+  (startselftest
+   (gexp
+    #~(#t))
+   "The handler for the startselftest event.")
+  (endselftest
+   (gexp
+    #~(#t))
+   "The handler for the endselftest event.")
+  (battdetach
+   (gexp
+    #~(#t))
+   "The handler for the battdetach event.")
+  (battattach
+   (gexp
+    #~(#t))
+   "The handler for the battattach event."))
+
+(define-syntax define-enum
+  (lambda (x)
+    (syntax-case x ()
+      ((_ name values)
+       (let* ((datum/name (syntax->datum #'name))
+              (datum/predicate (string->symbol
+                                (format #f "enum-~a?" datum/name)))
+              (datum/serialize (string->symbol
+                                (format #f "serialize-enum-~a" datum/name))))
+         (with-syntax
+             ((predicate (datum->syntax x datum/predicate))
+              (serialize (datum->syntax x datum/serialize)))
+           #'(begin
+               (define (predicate value)
+                 (memq value values))
+               (define serialize serialize-symbol))))))))
+
+(define mangle-field-name
+  (match-lambda
+    ('name                            "UPSNAME")
+    ('cable                           "UPSCABLE")
+    ('type                            "UPSTYPE")
+    ('device                          "DEVICE")
+    ('poll-time                       "POLLTIME")
+    ('lock-dir                        "LOCKFILE")
+    ('power-fail-dir                  "PWRFAILDIR")
+    ('no-login-dir                    "NOLOGINDIR")
+    ('on-batery-delay                 "ONBATTERYDELAY")
+    ('battery-level                   "BATTERYLEVEL")
+    ('remaining-minutes               "MINUTES")
+    ('timeout                         "TIMEOUT")
+    ('annoy-interval                  "ANNOY")
+    ('annoy-delay                     "ANNOYDELAY")
+    ('no-logon                        "NOLOGON")
+    ('kill-delay                      "KILLDELAY")
+    ('net-server                      "NETSERVER")
+    ('net-server-ip                   "NISIP")
+    ('net-server-port                 "NISPORT")
+    ('net-server-events-file          "EVENTSFILE")
+    ('net-server-events-file-max-size "EVENTSFILEMAX")
+    ('class                           "UPSCLASS")
+    ('mode                            "UPSMODE")
+    ('stat-time                       "STATTIME")
+    ('stat-file                       "STATFILE")
+    ('log-stats                       "LOGSTATS")
+    ('data-time                       "DATATIME")
+    ('facility                        "FACILITY")))
+
+(define (serialize-string field-name value)
+  #~(format #f "~a ~a\n" #$(mangle-field-name field-name) '#$value))
+(define serialize-symbol serialize-string)
+(define serialize-integer serialize-string)
+(define (serialize-boolean field-name value)
+  #~(format #f "~a ~a\n"
+            #$(mangle-field-name field-name)
+            #$(if value "on" "off")))
+
+(define-maybe string)
+
+(define-enum cable '( simple smart ether usb
+                      940-0119A 940-0127A 940-0128A 940-0020B 940-0020C
+                      940-0023A 940-0024B 940-0024C 940-1524C 940-0024G
+                      940-0095A 940-0095B 940-0095C 940-0625A MAM-04-02-2000))
+(define-enum type '(apcsmart usb net snmp netsnmp dumb pcnet modbus test))
+(define-enum no-logon '(disable timeout percent minutes always))
+(define-enum class '(standalone shareslave sharemaster))
+(define-enum mode '(disable share))
+
+(define-configuration apcupsd-configuration
+  (apcupsd (package apcupsd) "The @code{apcupsd} package to use.")
+
+  (shepherd-service-name
+   (symbol 'apcupsd)
+   "The name of the shepherd service.  You can add the service multiple times
+with different names to manage multiple UPSes."
+   empty-serializer)
+  (auto-start?
+   (boolean #t)
+   "Should the shepherd service auto-start?"
+   empty-serializer)
+  (pid-file
+   (string "/run/apcupsd.pid")
+   "The file name of the PID file."
+   empty-serializer)
+  (debug-level
+   (integer 0)
+   "The logging verbosity.  Bigger number means more logs.  The source code
+uses up to @code{300} as debug level value, so a value of @code{999} seems
+reasonable to enable all the logs."
+   empty-serializer)
+
+  (run-dir
+   (string "/run/apcupsd")
+   "The directory containing runtime information.  You need to change this if
+you desire to run multiple instances of the daemon."
+   empty-serializer)
+
+  ;; General configuration parameters
+  (name
+   maybe-string
+   "Use this to give your UPS a name in log files and such.  This is
+particularly useful if you have multiple UPSes.  This does not set the EEPROM.
+It should be 8 characters or less.")
+  (cable
+   (enum-cable 'usb)
+   "The type of a cable connecting the UPS to your computer.  Possible generic
+choices are @code{'simple}, @code{'smart}, @code{'ether} and
+@code{'usb}.
+
+Alternatively, a specific cable model number may be used: @code{'940-0119A},
+@code{'940-0127A}, @code{'940-0128A}, @code{'940-0020B}, @code{'940-0020C},
+@code{'940-0023A}, @code{'940-0024B}, @code{'940-0024C}, @code{'940-1524C},
+@code{'940-0024G}, @code{'940-0095A}, @code{'940-0095B}, @code{'940-0095C},
+@code{'940-0625A}, @code{'M-04-02-2000}.")
+  (type
+   (enum-type 'usb)
+   "The type of the UPS you have.
+
+@table @code
+@item apcsmart
+Newer serial character device, appropriate for SmartUPS models using a serial
+cable (not an USB).
+
+@item usb
+Most new UPSes are an USB.
+
+@item net
+Network link to a master apcupsd through apcupsd's Network Information Server.
+This is used if the UPS powering your computer is connected to a different
+computer for monitoring.
+
+@item snmp
+SNMP network link to an SNMP-enabled UPS device.
+
+@item netsnmp
+Same as the SNMP above but requires use of the net-snmp library.  Unless you
+have a specific need for this old driver, you should use the @code{'snmp}
+instead.
+
+@item dumb
+An old serial character device for use with simple-signaling UPSes.
+
+@item pcnet
+A PowerChute Network Shutdown protocol which can be used as an alternative to
+an SNMP with the AP9617 family of smart slot cards.
+
+@item modbus
+A serial device for use with newest SmartUPS models supporting the MODBUS
+protocol.
+
+@end table")
+  (device
+   (string "")
+   "For USB UPSes, usually you want to set this to an empty string (the
+default).  For other UPS types, you must specify an appropriate port or
+address.
+
+@table @code
+@item apcsmart
+Set to the appropriate @file{/dev/tty**} device.
+
+@item usb
+A null string setting enables auto-detection, which is the best choice for
+most installations.
+
+@item net
+Set to @code{@var{hostname}:@var{port}}.
+
+@item snmp
+Set to @code{@var{hostname}:@var{port}:@var{vendor}:@var{community}}.  The
+@var{hostname} is the ip address or hostname of the UPS on the network.  The
+@var{vendor} can be can be \"APC\" or \"APC_NOTRAP\".  \"APC_NOTRAP\" will
+disable SNMP trap catching; you usually want \"APC\".  The @var{port} is
+usually 161.  The @var{community} is usually \"private\".
+
+@item netsnmp
+Same as the @code{'snmp}.
+
+@item dumb
+Set to the appropriate @file{/dev/tty**} device.
+
+@item pcnet
+Set to @code{@var{ipaddr}:@var{username}:@var{passphrase}:@var{port}}.  The
+@var{ipaddr} is the IP address of the UPS management card.  The @var{username}
+and the @var{passphrase} are the credentials for which the card has been
+configured.  The @var{port} is the port number on which to listen for messages
+from the UPS, normally 3052.  If this parameter is empty or missing, the
+default of 3052 will be used.
+
+@item modbus
+Set to the appropriate @file{/dev/tty**} device.  You can also leave it empty
+for MODBUS over USB or set to the serial number of the UPS.
+
+@end table")
+  (poll-time
+   (integer 60)
+   "The interval (in seconds) at which apcupsd polls the UPS for status.  This
+setting applies both to directly-attached UPSes (apcsmart, usb, dumb) and
+networked UPSes (net, snmp).  Lowering this setting will improve the apcupsd's
+responsiveness to certain events at the cost of higher CPU utilization.")
+
+  ;; Configuration parameters used during power failures
+  (on-batery-delay
+   (integer 6)
+   "The time in seconds from when a power failure is detected until we react
+to it with an onbattery event.  The @code{'powerout} event will be triggered
+immediately when a power failure is detected.  However, the @code{'onbattery}
+event will be trigger only after this delay.")
+  (battery-level
+   (integer 5)
+   "If during a power failure, the remaining battery percentage (as reported
+by the UPS) is below or equal to this value, the apcupsd will initiate a
+system shutdown.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation")
+  (remaining-minutes
+   (integer 3)
+   "If during a power failure, the remaining runtime in minutes (as calculated
+internally by the UPS) is below or equal to this value, apcupsd will initiate
+a system shutdown.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation")
+  (timeout
+   (integer 0)
+   "If during a power failure, the UPS has run on batteries for this many
+seconds or longer, apcupsd will initiate a system shutdown.  The value of 0
+disables this timer.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation")
+  (annoy-interval
+   (integer 300)
+   "The time in seconds between annoying users (via the @code{'annoyme} event)
+to sign off prior to system shutdown.  0 disables.")
+  (annoy-delay
+   (integer 60)
+   "The initial delay in seconds after a power failure before warning users to
+get off the system.")
+  (no-logon
+   (enum-no-logon 'disable)
+   "The condition which determines when users are prevented from logging in
+during a power failure.")
+  (kill-delay
+   (integer 0)
+   "If this is non-zero, the apcupsd will continue running after a shutdown
+has been requested, and after the specified time in seconds attempt to kill
+the power.  This is for use on systems where apcupsd cannot regain control
+after a shutdown.")
+
+  ;; Configuration statements for Network Information Server
+  (net-server
+   (boolean #f)
+   "If enabled, a network information server process will be started.")
+  (net-server-ip
+   (string "127.0.0.1")
+   "An IP address on which the NIS server will listen for incoming
+connections.")
+  (net-server-port
+   (integer 3551)
+   "An IP port on which the NIS server will listen for incoming connections.")
+  (net-server-events-file
+   maybe-string
+   "If you want the last few EVENTS to be available over the network by the
+network information server, you must set this to a file name.")
+  (net-server-events-file-max-size
+   (integer 10)
+   "The maximum size of the events file in kilobytes.")
+  ;; Configuration statements used if sharing a UPS with more than one machine
+  (class (enum-class 'standalone)
+    "Normally standalone unless you share an UPS using an APC ShareUPS card.")
+  (mode (enum-mode 'disable)
+    "Normally disable unless you share an UPS using an APC ShareUPS card.")
+  ;; Configuration statements to control apcupsd system logging
+  (stat-time
+   (integer 0)
+   "The time interval in seconds between writing the status file, 0
+disables.")
+  (log-stats
+   (boolean #f)
+   "Also write the stats as a logs.  This generates a lot of output.")
+  (data-time
+   (integer 0)
+   "The time interval in seconds between writing the data records to the log
+file, 0 disables.")
+  (facility
+   maybe-string
+   "The logging facility for the syslog.")
+
+  ;; Event handlers
+  (event-handlers
+   (apcupsd-event-handlers (apcupsd-event-handlers))
+   "Handlers for events produced by apcupsd."
+   empty-serializer))
+
+(define (%apccontrol config)
+  (program-file
+   "apccontrol"
+   (match-record (apcupsd-configuration-event-handlers config)
+       <apcupsd-event-handlers>
+       ( killpower commfailure commok powerout onbattery offbattery mainsback
+         failing timeout loadlimit runlimit doreboot doshutdown annoyme
+         emergency changeme remotedown startselftest endselftest battdetach
+         battattach )
+     #~(begin
+         (use-modules (ice-9 format)
+                      (ice-9 match)
+                      (ice-9 popen)
+                      (srfi srfi-9)
+                      #$@(apcupsd-event-handlers-modules
+                          (apcupsd-configuration-event-handlers config)))
+         ;; Script dir depends on these, and the configuration depends on the
+         ;; script dir.  To sever the cyclic dependency, pass the file names via
+         ;; environment variables.
+         (define conf (getenv "GUIX_APCUPSD_CONF"))
+         (define powerfail-file (getenv "GUIX_APCUPSD_POWERFAIL_FILE"))
+
+         (define (err . args)
+           (apply format (current-error-port) args))
+         (define (wall . args)
+           (system* #$(file-append util-linux "/bin/wall") (apply format #f args)))
+         (define (apcupsd . args)
+           (apply system* #$(file-append apcupsd "/sbin/apcupsd") "-f" conf args))
+         (define (mail-to-root subject body)
+           (let ((port (open-pipe* OPEN_WRITE
+                                   "/run/privileged/bin/sendmail"
+                                   "-F" "apcupsd"
+                                   "root")))
+             (format port "Subject: ~a~%~%~a~&" subject body)
+             (close-pipe port)))
+         (match (cdr (command-line))
+           (((? string? cmd) name connected powered)
+            (let ((connected? (match connected
+                                ("1" #t)
+                                ("0" #f)))
+                  (powered? (match powered
+                              ("1" #t)
+                              ("0" #f))))
+              (match cmd
+                ("killpower"     #$killpower)
+                ("commfailure"   #$commfailure)
+                ("commok"        #$commok)
+	        ("powerout"      #$powerout)
+	        ("onbattery"     #$onbattery)
+	        ("offbattery"    #$offbattery)
+	        ("mainsback"     #$mainsback)
+	        ("failing"       #$failing)
+	        ("timeout"       #$timeout)
+	        ("loadlimit"     #$loadlimit)
+	        ("runlimit"      #$runlimit)
+	        ("doreboot"      #$doreboot)
+	        ("doshutdown"    #$doshutdown)
+	        ("annoyme"       #$annoyme)
+	        ("emergency"     #$emergency)
+	        ("changeme"      #$changeme)
+	        ("remotedown"    #$remotedown)
+	        ("startselftest" #$startselftest)
+	        ("endselftest"   #$endselftest)
+	        ("battdetach"    #$battdetach)
+	        ("battattach"    #$battattach)
+                (_
+                 (err "Unknown event: ~a~%" cmd)
+                 (err "Iff the event was emitted by apcupsd, this is a bug.~%")
+                 (err "Please report to [email-masked].~%")
+                 (exit #f)))))
+           (args
+            (err "Unknown arguments: ~a~%" args)
+            (err "Iff the arguments were passed by apcupsd, this is a bug.~%")
+            (err "Please report to [email-masked].~%")
+            (exit #f)))))))
+
+(define (apcupsd-script-dir config)
+  (computed-file
+   "apcupsd-script-dir"
+   #~(begin
+       (mkdir #$output)
+       (chdir #$output)
+       (symlink #$(%apccontrol config) "apccontrol"))))
+
+(define (apcupsd-config-file config)
+  (let ((run-dir (apcupsd-configuration-run-dir config)))
+    (mixed-text-file
+     "apcupsd.conf"
+     "\
+## apcupsd.conf v1.1 ##
+#
+#  for apcupsd - GNU Guix
+#
+# \"apcupsd\" POSIX config file (generated by apcupsd-service-type)
+"
+     (serialize-configuration config apcupsd-configuration-fields)
+     ;; This one is confusing.  The manual page states:
+     ;;
+     ;; > It must be changed when running more than one copy of apcupsd on the
+     ;; > same computer to control multiple UPSes.
+     ;;
+     ;; However would you not want the lock to be per-device, not per-process?
+     ;; I decided to follow the documentation, but I do not understand why it
+     ;; should be like this.  I do not have multiple UPSes to try.
+     (serialize-string 'lock-dir (string-append run-dir "/lock"))
+     (serialize-string 'power-fail-dir run-dir)
+     (serialize-string 'no-login-dir run-dir)
+     (serialize-string 'stat-file (string-append run-dir "/apcupsd.status"))
+     "SCRIPTDIR " (apcupsd-script-dir config) "\n")))
+
+(define (apcupsd-activation config)
+  (match-record config <apcupsd-configuration> (run-dir)
+    #~(begin
+        (use-modules (guix build utils))
+        (mkdir-p #$(string-append run-dir "/lock")))))
+
+(define (apcupsd-shepherd-services config)
+  (match-record config <apcupsd-configuration>
+                ( apcupsd pid-file debug-level run-dir
+                  shepherd-service-name auto-start?)
+    (let ((config-file (apcupsd-config-file config)))
+      (list
+       (shepherd-service
+        (documentation "Run the apcupsd daemon.")
+        (requirement '(user-processes))
+        (provision (list shepherd-service-name))
+        (auto-start? auto-start?)
+        (start #~(make-forkexec-constructor
+                  '(#$(file-append apcupsd "/sbin/apcupsd")
+                    "-b"                ;do not daemonize
+                    "-f" #$config-file
+                    "-P" #$pid-file
+                    "-d" #$(number->string debug-level))
+                  #:log-file
+                  #$(format #f "/var/log/~a.log" shepherd-service-name)
+                  #:environment-variables
+                  (cons* (string-append "GUIX_APCUPSD_CONF="
+                                        #$config-file)
+                         #$(string-append "GUIX_APCUPSD_POWERFAIL_FILE="
+                                          run-dir "/powerfail")
+                         (default-environment-variables))))
+        (stop #~(make-kill-destructor))
+        (actions (list (shepherd-configuration-action config-file))))))))
+
+(define (apcupsd-pam-extensions config)
+  ;; The apcupsd can be configured to prevent users from logging in on certain
+  ;; conditions.  This is implemented by creation of a "nologin" file, and
+  ;; using a pam nologin module to prevent the login (if the file exists).
+  (define pam-nologin
+    (pam-entry
+     (control "required")
+     (module "pam_nologin.so")
+     (arguments (list (string-append "file="
+                                     (apcupsd-configuration-run-dir config)
+                                     "/nologin")))))
+
+  (list (pam-extension
+         (transformer
+          (lambda (pam)
+            (pam-service
+             (inherit pam)
+             (auth (cons pam-nologin (pam-service-auth pam)))))))))
+
+(define apcupsd-service-type
+  (service-type
+   (name 'apcupsd)
+   (description "Configure and optionally start the apcupsd.")
+   (extensions (list (service-extension activation-service-type
+                                        apcupsd-activation)
+                     (service-extension shepherd-root-service-type
+                                        apcupsd-shepherd-services)
+                     (service-extension pam-root-service-type
+                                        apcupsd-pam-extensions)))
+   (default-value (apcupsd-configuration))))
-- 
2.48.1