Skip to content

Commit

Permalink
scripts/external_apis: add example external protocol script
Browse files Browse the repository at this point in the history
New scripts directory for external_apis, including one integrating
the Enphase IQ Gateway web-API with NUT.

Signed-off-by: Scott Shambarger <[email protected]>
  • Loading branch information
sshambar committed Feb 23, 2025
1 parent e718a1e commit fe0174a
Show file tree
Hide file tree
Showing 10 changed files with 1,244 additions and 2 deletions.
1 change: 1 addition & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ spellcheck spellcheck-interactive:
(cd $(builddir)/scripts/Solaris && $(MAKE) $(AM_MAKEFLAGS) -s $@) || RES=$$? ; \
(cd $(builddir)/scripts/Windows && $(MAKE) $(AM_MAKEFLAGS) -s $@) || RES=$$? ; \
(cd $(builddir)/scripts/devd && $(MAKE) $(AM_MAKEFLAGS) -s $@) || RES=$$? ; \
(cd $(builddir)/scripts/external_apis && $(MAKE) $(AM_MAKEFLAGS) -s $@) || RES=$$? ; \
(cd $(builddir)/scripts/hotplug && $(MAKE) $(AM_MAKEFLAGS) -s $@) || RES=$$? ; \
(cd $(builddir)/scripts/installer && $(MAKE) $(AM_MAKEFLAGS) -s $@) || RES=$$? ; \
(cd $(builddir)/scripts/python && $(MAKE) $(AM_MAKEFLAGS) -s $@) || RES=$$? ; \
Expand Down
2 changes: 2 additions & 0 deletions NEWS.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ https://github.com/networkupstools/nut/milestone/9

- (expected) Bug fixes for fallout possible due to "fightwarn" effort in 2.8.0+

- added `scripts/external_apis` with an example script integrating a
non-native protocol with NUT. [issue #2807]

PLANNED: Release notes for NUT 2.8.3 - what's new since 2.8.2
-------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -5685,6 +5685,7 @@ AC_CONFIG_FILES([
scripts/augeas/nutupssetconf.aug
scripts/avahi/nut.service
scripts/devd/Makefile
scripts/external_apis/Makefile
scripts/hotplug/Makefile
scripts/hotplug/libhidups
scripts/HP-UX/nut.psf
Expand Down
5 changes: 4 additions & 1 deletion docs/nut.dict
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
personal_ws-1.1 en 3288 utf-8
personal_ws-1.1 en 3291 utf-8
AAC
AAS
ABI
Expand Down Expand Up @@ -346,6 +346,7 @@ Eltek
Emilien
Energia
EnergySaving
Enphase's
Erikson
Eriksson
Evgeny
Expand Down Expand Up @@ -1491,6 +1492,7 @@ apcupsd
aphel
api
apinames
apis
appveyor
ar
architecting
Expand Down Expand Up @@ -1887,6 +1889,7 @@ endl
energizerups
energysave
english
enphase
enum
env
envvar
Expand Down
2 changes: 1 addition & 1 deletion scripts/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ EXTRA_DIST = \
Windows/halt.c \
Windows/Makefile

SUBDIRS = augeas devd hotplug installer python systemd udev ufw Solaris Windows upsdrvsvcctl
SUBDIRS = augeas devd hotplug installer python systemd udev ufw Solaris Windows upsdrvsvcctl external_apis

SPELLCHECK_SRC = README.adoc RedHat/README.adoc usb_resetter/README.adoc valgrind/README.adoc

Expand Down
33 changes: 33 additions & 0 deletions scripts/external_apis/Makefile.am
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Network UPS Tools: scripts/external_apis

EXTRA_DIST = \
README.adoc \
enphase/README.adoc \
enphase/enphase-monitor \
enphase/[email protected]

SPELLCHECK_SRC = README.adoc

# NOTE: Due to portability, we do not use a GNU percent-wildcard extension.
# We also have to export some variables that may be tainted by relative
# paths when parsing the other makefile (e.g. MKDIR_P that may be defined
# via expanded $(top_builddir)/install-sh):
#%-spellchecked: % Makefile.am $(top_srcdir)/docs/Makefile.am $(abs_srcdir)/$(NUT_SPELL_DICT)
# +$(MAKE) -s -f $(top_builddir)/docs/Makefile $(AM_MAKEFLAGS) MKDIR_P="$(MKDIR_P)" builddir="$(builddir)" srcdir="$(srcdir)" top_builddir="$(top_builddir)" top_srcdir="$(top_srcdir)" SPELLCHECK_SRC_ONE="$<" SPELLCHECK_SRCDIR="$(srcdir)" SPELLCHECK_BUILDDIR="$(builddir)" $@

# NOTE: Portable suffix rules do not allow prerequisites, so we shim them here
# by a wildcard target in case the make implementation can put the two together.
*-spellchecked: Makefile.am $(top_srcdir)/docs/Makefile.am $(abs_srcdir)/$(NUT_SPELL_DICT)

.sample.sample-spellchecked:
+$(MAKE) -s -f $(top_builddir)/docs/Makefile $(AM_MAKEFLAGS) MKDIR_P="$(MKDIR_P)" builddir="$(builddir)" srcdir="$(srcdir)" top_builddir="$(top_builddir)" top_srcdir="$(top_srcdir)" SPELLCHECK_SRC_ONE="$<" SPELLCHECK_SRCDIR="$(srcdir)" SPELLCHECK_BUILDDIR="$(builddir)" $@

.in.in-spellchecked:
+$(MAKE) -s -f $(top_builddir)/docs/Makefile $(AM_MAKEFLAGS) MKDIR_P="$(MKDIR_P)" builddir="$(builddir)" srcdir="$(srcdir)" top_builddir="$(top_builddir)" top_srcdir="$(top_srcdir)" SPELLCHECK_SRC_ONE="$<" SPELLCHECK_SRCDIR="$(srcdir)" SPELLCHECK_BUILDDIR="$(builddir)" $@

spellcheck spellcheck-interactive spellcheck-sortdict:
+$(MAKE) -f $(top_builddir)/docs/Makefile $(AM_MAKEFLAGS) MKDIR_P="$(MKDIR_P)" builddir="$(builddir)" srcdir="$(srcdir)" top_builddir="$(top_builddir)" top_srcdir="$(top_srcdir)" SPELLCHECK_SRC="$(SPELLCHECK_SRC)" SPELLCHECK_SRCDIR="$(srcdir)" SPELLCHECK_BUILDDIR="$(builddir)" $@

CLEANFILES = *-spellchecked enphase/*-spellchecked

MAINTAINERCLEANFILES = Makefile.in .dirstamp
12 changes: 12 additions & 0 deletions scripts/external_apis/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
NUT external API integration scripts
====================================

These directories hold scripts that integrate NUT with external APIs
not yet natively supported. These may include Rest APIs, Web-based JSON,
or any other protocol that doesn't have a supported subdriver. They
a useful both for adding the integration to an existing install, or
as a starting point for creating new integrations.

- enphase: web-API based integration with Enphase's locally hosted IQ Gateway.
Supports web-based login and token management, and maps JSON data
to files consumed by the dummy-ups driver.
192 changes: 192 additions & 0 deletions scripts/external_apis/enphase/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
Enphase Monitor for dummy-ups
=============================

Enphase Monitor is a bash-script that queries the local IQ Gateway's
API and makes "Grid On/Off" and Battery State-Of-Charge status
available to NUT's dummy-ups driver by updating it's "port" file
(see the dummy-ups(8) man page)

enphase-monitor supports the following:

- auto-login to enlighten.enphaseenergy.com to generate and auto-renew
tokens for local Gateway API access (tokens are cached until expiration)
- retains any (non-generated) values in the "port" file
- gracefully handles split-phase or 3-phase input/output values
- calculates derived values such as battery voltage, runtime and "ups.load"
- handles no-comms with temporary rename of the port file (indicates
"stale data")
- dedicated config-file (for login, API query timing etc.)
- enforces access permissions on files containing secrets
- is fully self-documented (leading comment in the script included below)
- GPLv3 licensed
- minimal requirements: bash, jq, base64 and curl
- includes systemd service for use with dummy-ups
- includes a "TEST" mode that loops through various states and
randomly expires the token

Developed by Scott Shambarger <[email protected]>

Documentation
-------------
The script is self-documented, but the following is the script's leading
comments with installation instructions and configuration reference:
----
Usage: enphase-monitor [ <options> ] -c <config> | <ups>

-c <config> - use named config-file (or set $CONFIG_FILE)
<ups> - use config-file /etc/ups/enphase-<ups>.conf (or set $UPS)

<options> may include:
-d - increase debug to stderr (2+ exposes secrets!)
-h - show help and exit
-s - perform one network check and exit
-v - verbose output
-x - set 'nocomms' and exit"

<config> must contain:

USERNAME=<enphase login>
PASSWORD=<enphase password>
SERIAL=<envoy serial#>
PORT_FILE=<portfile from ups.conf, see below>

and optionally (defaults shown):

DISABLE_METERS= any value to disable power reporting
ENVOY_HOST="envoy.local" ip/hostname of IQ Gateway on local network
STATE_DIR="/var/lib/ups" writable directory for portfile/tokens
POLLFREQ=60 seconds between API queries, min 5
POLLFREQALERT=20 seconds between API queries when on battery, min 5
TOKEN_FILE="enphase-<ups>.token" path defaults to STATE_DIR
LOADKWH=768 max load/1kWh capacity, used for ups.load calculation
0 disables calc (default based on IQ 5P rate 3.84kVA/5kWh)
LOGIN_TIMEOUT=10 timeout (secs) for login/token gen, min 5
API_TIMEOUT=5 timeout (secs) for local ENVOY_HOST api access, min 2

Add section to /etc/ups/ups.conf for your <ups> name (replace <XXX>)

[<ups>]
driver = dummy-ups
port = <STATE_DIR>/<portfile> this should be an absolute path!
mode = dummy-once or name <portfile> with `.dev` extension
desc = "Enphase IQ Gateway"

<portfile> MUST EXIST before running the monitor (to ensure it's running
on the correct machine). The following entries are optional but
used if specified (defaults shown); other non-generated entries are retained.

battery.charge.low: 20
battery.voltage.high: 86.4
battery.voltage.nominal: 76.8
battery.voltage.low: 68.5
device.mfr: Enphase Energy
device.model: IQ Gateway

The monitor uses the enphase <login> + <serial#> to retrieve a long-term
token and saves it in STATE_DIR (token renewal is handled automatically)

The monitor then queries the ENVOY_HOST (local IQ Gateway) API at POLLFREQ
intervals to retrieve the envoy state, and updates <portfile>.
Using values retrieved from the API and settings above, the monitor
calculates the values ups.load, battery.voltage and battery.runtime

enphase-monitor needs to have write access to <portfile>, so usually
upsd hosting <ups> should be on local host, but shared filesystems may
allow upsd to be remote.

NOTE: if connections to ENVOY_HOST fail, <portfile> is renamed
<portfile>-nocomms to trigger dummy-ups to show stale data.
Either filename may exist on startup.

Environment (optional):
CONFIG_FILE - override default <config>
UPS - set a default <ups>
NUT_SYSCONFIG - default <config> directory (/etc/ups)
NUT_LOCALSTATE - default for STATE_DIR (/var/lib/ups)

=== INSTALL ===

Install required support programs: bash, base64, jq, and curl

Create an entry in /etc/ups/ups.conf (as above)

Copy enphase-monitor to some <INSTALL-DIR> (eg /usr/local/libexec)

Create a <config> file with required variables. If only <ups>
used to start the monitor, is looks for `/etc/ups/enphase-<ups>.conf`
Ensure <config> can be read by monitor and is not world readable!

Choose a NUT writable directory for STATE_DIR (default /var/lib/ups),
and create an empty <portfile> there:

$ touch <STATE_DIR>/<portfile>
$ chown <nut-user>:<nut-group> <STATE_DIR>/<portfile>

If using SELinux, ensure NUT's dummy-ups has access to the <portfile>
(even in /var/lib/ups!) by adding a label, eg.

$ semanage fcontext -a -t nut_conf_t <STATE_DIR>/<portfile>
$ restoreconf -F <STATE_DIR>/<portfile>

Create a systemd template file (replace <XXX> items)

--- /etc/systemd/system/[email protected] ---
[Unit]
Description=Enphase API monitor for NUT dummy-ups %I
PartOf=nut-driver.target
Before=nut-driver@%i.service

[Service]
SyslogIdentifier=%N
User=<NUT-USER>
ExecStartPre=<INSTALL-PATH>/enphase-monitor -s %I
ExecStart=<INSTALL-PATH>/enphase-monitor %I
Type=exec
Restart=always
RestartSec=30

[Install]
WantedBy=nut-driver@%i.service
--- end of file ---

Enable the instance for <ups>

$ systemctl daemon-reload
$ systemctl enable nut-driver@<ups>
$ systemctl enable enphase-monitor@<ups>

Restart NUT :)

=== TEST MODE ===

If using the distributed `test.conf`, copy `test-ref.dev` to `test.dev`
and then run:

$ ./enphase-monitor -c test.conf

`test.conf` sets "UPS=test" and "STATE_DIR=." and PORT_FILE="test.dev"
(so token/portfiles are located in the current directory)
It also sets "DEBUG=1" to show debug output (optional), and POLLFREQ
to a few secs.

"TEST" mode will loop (and randomly expire the token):

online -> nocomms -> online -> onbatt -> lowbatt <- <repeat>

A "TEST" mode <config> should set:

TEST=1 <- required for "TEST" mode
TEST_SESS=<json> use {"session_id":"some-value"}
TEST_TOKEN=<web-token> JWT token, should have valid expires!
TEST_RELAY=<json> ivp/ensemble/relay {"mains_oper_state":"@RELAY_STATE@"}
TEST_LIVE=<json> ivp/livedata/status, {"soc":"@BATT_SOC@"}
TEST_REPORTS=<json> ivp/meters/reports
TEST_SECCTRL=<json> ivp/ensemble/secctrl, {"soc_recovery_exit":10}
TEST_INFO=<xml> info.xml

Output from real HTTP requests can be used (use "-d -d" to see output)
for each of those APIs. Any empty TEST_XXXX value simulates a
failed API query.
----
Loading

0 comments on commit fe0174a

Please sign in to comment.