From fe0174a0bde8fdde304f35cf48ac89af757788cb Mon Sep 17 00:00:00 2001 From: Scott Shambarger Date: Sun, 23 Feb 2025 11:42:52 -0800 Subject: [PATCH 01/10] scripts/external_apis: add example external protocol script New scripts directory for external_apis, including one integrating the Enphase IQ Gateway web-API with NUT. Signed-off-by: Scott Shambarger --- Makefile.am | 1 + NEWS.adoc | 2 + configure.ac | 1 + docs/nut.dict | 5 +- scripts/Makefile.am | 2 +- scripts/external_apis/Makefile.am | 33 + scripts/external_apis/README.adoc | 12 + scripts/external_apis/enphase/README.adoc | 192 ++++ scripts/external_apis/enphase/enphase-monitor | 981 ++++++++++++++++++ .../enphase/enphase-monitor@.service | 17 + 10 files changed, 1244 insertions(+), 2 deletions(-) create mode 100644 scripts/external_apis/Makefile.am create mode 100644 scripts/external_apis/README.adoc create mode 100644 scripts/external_apis/enphase/README.adoc create mode 100755 scripts/external_apis/enphase/enphase-monitor create mode 100644 scripts/external_apis/enphase/enphase-monitor@.service diff --git a/Makefile.am b/Makefile.am index 34727853e8..044e1c1e56 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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=$$? ; \ diff --git a/NEWS.adoc b/NEWS.adoc index aa602b1d47..0a37a4d4be 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -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 ------------------------------------------------------------- diff --git a/configure.ac b/configure.ac index f53a069948..d87467365a 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/docs/nut.dict b/docs/nut.dict index 615e6cde86..df31b8f629 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3288 utf-8 +personal_ws-1.1 en 3291 utf-8 AAC AAS ABI @@ -346,6 +346,7 @@ Eltek Emilien Energia EnergySaving +Enphase's Erikson Eriksson Evgeny @@ -1491,6 +1492,7 @@ apcupsd aphel api apinames +apis appveyor ar architecting @@ -1887,6 +1889,7 @@ endl energizerups energysave english +enphase enum env envvar diff --git a/scripts/Makefile.am b/scripts/Makefile.am index 194e95840a..198b88c67c 100644 --- a/scripts/Makefile.am +++ b/scripts/Makefile.am @@ -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 diff --git a/scripts/external_apis/Makefile.am b/scripts/external_apis/Makefile.am new file mode 100644 index 0000000000..bbd3927396 --- /dev/null +++ b/scripts/external_apis/Makefile.am @@ -0,0 +1,33 @@ +# Network UPS Tools: scripts/external_apis + +EXTRA_DIST = \ + README.adoc \ + enphase/README.adoc \ + enphase/enphase-monitor \ + enphase/enphase-monitor@.service + +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 diff --git a/scripts/external_apis/README.adoc b/scripts/external_apis/README.adoc new file mode 100644 index 0000000000..e87bfcfaa8 --- /dev/null +++ b/scripts/external_apis/README.adoc @@ -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. diff --git a/scripts/external_apis/enphase/README.adoc b/scripts/external_apis/enphase/README.adoc new file mode 100644 index 0000000000..7ff6ef7d00 --- /dev/null +++ b/scripts/external_apis/enphase/README.adoc @@ -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 + +Documentation +------------- + +The script is self-documented, but the following is the script's leading +comments with installation instructions and configuration reference: + +---- +Usage: enphase-monitor [ ] -c | + + -c - use named config-file (or set $CONFIG_FILE) + - use config-file /etc/ups/enphase-.conf (or set $UPS) + + 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" + + must contain: + + USERNAME= + PASSWORD= + SERIAL= + PORT_FILE= + +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-.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 name (replace ) + +[] + driver = dummy-ups + port = / this should be an absolute path! + mode = dummy-once or name with `.dev` extension + desc = "Enphase IQ Gateway" + + 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 + 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 . +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 , so usually +upsd hosting should be on local host, but shared filesystems may +allow upsd to be remote. + +NOTE: if connections to ENVOY_HOST fail, is renamed +-nocomms to trigger dummy-ups to show stale data. +Either filename may exist on startup. + +Environment (optional): + CONFIG_FILE - override default + UPS - set a default + NUT_SYSCONFIG - default 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 (eg /usr/local/libexec) + +Create a file with required variables. If only +used to start the monitor, is looks for `/etc/ups/enphase-.conf` +Ensure 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 there: + + $ touch / + $ chown : / + +If using SELinux, ensure NUT's dummy-ups has access to the +(even in /var/lib/ups!) by adding a label, eg. + + $ semanage fcontext -a -t nut_conf_t / + $ restoreconf -F / + +Create a systemd template file (replace items) + + --- /etc/systemd/system/enphase-monitor@.service --- + [Unit] + Description=Enphase API monitor for NUT dummy-ups %I + PartOf=nut-driver.target + Before=nut-driver@%i.service + + [Service] + SyslogIdentifier=%N + User= + ExecStartPre=/enphase-monitor -s %I + ExecStart=/enphase-monitor %I + Type=exec + Restart=always + RestartSec=30 + + [Install] + WantedBy=nut-driver@%i.service + --- end of file --- + +Enable the instance for + + $ systemctl daemon-reload + $ systemctl enable nut-driver@ + $ systemctl enable enphase-monitor@ + +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 <- + +A "TEST" mode should set: + + TEST=1 <- required for "TEST" mode + TEST_SESS= use {"session_id":"some-value"} + TEST_TOKEN= JWT token, should have valid expires! + TEST_RELAY= ivp/ensemble/relay {"mains_oper_state":"@RELAY_STATE@"} + TEST_LIVE= ivp/livedata/status, {"soc":"@BATT_SOC@"} + TEST_REPORTS= ivp/meters/reports + TEST_SECCTRL= ivp/ensemble/secctrl, {"soc_recovery_exit":10} + TEST_INFO= 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. +---- diff --git a/scripts/external_apis/enphase/enphase-monitor b/scripts/external_apis/enphase/enphase-monitor new file mode 100755 index 0000000000..8442c846e7 --- /dev/null +++ b/scripts/external_apis/enphase/enphase-monitor @@ -0,0 +1,981 @@ +#!/usr/bin/env bash +# -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*- +# vim:set ft=sh et sw=2 ts=2: +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Copyright (C) 2025 Scott Shambarger +# +# Enphase Monitor for NUT dummy-ups v0.9.1 +# Author: Scott Shambarger +# +# Enphase Monitor is designed to work with Network UPS Tools +# , accessing Enphase IQ Gateway's +# local API and supplying "Grid On/Off" and Battery State-Of-Charge +# to the dummy-ups driver (v2.8+ recommended) +# +# Usage: enphase-monitor [ ] -c | +# +# -c - use named config-file (or set $CONFIG_FILE) +# - use config-file /etc/ups/enphase-.conf (or set $UPS) +# +# 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" +# +# must contain: +# +# USERNAME= +# PASSWORD= +# SERIAL= +# PORT_FILE= +# +# 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-.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 name (replace ) +# +# [] +# driver = dummy-ups +# port = / # this should be an absolute path! +# mode = dummy-once # or name with `.dev` extension +# desc = "Enphase IQ Gateway" +# +# 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 + 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 . +# 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 , so usually +# upsd hosting should be on local host, but shared filesystems may +# allow upsd to be remote. +# +# NOTE: if connections to ENVOY_HOST fail, is renamed +# -nocomms to trigger dummy-ups to show stale data. +# Either filename may exist on startup. +# +# Environment (optional): +# CONFIG_FILE - override default +# UPS - set a default +# NUT_SYSCONFIG - default 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 (eg /usr/local/libexec) +# +# Create a file with required variables. If only +# used to start the monitor, is looks for `/etc/ups/enphase-.conf` +# Ensure 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 there: +# +# $ touch / +# $ chown : / +# +# If using SELinux, ensure NUT's dummy-ups has access to the +# (even in /var/lib/ups!) by adding a label, eg. +# +# $ semanage fcontext -a -t nut_conf_t / +# $ restoreconf -F / +# +# Create a systemd template file (replace items) +# +# --- /etc/systemd/system/enphase-monitor@.service --- +# [Unit] +# Description=Enphase API monitor for NUT dummy-ups %I +# PartOf=nut-driver.target +# Before=nut-driver@%i.service +# +# [Service] +# SyslogIdentifier=%N +# User= +# ExecStartPre=/enphase-monitor -s %I +# ExecStart=/enphase-monitor %I +# Type=exec +# Restart=always +# RestartSec=30 +# +# [Install] +# WantedBy=nut-driver@%i.service +# --- end of file --- +# +# Enable the instance for +# +# $ systemctl daemon-reload +# $ systemctl enable nut-driver@ +# $ systemctl enable enphase-monitor@ +# +# 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 <- +# +# A "TEST" mode should set: +# +# TEST=1 # <- required for "TEST" mode +# TEST_SESS= # use {"session_id":"some-value"} +# TEST_TOKEN= # JWT token, should have valid expires! +# TEST_RELAY= # ivp/ensemble/relay {"mains_oper_state":"@RELAY_STATE@"} +# TEST_LIVE= # ivp/livedata/status, {"soc":"@BATT_SOC@"} +# TEST_REPORTS= # ivp/meters/reports +# TEST_SECCTRL= # ivp/ensemble/secctrl, {"soc_recovery_exit":10} +# TEST_INFO= # 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. +# +# shellcheck disable=SC2076 + +# install defaults +NUT_SYSCONFIG=${NUT_SYSCONFIG:-/etc/ups} +NUT_LOCALSTATE=${NUT_LOCALSTATE:-/var/lib/ups} + +# these can be changed +ENPHASE_LOGIN="https://enlighten.enphaseenergy.com/login/login.json?" +ENPHASE_TOKENS="https://entrez.enphaseenergy.com/tokens" + +msg() { printf '%s\n' "$*"; } +warn() { msg >&2 "$*"; } +debug() { # [ ] + [[ $DEBUG ]] || return 0 + local p=': ' + [[ $1 =~ ^[1-9]$ ]] && { + (( $1 > DEBUG )) && return + p="$1:" + shift + } + warn "DEBUG$p $*" +} +verbose() { + if [[ $DEBUG ]]; then + warn "$*" + elif [[ $VERBOSE ]]; then + msg "$*" + fi +} +die() { warn "enphase-monitor $UPS: $*"; exit 1; } + +int100() { # = * 100 + LC_NUMERIC=C printf -v _i "%.2f" "${2/,/.}" + printf -v "$1" "%s" "${_i/./}" +} + +fmtfloat() { # + LC_NUMERIC=C printf -v "$2" "%.${1}f" "${3/,/.}" +} + +idiv2float() { # (dest = inta / intb) + local -i _x=${3-} _y=${4-}; local _i _j _s='' + (( _y == 0 )) && { LC_NUMERIC=C printf -v "$2" "%.${1}f" "0"; return; } + (( _x<0 || _y<0 )) && { ((_x<0 && _y<0)) || _s=-; } + _x=${_x/-/}; _y=${_y/-/} + (( _i = _x / _y, _j = (_x % _y) * (10 ** $1) / _y )) || : + printf -v _j "%0${1}d" "$_j" + LC_NUMERIC=C printf -v "$2" "%.${1}f" "$_s$_i.$_j" +} + +get_perms() { # + stat -L "${STAT_ARGS[@]}" "$1" +} + +check_dir_perms() { # + local type=$1 dir=$2 perm + debug 2 "checking directory perms for $dir" + perm=$(get_perms "$dir") || die "Unable to stat $type directory '$dir'" + perm=${perm: -1} + (( ( perm >> 1 ) % 2 == 0 )) || + die "World write permissions on $type directory $dir" + debug 2 " perms $perm ok" +} + +# check permissions (contains secrets) +check_file_perms() { # + local type=$1 file=$2 perm dir + debug 2 "checking file perms for $file" + perm=$(get_perms "$file") || die "Unable to stat $type '$file'" + perm=${perm: -1} + (( ( perm >> 2 ) % 2 == 0 )) || + die "World read permissions exist on $type $file" + debug 2 " perms ok" + dir=${file%/*} + [[ $dir == "$file" ]] && dir=. + check_dir_perms "$type" "$dir" +} + +TEST_CYCLE=init +test_cycle() { + case $TEST_CYCLE in + init) TEST_CYCLE=online ;; + online) TEST_CYCLE=nocomms ;; + nocomms) TEST_CYCLE=online2 ;; + online2) TEST_CYCLE=onbatt ;; + onbatt) TEST_CYCLE=lowbatt ;; + *) TEST_CYCLE=online ;; + esac + debug "TEST_CYCLE now $TEST_CYCLE" +} + +# echos output based on and TEST_CYCLE +gen_test_out() { # + local out relay batt mode + + case $TEST_CYCLE in + onbatt*) + relay=open + batt=90 + ;; + lowbatt*) + relay=open + batt=${REF[batt_low]} + if [[ $batt ]]; then + int100 batt "$batt" + (( batt = (batt - 100) / 100 )) || : + else + batt=10 + fi + ;; + *) + relay=closed + batt=100 + ;; + esac + + if [[ $* =~ "$ENPHASE_LOGIN" ]]; then + out=$TEST_SESS; mode=login + elif [[ $* =~ "$ENPHASE_TOKENS" ]]; then + out=$TEST_TOKEN; mode=tokens + else + # only apply nocomms to envoy queries + [[ $TEST_CYCLE == nocomms ]] && die "Testing nocomms" + + if [[ $* =~ ensemble/relay$ ]]; then + out=${TEST_RELAY/@RELAY_STATE@/$relay}; mode=relay + elif [[ $* =~ livedata/status$ ]]; then + out=${TEST_LIVE/@BATT_SOC@/$batt}; mode=live + elif [[ $* =~ meters/reports$ ]]; then + out=$TEST_REPORTS; mode=reports + elif [[ $* =~ ensemble/secctrl$ ]]; then + out=$TEST_SECCTRL; mode=secctrl + elif [[ $* =~ info\.xml$ ]]; then + out=$TEST_INFO; mode=info + else + die "Unknown test url: $*" + fi + fi + + [[ $out ]] || die "Testing $mode failure" + printf %s "$out" +} + +debug_curl() { # + [[ $DEBUG ]] || return 0 + if (( DEBUG > 1 )); then + debug 2 "curl $*" + else + local p=$* + debug "curl to http${p##*http}" + fi +} + +curl_get() { # + debug_curl -ksSf -X GET "$@" + if [[ $TEST ]]; then + gen_test_out "$*" + else + curl -ksSf -X GET "$@" + fi +} + +envoy_get() { # + [[ $1 ]] || die "envoy_get() requires a token" + local token=$1 url=$2 + [[ $url ]] || die "envoy_get() requires a URL" + curl_get -m "$API_TIMEOUT" -H "Authorization: Bearer $token" \ + -H 'Accept: application/json' "https://$ENVOY_HOST/${url#/}" +} + +curl_post() { # + [[ $1 ]] || die "curl_post() requires a URL" + debug_curl -sSf -X POST "$@" + if [[ $TEST ]]; then + gen_test_out "$*" + else + curl -sSf -X POST "$@" + fi +} + +json() { # + jq 2>/dev/null -r "$2" <<< "$1" +} + +# echo if valid +echo_token_expires() { # + local token=$1 data payload expires now + + debug 2 "parsing token for expires: $token" + data=${token%.*} + data=${data#*.} + [[ $data ]] || die "Failed to parse token" + + payload=$(base64 2>/dev/null -d <<< "${data}==") + [[ $payload ]] || die "Failed to base64 decode token payload" + debug 2 "token payload: $payload" + + expires=$(json "$payload" '.exp') + [[ $expires ]] || die "Token payload missing expires" + + now=$(date +%s) + [[ $now ]] || die "Unable to get current time" + + [[ $TEST ]] && { + # randomly expire token (force re-query) unless initial query + (( RANDOM % 5 == 0 )) && [[ -z ${web_token-} ]] && { + debug "randomly expiring token" + expires=$now + } + } + + [[ $DEBUG ]] && { + local full + if [[ $(uname -s) == Darwin ]]; then + full=$(date 2>/dev/null -r "$expires") + else + full=$(date 2>/dev/null -d "@$expires") + fi + debug "token expires: $expires ($full)" + } + + # token needs at least an hour of life... + (( expires < ( now + 3600 ) )) && { + if (( expires <= now )); then + verbose "Token expired" + else + verbose "Token lifetime less than 1 hour" + fi + return 1 + } + debug 2 "token expires ok" + + printf %s "$expires" +} + +# echo +echo_sid() { + local session sid + + # login + session=$(curl_post \ + -m "$LOGIN_TIMEOUT" \ + -H "Accept: application/json" \ + --data-urlencode "user%5Bemail%5D=$USERNAME" \ + --data-urlencode "user%5Bpassword%5d=$PASSWORD" \ + "$ENPHASE_LOGIN") + [[ $session ]] || die "Enphase login failed for user $USERNAME" + debug 2 "session created: $session" + + sid=$(json "$session" ".session_id") + [[ $sid ]] || die "Unable to retrieve session_id for user $USERNAME" + debug "session_id found: $sid" + + printf %s "$sid" +} + +# echo token or return false +echo_saved_token() { + local token='' + + if [[ $TOKEN_CACHE ]]; then + token=$TOKEN_CACHE + else + [[ -r $TOKEN_FILE ]] || return + + check_file_perms "token" "$TOKEN_FILE" + + debug "loading token from $TOKEN_FILE" + read -d '' -r token < "$TOKEN_FILE" || : + [[ $token ]] || return + fi + [[ $(echo_token_expires "$token") ]] || return + printf %s "$token" +} + +save_token() { # + local val=$1 dir + + dir=${TOKEN_FILE%/*} + [[ $dir ]] || dir=/ + [[ -d $dir && -w $dir ]] || die "Token directory '$dir' not writable" + check_dir_perms "token" "$dir" + + debug 2 "Saving token $val to $TOKEN_FILE" + rm -f "$TOKEN_FILE" + printf '' > "$TOKEN_FILE" || + die "Unable to create token file '$TOKEN_FILE'" + chmod o-rwx "$TOKEN_FILE" || + die "Unable to remove world permissions from token file $TOKEN_FILE" + printf %s "$val" > "$TOKEN_FILE" + debug "Saved new token to $TOKEN_FILE" + return 0 +} + +echo_token() { + local sid web_token + + echo_saved_token && return + [[ -f $TOKEN_FILE ]] && rm -f "$TOKEN_FILE" + + # if login failure, fail + sid=$(echo_sid) || return + + # acquire long-term token + web_token=$(curl_post \ + -m "$LOGIN_TIMEOUT" \ + --json "{\"session_id\": \"$sid\", \"serial_num\": \"$SERIAL\", \"username\": \"$USERNAME\"}" \ + "$ENPHASE_TOKENS") + # token query failure means serial# invalid, fail + [[ $web_token ]] || + die "Failed to acquire web_token for user $USERNAME, serial# $SERIAL" + debug "web_token retrieved" + + # if new token not valid, fail + [[ $(echo_token_expires "$web_token") ]] || + die "New token invalid, verify serial# $SERIAL" + + save_token "$web_token" + + printf %s "$web_token" +} + +# CONSTANTS + +# nut fields to DS key mapping +declare -rA FM=([ups.status]=status [battery.charge]=batt_soc) +# nut group to DS key mapping +declare -rA FGM=([input]=input [output]=output [battery]=battery) +# nut fields to REF key mapping +declare -rA RM=([battery.charge.low]=batt_low [battery.capacity]=capacity + [battery.charge.restart]=batt_restart [battery.type]=batt_type + [battery.voltage.high]=batt_high [battery.voltage.low]=batt_min + [battery.voltage.nominal]=batt_nom [device.serial]=serial + [device.mfr]=mfr [device.model]=model [device.type]=dev_type + [ups.serial]=userial [ups.mfr]=umfr [ups.model]=umodel + [ups.firmware]=software [input.phases]=phases + [output.phases]=phases + [output.current.high.critical]=max_current) +# used for power meter fields; METER_KEYS indexes used in update_portfile +declare -r METER_KEYS=(realpower power voltage powerfactor frequency) +# METER_FIELDS must map to METER_KEYS +declare -r METER_FIELDS="currW, apprntPwr, rmsVoltage, pwrFactor, freqHz" + +# LiFePO4 SOC to voltage map (in 10% increments, >100% charging) +declare -ra LFP=(250 290 310 320 325 330 335 340 345 350 360 365) + +# sets STATE, NDS +query_envoy() { + local token data item live alive=() + + token=$(echo_token) || exit + TOKEN_CACHE=$token + [[ $token ]] || return 0 + + STATE=nocomms + + [[ $NOCOMMS ]] && return + + # relay appears to be the only reliable way to see if grid is hot + # ("live" query doesn't show a change in main_relay_state) + data=$(envoy_get "$token" "ivp/ensemble/relay") && [[ $data ]] && { + item=$(json "$data" ".mains_oper_state") + debug "mains operational state: $item" + case $item in + '') warn "envoy relay API missing mains_oper_state" ;; + closed) STATE=online ;; + open) STATE=onbatt ;; + *) warn "envoy relay API unknown mains_oper_state '$item'" ;; + esac + } + [[ $STATE == nocomms ]] && return + + # livedata if fast + data=$(envoy_get "$token" "ivp/livedata/status") && [[ $data ]] && { + live=$(json "$data" ".meters | { soc, phase_count, enc_agg_energy } | [.[]] | @csv") + # some values used in INIT + [[ $live ]] && IFS=, read -r -a alive <<< "$live" + [[ ${alive[0]} != null ]] && NDS[batt_soc]=${alive[0]} + } + + [[ $INIT ]] || { + # just try this once... + INIT=1 + REF[serial]=$SERIAL + REF[userial]=$SERIAL + data=$(envoy_get "$token" "info.xml") && [[ $data ]] && { + while read -r item; do + case $item in + ""*"") + item=${item#*} + REF[software]=${item%*} + ;; + esac + done <<< "$data" + } + # get batt_restart for runtime calc + data=$(envoy_get "$token" "ivp/ensemble/secctrl") && [[ $data ]] && { + item=$(json "$data" ".soc_recovery_exit") + [[ $item != null ]] && REF[batt_restart]=$item + } + REF[phases]='' + [[ ${#alive[*]} ]] && { + [[ ${alive[1]} == 3 ]] && REF[phases]=3 + [[ ${alive[2]} != null ]] && REF[capacity]=${alive[2]} + } + } + + # TODO with meters off, we could still get power from livedata + [[ $DISABLE_METERS ]] || { + # meters is slow, so can be disabled if values aren't wanted + data=$(envoy_get "$token" "ivp/meters/reports") && [[ $data ]] && { + local phase key=input vals + for meter in net total; do + vals=$(json "$data" ".[] | select(.reportType == \"${meter}-consumption\") | .cumulative, .lines[] | { $METER_FIELDS } | [.[]] | @csv") + [[ $vals ]] && { + for (( phase=0; phase <= ${REF[phases]:-0}; phase++ )); do + read -r item && NDS[$key.L$phase]=$item + done <<< "$vals" + } + key=output + done + NDS[battery]=$(json "$data" ".[] | select(.reportType == \"storage\") | .cumulative | { $METER_FIELDS } | [.[]] | @csv") + } + } + + return 0 +} + +parse_portline() { # + local k=${1%%:*} v=${1#*:}; v=${v## } + [[ ${FM[$k]} ]] && DS[${FM[$k]}]=$v + [[ ${RM[$k]} ]] && REF[${RM[$k]}]=$v +} + +# uses STATE, sets NDS[status] +check_status() { + case $STATE in + online*) NDS[status]=OL ;; + onbatt*) NDS[status]=OB ;; + nocomms*) NDS[status]='' ;; + esac + [[ ${NDS[batt_soc]} && ${REF[batt_low]} && ${NDS[status]} ]] && { + local soc low + int100 soc "${NDS[batt_soc]}"; int100 low "${REF[batt_low]}" + (( soc < low )) && NDS[status]+=" LB" + } +} + +update_portfile() { + local change line key lines=() + + # check for changes + check_status + + debug "checking if portfile needs updating" + [[ $DEBUG ]] && debug 3 "$(declare -p NDS)" + + [[ ${NDS[status]} != "${DS[status]}" ]] && { + if [[ ${DS[status]} && -z ${NDS[status]} ]]; then + verbose "Communication lost, hiding portfile" + [[ -f $PORT_FILE ]] && { + mv "$PORT_FILE" "${PORT_FILE}-nocomms" || + die "Unable to disable port file $PORT_FILE" + } + elif [[ ${NDS[status]} && -z ${DS[status]} ]]; then + verbose "Communication restored, restoring portfile" + [[ -f ${PORT_FILE}-nocomms ]] && { + mv "${PORT_FILE}-nocomms" "$PORT_FILE" || + die "Unable to enable port file $PORT_FILE" + } + fi + [[ ${NDS[status]} ]] && verbose "ups.status now ${NDS[status]}" + DS[status]=${NDS[status]} + change=1 + } + # if port file disabled, we're done + [[ ${DS[status]} ]] || return 0 + + debug 2 "checking for changed values" + for key in "${!NDS[@]}"; do + [[ ${NDS[$key]} == "${DS[$key]}" ]] && continue + debug 2 " new $key ${NDS[$key]}" + DS[$key]=${NDS[$key]} + change=1 + done + [[ $change ]] || return 0 + + verbose "Updating $PORT_FILE" + [[ -w $PORT_FILE ]] || die "Port file '$PORT_FILE' missing/write-protected!" + local nk + while read -r line; do + nk=${line%%:*} + [[ ${FM[$nk]} ]] && continue + # keep any REF user may configure + case $nk in + battery.charge.low|*.mfr|*.model) : ;; + battery.voltage.high|battery.voltage.nominal|battery.voltage.low) : ;; + ups.load) continue ;; # calculated + *) [[ ${RM[$nk]} ]] && continue ;; # don't parse other REFs + esac + parse_portline "$line" + [[ ${RM[$nk]} ]] && continue + for key in "${!FGM[@]}"; do + case $nk in "$key"*) continue 2 ;; esac + done + lines+=("$line") + done < "$PORT_FILE" + + [[ $DEBUG ]] && debug 3 "$(declare -p DS REF)" + + for nk in "${!FM[@]}"; do + key=${FM[$nk]}; [[ ${DS[$key]} ]] && lines+=("$nk: ${DS[$key]}") + done + for nk in "${!RM[@]}"; do + key=${RM[$nk]}; [[ ${REF[$key]} ]] && lines+=("$nk: ${REF[$key]}") + done + + local i v p phase pfx val outp soc vals=() + for nk in "${!FGM[@]}"; do + key=${FGM[$nk]} + case $key in + input|output) + for (( phase=0; phase <= ${REF[phases]:-0}; phase++ )); do + p='' v='' pfx=$nk + (( phase > 0 )) && pfx+=".L$phase" + IFS=, read -r -a vals <<< "${DS[$key.L${phase}]}" + # loop over meter keys + for (( i=0; i<${#METER_KEYS[*]}; i++ )); do + val=${vals[i]} + # treat nulls as 0 + [[ $val == null ]] && val=0 + if (( i == 3 )); then # pwrFactor + fmtfloat 2 val "$val" + else + fmtfloat 1 val "$val" + fi + [[ ${METER_KEYS[i]} ]] && { + if (( phase > 0 && i == 2 )); then + # phase voltage is special + lines+=("${pfx}-N.${METER_KEYS[i]}: $val") + else + lines+=("${pfx}.${METER_KEYS[i]}: $val") + fi + } + # enphase cumulative current is 2x, calc real + (( i == 0 )) && { + int100 p "$val" + # save for load calc + [[ $phase == 0 && $key == output ]] && outp=$p + } + (( i == 2 )) && int100 v "$val" + done + [[ $p && $v ]] && { + idiv2float 2 val "$p" "$v" + lines+=("${pfx}.current: $val") + } + done + ;; + battery) + IFS=, read -r -a vals <<< "${DS[$key]}" + int100 p "${vals[0]}" + int100 v "${vals[2]}" + idiv2float 2 val "$p" "$v" + lines+=("$nk.current: $val") + ;; + esac + done + + # batt voltage based on SOC and voltage.high + [[ ${NDS[batt_soc]} && ${REF[batt_high]} ]] && { + # cell voltage not in the API, so calc from known LiFePO4 charts + int100 soc "${NDS[batt_soc]}" + # range limit + (( (soc>11000) ? (soc=11000) : ( (soc<0) ? (soc=0) : 0 ) )) || : + # calc LFP index + (( i = (soc > 10000) ? 10 : (soc / 1000) )) || : + # voltage = base + % to next base + val=${LFP[i]} + (( val += (LFP[i+1] - val) * (soc - (i * 1000)) / 1000 )) || : + # adjust based on high voltage == 100% SOC + int100 v "${REF[batt_high]}"; (( val = (val * v) / LFP[10] )) || : + idiv2float 1 val "$val" 100 + lines+=("battery.voltage: $val") + } + + # load calc if output.power and capacity available + [[ ${REF[capacity]} && $outp ]] && { + v=${REF[capacity]} + # runtime based on (100 - batt_restart) * (seconds at power) + [[ ${NDS[batt_soc]} ]] && { + int100 p "${REF[batt_restart]}" + int100 soc "${NDS[batt_soc]}" + if (( outp <= 0 )); then + val=172800 + elif (( soc < p )); then + val=0 + else + # runtime = (% capacity) / power (in secs) + (( val = ((soc - p) * v * 36) / outp )) || : + # 2 day max + (( val > 172800 ? (val = 172800) : 0 )) || : + fi + lines+=("battery.runtime: $val") + } + (( ${LOADKWH:-0} > 0 )) && { + (( v = LOADKWH * (v / 10), outp *= 100 )) + idiv2float 1 val "$outp" "$v" + lines+=("ups.load: $val") + } + } + + [[ $DEBUG ]] && debug 3 "new file: ${lines[*]}" + local IFS=$'\n' + printf '%s\n' "${lines[*]}" > "$PORT_FILE" || + die "Unable to update PORT_FILE $PORT_FILE" +} + +# sets NDS +read_portfile() { + local file=$PORT_FILE + [[ -r $file ]] || { + # check for disabled + file="${PORT_FILE}-nocomms" + [[ -r $file ]] || die "Port file '$PORT_FILE' unreadable" + } + debug "Reading portfile '$file'" + local line + while read -r line; do + parse_portline "$line" + done < "$file" + [[ $DEBUG ]] && debug 3 "$(declare -p DS REF)" + # if nocomms, set [status] + [[ -r $PORT_FILE ]] || DS[status]='' +} + +main_loop() { + local -A DS NDS REF=([batt_low]="20" [batt_type]=LFP [batt_nom]="76.8" + [batt_high]="86.4" [batt_min]="68.5" [dev_type]="ups" + [batt_restart]=10 + [mfr]="Enphase Energy" [model]="IQ Gateway" + [umfr]="Enphase Energy" [umodel]="IQ Gateway") + local STATE INIT + local -i delay + + read_portfile + debug "initial status: ${DS[status]}" + + while :; do + [[ $TEST ]] && test_cycle + + query_envoy + + debug "current state: $STATE" + + update_portfile + + [[ $NOCOMMS || $SINGLE ]] && break + + # adaptive delay + delay=$POLLFREQ + case $STATE in + onbatt|nocomms) delay=$POLLFREQALERT ;; + esac + debug "delay: $delay" + sleep $delay + done +} + +gen_token_file() { + if [[ $TOKEN_FILE ]]; then + [[ ${TOKEN_FILE#/} == "$TOKEN_FILE" ]] && + TOKEN_FILE="$STATE_DIR/$TOKEN_FILE" + else + TOKEN_FILE="$STATE_DIR/enphase-${UPS}.token" + fi + debug "token file is '$TOKEN_FILE'" +} + +read_config() { + + [[ $CONFIG_FILE ]] || { + [[ $UPS ]] || usage + CONFIG_FILE=${CONFIG_FILE:-${DEF_CONFIG//"@UPS@"/$UPS}} + } + + [[ -r $CONFIG_FILE ]] || die "Unable to read config '$CONFIG_FILE'" + + check_file_perms config "$CONFIG_FILE" + + # read config + verbose "Reading config file $CONFIG_FILE" + bash -n "$CONFIG_FILE" || die "Config $CONFIG_FILE cannot be parsed" + # shellcheck disable=SC1090 + . "$CONFIG_FILE" + + # validate config + local var fail val + (( POLLFREQ < 5 )) && POLLFREQ=5 + (( POLLFREQALERT < 5 )) && POLLFREQALERT=5 + (( LOGIN_TIMEOUT < 5 )) && LOGIN_TIMEOUT=5 + (( API_TIMEOUT < 2 )) && API_TIMEOUT=2 + debug "config values:" + for var in USERNAME PASSWORD SERIAL ENVOY_HOST STATE_DIR PORT_FILE \ + POLLFREQ POLLFREQALERT LOGIN_TIMEOUT API_TIMEOUT; do + val=${!var-} + [[ $val ]] || fail+="$var " + [[ $DEBUG ]] || continue + if [[ $var == PASSWORD ]]; then + (( DEBUG > 1 )) && { debug 2 " $var=$val"; continue; } + val="" + fi + debug " $var=$val" + done + [[ $fail ]] && die "Config $CONFIG_FILE requires values for $fail" + [[ -d $STATE_DIR && -w $STATE_DIR ]] || + die "STATE_DIR '$STATE_DIR' not writable!" + check_dir_perms "state" "$STATE_DIR" + + gen_token_file +} + +check_portfile() { + [[ ${PORT_FILE#/} == "$PORT_FILE" ]] && + PORT_FILE="$STATE_DIR/$PORT_FILE" + + debug "portfile is '$PORT_FILE'" + + local dir=${PORT_FILE%/*} + [[ $dir ]] || dir=/ + [[ -d $dir && -w $dir ]] || die "Port file directory '$dir' not writable" +} + +check_requires() { + local exe fail + (( BASH_VERSINFO[0] < 4 )) && die "Bash v4+ required (for assoc arrays)" + for exe in head stat base64 curl jq date chmod uname; do + command >/dev/null -v "$exe" || fail+="$exe " + done + [[ $fail ]] && die "Unable to find required programs in PATH: $fail" +} + +usage() { + local rc=2 out=warn + + case ${1-} in + '') ;; + 0) rc=0; out=msg ;; + *) warn "Unknown option: '$1'" ;; + esac + + $out "Usage: ${0##*/} [ ] -c | " + $out " -c - use named config-file (or set \$CONFIG_FILE)" + $out " - use config-file ${DEF_CONFIG//@UPS@/} (or set \$UPS)" + $out + $out " may include:" + $out " -d - increase debug to stderr (2+ exposes secrets!)" + $out " -h - show help and exit" + $out " -s - perform one network check and exit" + $out " -v - verbose output" + $out " -x - set 'nocomms' and exit" + + exit $rc +} + +main() { + # global state + local VERBOSE DEBUG TOKEN_CACHE STAT_ARGS=(-c %a) NOCOMMS SINGLE + local DEF_CONFIG="$NUT_SYSCONFIG/enphase-@UPS@.conf" + # config + local USERNAME PASSWORD SERIAL PORT_FILE TOKEN_FILE DISABLE_METERS + local ENVOY_HOST="envoy.local" STATE_DIR="$NUT_LOCALSTATE" + local -i POLLFREQ=60 POLLFREQALERT=20 LOADKWH=768 + local -i LOGIN_TIMEOUT=10 API_TIMEOUT=5 + + check_requires + + [[ $(uname -s) == Darwin ]] && STAT_ARGS=(-f %p) + + while [[ $1 ]]; do + case $1 in + -c) + shift + [[ $1 ]] || usage + CONFIG_FILE=$1 + ;; + -d) [[ $DEBUG ]] && { (( DEBUG++ )) || :; } || DEBUG=1 ;; + -h|--help) usage 0 ;; + -s) SINGLE=1 ;; + -v) VERBOSE=1 ;; + -x) NOCOMMS=1 ;; + -*) usage "$1" ;; + *) UPS=$1; break ;; + esac + shift + done + + read_config + + [[ $UPS ]] || { + warn "UPS must be set (env, or as argument)" + usage + } + + check_portfile + + main_loop +} + +main "$@" diff --git a/scripts/external_apis/enphase/enphase-monitor@.service b/scripts/external_apis/enphase/enphase-monitor@.service new file mode 100644 index 0000000000..b579812635 --- /dev/null +++ b/scripts/external_apis/enphase/enphase-monitor@.service @@ -0,0 +1,17 @@ +[Unit] +Description=Enphase API monitor for NUT dummy-ups %I +PartOf=nut-driver.target +Before=nut-driver@%i.service + +[Service] +SyslogIdentifier=%N +User=nut +# replace -s with -x to start in nocomms (no network apis) +ExecStartPre=/usr/local/libexec/enphase-monitor -s %I +ExecStart=/usr/local/libexec/enphase-monitor %I +Type=exec +Restart=always +RestartSec=30 + +[Install] +WantedBy=nut-driver@%i.service From 3306512624c8048e17cc06b511b0e815e5402aa0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 24 Feb 2025 10:59:36 +0100 Subject: [PATCH 02/10] scripts/external_apis/README.adoc, scripts/external_apis/enphase/README.adoc: adjust wording in layman English parts of the text [#2813] Note that scripts/external_apis/enphase/README.adoc is not spell-checked because it includes text from a bash script and its many variable names. Signed-off-by: Jim Klimov --- scripts/external_apis/README.adoc | 17 ++++++++++------- scripts/external_apis/enphase/README.adoc | 22 +++++++++++----------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/scripts/external_apis/README.adoc b/scripts/external_apis/README.adoc index e87bfcfaa8..0f56ad4a19 100644 --- a/scripts/external_apis/README.adoc +++ b/scripts/external_apis/README.adoc @@ -1,12 +1,15 @@ 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 +These directories hold scripts that help integrate NUT with external APIs +not yet natively supported. These may include REST API, Web-based JSON, +or any other protocol that doesn't have a supported NUT driver but a +prototype to interact with the device can be easily scripted. They +are 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. +- `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 linkman:dummy-ups[8] driver. diff --git a/scripts/external_apis/enphase/README.adoc b/scripts/external_apis/enphase/README.adoc index 7ff6ef7d00..14e05eb4ae 100644 --- a/scripts/external_apis/enphase/README.adoc +++ b/scripts/external_apis/enphase/README.adoc @@ -1,26 +1,26 @@ 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) +The 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 its "port" file +(see the linkman:dummy-ups[8] man page). -enphase-monitor supports the following: +The `enphase-monitor` script supports the following: - - auto-login to enlighten.enphaseenergy.com to generate and auto-renew + - 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" + - 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.) + - dedicated configuration 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 + - minimal requirements: `bash`, `jq`, `base64` and `curl` + - includes a sample instantiated systemd service for use with `dummy-ups` - includes a "TEST" mode that loops through various states and randomly expires the token @@ -30,7 +30,7 @@ Documentation ------------- The script is self-documented, but the following is the script's leading -comments with installation instructions and configuration reference: +comments block with installation instructions and configuration reference: ---- Usage: enphase-monitor [ ] -c | From b2824a6bd10aedd00a5dd40771888b489bd7837c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 24 Feb 2025 11:21:14 +0100 Subject: [PATCH 03/10] configure.ac, scripts/external_apis/*: convert enphase script and unit sources into .in templates [#2813] Signed-off-by: Jim Klimov --- configure.ac | 4 +++- scripts/external_apis/Makefile.am | 8 +++++--- scripts/external_apis/enphase/.gitignore | 2 ++ .../enphase/{enphase-monitor => enphase-monitor.in} | 0 ...phase-monitor@.service => enphase-monitor@.service.in} | 0 5 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 scripts/external_apis/enphase/.gitignore rename scripts/external_apis/enphase/{enphase-monitor => enphase-monitor.in} (100%) rename scripts/external_apis/enphase/{enphase-monitor@.service => enphase-monitor@.service.in} (100%) diff --git a/configure.ac b/configure.ac index d87467365a..68128dea1f 100644 --- a/configure.ac +++ b/configure.ac @@ -5686,6 +5686,7 @@ AC_CONFIG_FILES([ scripts/avahi/nut.service scripts/devd/Makefile scripts/external_apis/Makefile + scripts/external_apis/enphase/enphase-monitor@.service scripts/hotplug/Makefile scripts/hotplug/libhidups scripts/HP-UX/nut.psf @@ -5739,9 +5740,10 @@ m4_foreach_w([SCRIPTFILE], [ scripts/HP-UX/postinstall scripts/RedHat/upsd scripts/RedHat/upsmon + scripts/augeas/gen-nutupsconf-aug.py + scripts/external_apis/enphase/enphase-monitor scripts/python/app/NUT-Monitor-py2gtk2 scripts/python/app/NUT-Monitor-py3qt5 - scripts/augeas/gen-nutupsconf-aug.py scripts/python/module/test_nutclient.py scripts/upsdrvsvcctl/nut-driver-enumerator.sh scripts/upsdrvsvcctl/upsdrvsvcctl diff --git a/scripts/external_apis/Makefile.am b/scripts/external_apis/Makefile.am index bbd3927396..32738c6b52 100644 --- a/scripts/external_apis/Makefile.am +++ b/scripts/external_apis/Makefile.am @@ -3,8 +3,8 @@ EXTRA_DIST = \ README.adoc \ enphase/README.adoc \ - enphase/enphase-monitor \ - enphase/enphase-monitor@.service + enphase/enphase-monitor.in \ + enphase/enphase-monitor@.service.in SPELLCHECK_SRC = README.adoc @@ -28,6 +28,8 @@ SPELLCHECK_SRC = README.adoc 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 +CLEANFILES = *-spellchecked enphase/*-spellchecked \ + enphase/enphase-monitor \ + enphase/enphase-monitor@.service MAINTAINERCLEANFILES = Makefile.in .dirstamp diff --git a/scripts/external_apis/enphase/.gitignore b/scripts/external_apis/enphase/.gitignore new file mode 100644 index 0000000000..637b3c9012 --- /dev/null +++ b/scripts/external_apis/enphase/.gitignore @@ -0,0 +1,2 @@ +enphase-monitor@.service +enphase-monitor diff --git a/scripts/external_apis/enphase/enphase-monitor b/scripts/external_apis/enphase/enphase-monitor.in similarity index 100% rename from scripts/external_apis/enphase/enphase-monitor rename to scripts/external_apis/enphase/enphase-monitor.in diff --git a/scripts/external_apis/enphase/enphase-monitor@.service b/scripts/external_apis/enphase/enphase-monitor@.service.in similarity index 100% rename from scripts/external_apis/enphase/enphase-monitor@.service rename to scripts/external_apis/enphase/enphase-monitor@.service.in From dad96022a7828786d07cf53614579a658edb7a55 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 24 Feb 2025 12:35:10 +0100 Subject: [PATCH 04/10] NEWS.adoc, configure.ac, docs/configure.txt, docs/nut.dict, scripts/external_apis/Makefile.am: introduce `configure --enable-extapi-enphase` installation [#2813] Signed-off-by: Jim Klimov --- NEWS.adoc | 3 ++- configure.ac | 33 +++++++++++++++++++++++++++++++ docs/configure.txt | 25 +++++++++++++++++++++++ docs/nut.dict | 3 ++- scripts/external_apis/Makefile.am | 18 +++++++++++++++++ 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 85bbdeecda..8aa9886c9e 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -42,7 +42,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] + non-native protocol with NUT; that example can be installed using + `configure --enable-extapi-enphase`. [issue #2807, PR #2813] PLANNED: Release notes for NUT 2.8.3 - what's new since 2.8.2 ------------------------------------------------------------- diff --git a/configure.ac b/configure.ac index 68128dea1f..780361a133 100644 --- a/configure.ac +++ b/configure.ac @@ -5465,6 +5465,39 @@ dnl Use these at best for tests (e.g. nutconf), not production code: AC_DEFINE_UNQUOTED([ABS_TOP_SRCDIR], ["${ABS_TOP_SRCDIR}"], [NUT source directory when the build was configured]) AC_DEFINE_UNQUOTED([ABS_TOP_BUILDDIR], ["${ABS_TOP_BUILDDIR}"], [NUT build directory when the build was configured]) +AC_MSG_CHECKING([whether to install External API integration script: Enphase Monitor]) +nut_with_extapi_enphase="no" +AC_ARG_ENABLE([extapi-enphase], + AS_HELP_STRING([--enable-extapi-enphase=(yes|auto|no)], [Enable installation of integration script for External API: Enphase Monitor (default: no)]), +[ + case "${enableval}" in + yes|"") + nut_enable_extapi_enphase="auto" + ;; + auto|no) + nut_enable_extapi_enphase="${enableval}" + ;; + *) + AC_MSG_ERROR([Unexpected argument for --enable-extapi-enphase=${enableval}]) + ;; + esac +], []) + +if test x"$nut_enable_extapi_enphase" != xno ; then + dnl Depends on usability of further programs and bash version + dnl Luckily the script checks that itself, before handling CLI args + /usr/bin/env bash "${ABS_TOP_SRCDIR}/scripts/external_apis/enphase/enphase-monitor.in" --help >/dev/null \ + && nut_can_extapi_enphase="yes" \ + || nut_can_extapi_enphase="no" + + if test x"$nut_enable_extapi_enphase" = xyes && x"$nut_can_extapi_enphase" = xno ; then + AC_MSG_ERROR([Caller required installation --enable-extapi-enphase but the system can not run it]) + fi + nut_enable_extapi_enphase="${nut_can_extapi_enphase}" +fi +AC_MSG_RESULT([${nut_enable_extapi_enphase}]) +AM_CONDITIONAL(ENABLE_EXTAPI_ENPHASE, test x"$nut_enable_extapi_enphase" = x"yes") + AC_MSG_CHECKING([whether to customize ${TOP_BUILDDIR}/scripts/systemd/nut-common-tmpfiles.conf.in for this system]) dnl TOTHINK: Some distributions make the directories below owned dnl by "root:${RUN_AS_GROUP}" with 77x permissions. Is it safer?.. diff --git a/docs/configure.txt b/docs/configure.txt index 260a452b59..7f0d3e9105 100644 --- a/docs/configure.txt +++ b/docs/configure.txt @@ -619,6 +619,31 @@ Change the facility used when writing to the log file. Read the man page for `openlog` to get some idea of what's available on your system. Default is `LOG_DAEMON`. +External API integration scripts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes as a developer or user you need to interact with a device for +which a "proper" NUT driver does not yet exist (or is not in your version), +but some proof-of-concept script can be good enough to collect some data. + +In some cases, an UPS does not support local monitoring at all, but has +a network port for cloud-based monitoring through its vendor's portal. + +Such data can be converted and fed into the NUT `dummy-ups` driver, and so +represented in the NUT ecosystem, by rewriting the "sequence" file whose +contents it processes in a loop (see linkman:dummy-ups[8] for more details). + +NUT provides sample scripts for such integration, which can be used if you +have a suitable use-case, or provide inspiration for you to begin experiments +with a new device and (as often happens) a shell or Python script polling +it for information. For more details, see `scripts/external_apis` in NUT +sources (and pull requests with more integrations would be welcome there). + + --enable-extapi-enphase=(yes|auto|no) + +Enable installation of integration script for External API: Enphase Monitor +(default: no) + Installation directories ------------------------ diff --git a/docs/nut.dict b/docs/nut.dict index 60e322efb1..bdee4be484 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3292 utf-8 +personal_ws-1.1 en 3293 utf-8 AAC AAS ABI @@ -1916,6 +1916,7 @@ execfuse executables executeCommand execve +extapi extern externalConsole extradata diff --git a/scripts/external_apis/Makefile.am b/scripts/external_apis/Makefile.am index 32738c6b52..014f4cdee9 100644 --- a/scripts/external_apis/Makefile.am +++ b/scripts/external_apis/Makefile.am @@ -8,6 +8,24 @@ EXTRA_DIST = \ SPELLCHECK_SRC = README.adoc +# Handle optional installation: +if ENABLE_EXTAPI_ENPHASE + +extapi_enphase_datadir = @datadir@/external_apis/enphase +extapi_enphase_data_DATA = enphase/README.adoc + +extapi_enphase_execdir = @libexec@ +extapi_enphase_exec_SCRIPTS = enphase/enphase-monitor + +if HAVE_SYSTEMD + +systemdsystemunit_DATA = \ + enphase/enphase-monitor@.service + +endif HAVE_SYSTEMD + +endif ENABLE_EXTAPI_ENPHASE + # 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 From c6ed2f942bb5c4f48cbe01919f8eb04c1b332aa2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 24 Feb 2025 12:43:30 +0100 Subject: [PATCH 05/10] scripts/external_apis/enphase/enphase-monitor@.service.in: add (C) header, parameterize installed script location and NUT run-time user [#2813] Signed-off-by: Jim Klimov --- .../enphase/enphase-monitor@.service.in | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/scripts/external_apis/enphase/enphase-monitor@.service.in b/scripts/external_apis/enphase/enphase-monitor@.service.in index b579812635..4de59db232 100644 --- a/scripts/external_apis/enphase/enphase-monitor@.service.in +++ b/scripts/external_apis/enphase/enphase-monitor@.service.in @@ -1,15 +1,31 @@ +# Network UPS Tools (NUT) systemd integration +# Copyright (C) 2025 by Scott Shambarger +# Copyright (C) 2025 by NUT contributors +# Distributed under the terms of GPLv3+ +# See https://networkupstools.org/ +# and https://github.com/networkupstools/nut/ + [Unit] Description=Enphase API monitor for NUT dummy-ups %I PartOf=nut-driver.target +# NOTE: Works with individual service instances prepared by nut-driver-enumerator Before=nut-driver@%i.service [Service] SyslogIdentifier=%N -User=nut -# replace -s with -x to start in nocomms (no network apis) -ExecStartPre=/usr/local/libexec/enphase-monitor -s %I -ExecStart=/usr/local/libexec/enphase-monitor %I +# NOTE: Does not have to be same run-time account as NUT daemons, just the +# files created by it should be readable by the dummy-ups daemon. For more +# secured installations, customize this via a drop-in systemd file with a +# different dedicated account: +User=@RUN_AS_USER@ +# First run once to scrap obsolete data from the file +# Replace -s with -x to start in nocomms (no network APIs) below: +ExecStartPre=@libexec@/enphase-monitor -s %I +# Start as the continuously running service +ExecStart=@libexec@/enphase-monitor %I Type=exec +# Restart really always, do not stop trying: +StartLimitInterval=0 Restart=always RestartSec=30 From 8df4ee0f470229c00162e951954fde4b31340c5a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 24 Feb 2025 13:06:14 +0100 Subject: [PATCH 06/10] scripts/external_apis/enphase/enphase-monitor@.service.in, scripts/external_apis/enphase/enphase-monitor.in: parameterize config/state file locations to defaults with NUT configuration and locations made by package or make-install [#2813] Signed-off-by: Jim Klimov --- .../external_apis/enphase/enphase-monitor.in | 22 +++++++++++++++++-- .../enphase/enphase-monitor@.service.in | 7 +++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/scripts/external_apis/enphase/enphase-monitor.in b/scripts/external_apis/enphase/enphase-monitor.in index 8442c846e7..06e5caab12 100755 --- a/scripts/external_apis/enphase/enphase-monitor.in +++ b/scripts/external_apis/enphase/enphase-monitor.in @@ -173,8 +173,26 @@ # shellcheck disable=SC2076 # install defaults -NUT_SYSCONFIG=${NUT_SYSCONFIG:-/etc/ups} -NUT_LOCALSTATE=${NUT_LOCALSTATE:-/var/lib/ups} + +# Script configuration file location, defaults to NUT configuration location: +NUT_CONFPATH=${NUT_CONFPATH:-@CONFPATH@} +NUT_SYSCONFIG=${NUT_SYSCONFIG:-$NUT_CONFPATH} + +# Location where we write the data file for dummy-ups to process and publish. +# Should not be in a tmpfs, as we may inherit some manually added data points +# that we want to retain across reboots. On systems with wear-prone storage +# (flash/SSD), you can fiddle with a "manually-made" file that would be copied +# into a tmpfs, and have the script pick up that tmpfs location with initially +# inherited data points. +# Does not have to be among NUT common directories (/var/lib/ups is another +# reasonable option), but should be writeable to this script (see service +# definition for run-time user involved) and readable for NUT driver run-time +# user account. +# The corresponding dummy-ups section in ups.conf should refer to file name +# under this location in its "port" definition (path may be skipped if this +# is NUT_CONFPATH). +NUT_STATEPATH=${NUT_STATEPATH:-@STATEPATH@} +NUT_LOCALSTATE=${NUT_LOCALSTATE:-$NUT_STATEPATH} # these can be changed ENPHASE_LOGIN="https://enlighten.enphaseenergy.com/login/login.json?" diff --git a/scripts/external_apis/enphase/enphase-monitor@.service.in b/scripts/external_apis/enphase/enphase-monitor@.service.in index 4de59db232..871cbf1818 100644 --- a/scripts/external_apis/enphase/enphase-monitor@.service.in +++ b/scripts/external_apis/enphase/enphase-monitor@.service.in @@ -18,7 +18,12 @@ SyslogIdentifier=%N # secured installations, customize this via a drop-in systemd file with a # different dedicated account: User=@RUN_AS_USER@ -# First run once to scrap obsolete data from the file +# NOTE: On systems with wear-prone storage (flash/SSD), you can fiddle with +# a "manually-made" file that would be copied into a tmpfs, and have the +# script (and dummy-ups driver) pick up that tmpfs location with initially +# inherited data points, e.g.: +### ExecStartPre=/bin/cp @CONFPATH@/enphase-%I.default /dev/shm/nut/enphase-%I.seq +# First run once to scrap obsolete data from the existing file # Replace -s with -x to start in nocomms (no network APIs) below: ExecStartPre=@libexec@/enphase-monitor -s %I # Start as the continuously running service From ed682a4967ef23223e5fb001ec421fa7d75e288c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 24 Feb 2025 13:09:41 +0100 Subject: [PATCH 07/10] scripts/external_apis/enphase/README.adoc: update about tmpfs or not for the enphase data file [#2813] Signed-off-by: Jim Klimov --- scripts/external_apis/enphase/README.adoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/external_apis/enphase/README.adoc b/scripts/external_apis/enphase/README.adoc index 14e05eb4ae..2f1e334dd5 100644 --- a/scripts/external_apis/enphase/README.adoc +++ b/scripts/external_apis/enphase/README.adoc @@ -6,6 +6,14 @@ API, and makes "Grid On/Off" and Battery State-Of-Charge status available to NUT's `dummy-ups` driver by updating its "port" file (see the linkman:dummy-ups[8] man page). +NOTE: Location where we write the data file for `dummy-ups` to process and +publish should not be in a temporary file system, as we may inherit some +manually added data points in the file that we want to retain across reboots. +On systems with wear-prone storage (flash/SSD), you can fiddle with a +"manually-made" file that would be copied into a tmpfs mount point, and +have the script (and `dummy-ups` driver) pick up *that* tmpfs location +with initially inherited data points. + The `enphase-monitor` script supports the following: - auto-login to 'enlighten.enphaseenergy.com' to generate and auto-renew From ec9f1d0dad041e9fe35f9ee843c4aa6abc1664e0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 24 Feb 2025 13:24:59 +0100 Subject: [PATCH 08/10] scripts/external_apis/Makefile.am: enable spellcheck of scripts/external_apis/enphase/README.adoc, fix corresponding typos or update the dictionary [#2813] Signed-off-by: Jim Klimov --- docs/nut.dict | 26 ++++++++++++++++++- scripts/external_apis/Makefile.am | 2 +- scripts/external_apis/enphase/README.adoc | 10 +++---- .../external_apis/enphase/enphase-monitor.in | 10 +++---- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/docs/nut.dict b/docs/nut.dict index bdee4be484..75e3459665 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3293 utf-8 +personal_ws-1.1 en 3317 utf-8 AAC AAS ABI @@ -353,6 +353,7 @@ Eriksson Evgeny Exar ExecCGI +ExecStart ExecStartPre FD FEMEA @@ -410,6 +411,7 @@ GNUmakefile GObject GPIO GPL +GPLv GPSER GRs GTK @@ -540,6 +542,7 @@ JRE JSON JVM JW +JWT Jageson Jarosch Jasuny @@ -606,7 +609,9 @@ LISTRW LISTVARS LLDB LLNC +LOADKWH LOADPCT +LOCALSTATE LOCKFN LOCKNAME LOTRANS @@ -899,6 +904,7 @@ PaaS Pac PackageRequired Parisi +PartOf Patrik Pavel Pawe @@ -1039,6 +1045,7 @@ René Repotec's Repoteck RequireAny +RestartSec Richthof Rickard Ridgway @@ -1064,12 +1071,14 @@ SDE SDFLAG SDR SDT +SECCTRL SELFTEST SELinux SENTR SER SERIALNO SERVER's +SESS SETFL SETINFOs SETLK @@ -1117,6 +1126,7 @@ SPECs SPLY SPS SRC +SSD SSSS STARTTLS STB @@ -1158,6 +1168,7 @@ SerialNumber Serv Serveur SetRWVar +Shambarger Shara ShareAlike Shaul @@ -1632,6 +1643,7 @@ bypassOff bypassOn cStandard cablepower +calc calloc cb cbe @@ -1700,6 +1712,7 @@ collectd command's commandlen committer +comms compat compilerPath conf @@ -1928,6 +1941,7 @@ fatalx fc fcb fcntl +fcontext fd fdX fds @@ -2167,6 +2181,7 @@ ish iso isolator iter +ivp ivtscd jNUT jNut @@ -2179,6 +2194,7 @@ jimklimov journalctl jpeg jpg +jq jre json kVA @@ -2285,6 +2301,7 @@ linuxdoc lipo listDeviceClients littleguy +livedata lk lldb llvm @@ -2475,6 +2492,7 @@ noAuthNoPriv nobody's nobreak nobt +nocomms nodev nodownload noexec @@ -2556,6 +2574,7 @@ openlog openmp openssh openssl +oper optimizations optiups oq @@ -2623,6 +2642,8 @@ pollfreq pollinterval pollonly popa +portfile +portfiles portname porttype posix @@ -2733,6 +2754,7 @@ repotec req resetter resolv +restoreconf resync ret retrydelay @@ -2797,11 +2819,13 @@ se searchable secLevel secName +secctrl secretpass securityLevel securityName sed selftest +semanage semver sendback sendline diff --git a/scripts/external_apis/Makefile.am b/scripts/external_apis/Makefile.am index 014f4cdee9..5e4d8436be 100644 --- a/scripts/external_apis/Makefile.am +++ b/scripts/external_apis/Makefile.am @@ -6,7 +6,7 @@ EXTRA_DIST = \ enphase/enphase-monitor.in \ enphase/enphase-monitor@.service.in -SPELLCHECK_SRC = README.adoc +SPELLCHECK_SRC = README.adoc enphase/README.adoc # Handle optional installation: if ENABLE_EXTAPI_ENPHASE diff --git a/scripts/external_apis/enphase/README.adoc b/scripts/external_apis/enphase/README.adoc index 2f1e334dd5..4d0959180d 100644 --- a/scripts/external_apis/enphase/README.adoc +++ b/scripts/external_apis/enphase/README.adoc @@ -64,7 +64,7 @@ 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 + STATE_DIR="$NUT_STATEPATH" (e.g. "/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-.token" path defaults to STATE_DIR @@ -111,8 +111,8 @@ Either filename may exist on startup. Environment (optional): CONFIG_FILE - override default UPS - set a default - NUT_SYSCONFIG - default directory (/etc/ups) - NUT_LOCALSTATE - default for STATE_DIR (/var/lib/ups) + NUT_SYSCONFIG - default directory (NUT_CONFPATH, e.g. /etc/ups) + NUT_LOCALSTATE - default for STATE_DIR (NUT_STATEPATH, e.g. /var/lib/ups) === INSTALL === @@ -120,7 +120,7 @@ Install required support programs: bash, base64, jq, and curl Create an entry in /etc/ups/ups.conf (as above) -Copy enphase-monitor to some (eg /usr/local/libexec) +Copy enphase-monitor to some (e.g. /usr/local/libexec) Create a file with required variables. If only used to start the monitor, is looks for `/etc/ups/enphase-.conf` @@ -133,7 +133,7 @@ and create an empty there: $ chown : / If using SELinux, ensure NUT's dummy-ups has access to the -(even in /var/lib/ups!) by adding a label, eg. +(even in /var/lib/ups!) by adding a label, e.g. $ semanage fcontext -a -t nut_conf_t / $ restoreconf -F / diff --git a/scripts/external_apis/enphase/enphase-monitor.in b/scripts/external_apis/enphase/enphase-monitor.in index 06e5caab12..bb7a0a95d8 100755 --- a/scripts/external_apis/enphase/enphase-monitor.in +++ b/scripts/external_apis/enphase/enphase-monitor.in @@ -36,7 +36,7 @@ # # 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 +# STATE_DIR="$NUT_STATEPATH" # (e.g. "/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-.token" # path defaults to STATE_DIR @@ -83,8 +83,8 @@ # Environment (optional): # CONFIG_FILE - override default # UPS - set a default -# NUT_SYSCONFIG - default directory (/etc/ups) -# NUT_LOCALSTATE - default for STATE_DIR (/var/lib/ups) +# NUT_SYSCONFIG - default directory (NUT_CONFPATH, e.g. /etc/ups) +# NUT_LOCALSTATE - default for STATE_DIR (NUT_STATEPATH, e.g. /var/lib/ups) # # === INSTALL === # @@ -92,7 +92,7 @@ # # Create an entry in /etc/ups/ups.conf (as above) # -# Copy enphase-monitor to some (eg /usr/local/libexec) +# Copy enphase-monitor to some (e.g. /usr/local/libexec) # # Create a file with required variables. If only # used to start the monitor, is looks for `/etc/ups/enphase-.conf` @@ -105,7 +105,7 @@ # $ chown : / # # If using SELinux, ensure NUT's dummy-ups has access to the -# (even in /var/lib/ups!) by adding a label, eg. +# (even in /var/lib/ups!) by adding a label, e.g. # # $ semanage fcontext -a -t nut_conf_t / # $ restoreconf -F / From 4b1d82f118f877c3153639a2120205fd48921e66 Mon Sep 17 00:00:00 2001 From: Scott Shambarger Date: Mon, 24 Feb 2025 09:44:50 -0800 Subject: [PATCH 09/10] scripts/external_apis/enphase/enphase-monitor - GPLv2 licensed Signed-off-by: Scott Shambarger --- scripts/external_apis/enphase/README.adoc | 2 +- scripts/external_apis/enphase/enphase-monitor.in | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/external_apis/enphase/README.adoc b/scripts/external_apis/enphase/README.adoc index 4d0959180d..6aa913ae8b 100644 --- a/scripts/external_apis/enphase/README.adoc +++ b/scripts/external_apis/enphase/README.adoc @@ -26,7 +26,7 @@ The `enphase-monitor` script supports the following: - dedicated configuration 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 + - GPLv2 licensed - minimal requirements: `bash`, `jq`, `base64` and `curl` - includes a sample instantiated systemd service for use with `dummy-ups` - includes a "TEST" mode that loops through various states and diff --git a/scripts/external_apis/enphase/enphase-monitor.in b/scripts/external_apis/enphase/enphase-monitor.in index bb7a0a95d8..7bcef66331 100755 --- a/scripts/external_apis/enphase/enphase-monitor.in +++ b/scripts/external_apis/enphase/enphase-monitor.in @@ -1,7 +1,7 @@ #!/usr/bin/env bash # -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*- # vim:set ft=sh et sw=2 ts=2: -# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-License-Identifier: GPL-2.0-or-later # # Copyright (C) 2025 Scott Shambarger # From 088568039479c3f529fcd094f8d7abad9185452d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 25 Feb 2025 08:51:22 +0100 Subject: [PATCH 10/10] scripts/external_apis/enphase/enphase-monitor@.service.in: GPLv2 licensed [#2813] Signed-off-by: Jim Klimov --- scripts/external_apis/enphase/enphase-monitor@.service.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/external_apis/enphase/enphase-monitor@.service.in b/scripts/external_apis/enphase/enphase-monitor@.service.in index 871cbf1818..55e9cd67fd 100644 --- a/scripts/external_apis/enphase/enphase-monitor@.service.in +++ b/scripts/external_apis/enphase/enphase-monitor@.service.in @@ -1,7 +1,7 @@ # Network UPS Tools (NUT) systemd integration # Copyright (C) 2025 by Scott Shambarger # Copyright (C) 2025 by NUT contributors -# Distributed under the terms of GPLv3+ +# Distributed under the terms of GPLv2+ # See https://networkupstools.org/ # and https://github.com/networkupstools/nut/