diff --git a/net.solarnetwork.node.datum.filter.pvlib/META-INF/MANIFEST.MF b/net.solarnetwork.node.datum.filter.pvlib/META-INF/MANIFEST.MF index c1adf95c3..db81b191c 100644 --- a/net.solarnetwork.node.datum.filter.pvlib/META-INF/MANIFEST.MF +++ b/net.solarnetwork.node.datum.filter.pvlib/META-INF/MANIFEST.MF @@ -3,7 +3,7 @@ Bundle-ManifestVersion: 2 Bundle-Name: pvlib Datum Filter Bundle-Description: Calculate solar characteristics like POA irradiance from GHI data. Bundle-SymbolicName: net.solarnetwork.node.datum.filter.pvlib -Bundle-Version: 2.0.0 +Bundle-Version: 2.1.0 Bundle-Vendor: SolarNetwork Automatic-Module-Name: net.solarnetwork.node.datum.filter.pvlib Bundle-RequiredExecutionEnvironment: JavaSE-17 diff --git a/net.solarnetwork.node.datum.filter.pvlib/README.md b/net.solarnetwork.node.datum.filter.pvlib/README.md index e8cf90084..5889d77c8 100644 --- a/net.solarnetwork.node.datum.filter.pvlib/README.md +++ b/net.solarnetwork.node.datum.filter.pvlib/README.md @@ -12,11 +12,11 @@ filters. The general idea on how to use this filter can be used can be outlined like this: - 1. You have a GHI irradiance measurement property collected by some source ID - 2. You configure this filter to process that same source ID, telling it which property is the GHI +1. You have a GHI irradiance measurement property collected by some source ID +2. You configure this filter to process that same source ID, telling it which property is the GHI measurement and configuring all the PV characteristics necessary for deriving a POA irradiance values from the GHI values. - 3. Instead of configuring the PV characteristics on this filter itself, you can alternatively +3. Instead of configuring the PV characteristics on this filter itself, you can alternatively configure them in datum, node, or user metadata. See the [Metadata Parameters](#metadata-parameters) section for more details. @@ -26,31 +26,37 @@ The general idea on how to use this filter can be used can be outlined like this Each filter configuration contains the following overall settings: -| Setting | Description | -|:-------------------|:------------------------------------------------------------------| -| Service Name | A unique ID for the filter, to be referenced by other components. | -| Service Group | An optional service group name to assign. | -| Source ID | The source ID(s) to filter. | -| Required Mode | If configured, an [operational mode][opmodes] that must be active for this filter to be applied. | -| Required Tag | Only apply the filter on datum with the given tag. A tag may be prefixed with `!` to invert the logic so that the filter only applies to datum **without** the given tag. Multiple tags can be defined using a `,` delimiter, in which case **at least one** of the configured tags must match to apply the filter. | -| Metadata Service | The **Service Name** of the Metadata Service to obtain the PV parameters from. See [Metadata Parameters](#metadata-parameters) for more information. | -| Metadata Path | The [metadata path][meta-path] that will resolve the PV parameters from the configured **Metadata Service**. See [Metadata Parameters](#metadata-parameters) for more information. | -| Alternate Metadata Path | An alternate [metadata path][meta-path] to resolve the PV parameters from the configured **Metadata Service**. See [Metadata Parameters](#metadata-parameters) for more information. | -| Use Node Location | If enabled, then the location configured for this node in SolarNetwork will be used in preference to the **Latitude**, **Longitude**, and **Altitude** settings configured on this filter. | -| GHI Property | The name of the datum property to obtain the GHI irradiance value from, to use in the **POA Property** calculation. | -| POA Property | The name of the datum property to populate with the calculated POA irradiance values. | -| Latitude | The decimal latitude of the PV system. | -| Longitude | The decimal longitude of the PV system. | -| Altitude | The altitude of the PV system, in meters above sea level. | -| Time Zone | The identifier of the time zone of the PV system, for example `Pacific/Auckland`. If not specified then the system default zone will be used. | -| Azimuth | The angle of the PV array in degrees clockwise from true north that the PV system is facing. | -| Tilt | The angle of the PV array in degrees from horizontal, from `0` (facing directly upwards) to `90` (facing the horizon). | -| Minimum cos(zenith) | The minimum value of `cos(zenith)` to allow when calculating the global clearness index. | -| Maximum Zenith | The maximum zenith value to allow in DNI calculation. | -| Transposition Model | The transposition model name to use. See [pvlib][pvlib-transpose] for more info. | -| Command | The external command to run, where the parameters and GHI irradiance will be passed as arguments and the calculated POA irradiance is returned. See [Command](#command) below. | -| POA Result Key | The command result key to extract for the calculated POA irradiance value. | -| Expressions | A list of expression configurations that are evaluated to derive datum property values from the **Command** output. See [Expressions](#expressions) below. | +| Setting | Description | +| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Service Name | A unique ID for the filter, to be referenced by other components. | +| Service Group | An optional service group name to assign. | +| Source ID | The source ID(s) to filter. | +| Required Mode | If configured, an [operational mode][opmodes] that must be active for this filter to be applied. | +| Required Tag | Only apply the filter on datum with the given tag. A tag may be prefixed with `!` to invert the logic so that the filter only applies to datum **without** the given tag. Multiple tags can be defined using a `,` delimiter, in which case **at least one** of the configured tags must match to apply the filter. | +| Metadata Service | The **Service Name** of the Metadata Service to obtain the PV parameters from. See [Metadata Parameters](#metadata-parameters) for more information. | +| Metadata Path | The [metadata path][meta-path] that will resolve the PV parameters from the configured **Metadata Service**. See [Metadata Parameters](#metadata-parameters) for more information. | +| Alternate Metadata Path | An alternate [metadata path][meta-path] to resolve the PV parameters from the configured **Metadata Service**. See [Metadata Parameters](#metadata-parameters) for more information. | +| Use Node Location | If enabled, then the location configured for this node in SolarNetwork will be used in preference to the **Latitude**, **Longitude**, and **Altitude** settings configured on this filter. | +| GHI Property | The name of the datum property to obtain the GHI irradiance value from, to use in the **POA Property** calculation. | +| POA Property | The name of the datum property to populate with the calculated POA irradiance values. | +| Latitude | The decimal latitude of the PV system. | +| Longitude | The decimal longitude of the PV system. | +| Altitude | The altitude of the PV system, in meters above sea level. | +| Time Zone | The identifier of the time zone of the PV system, for example `Pacific/Auckland`. If not specified then the system default zone will be used. | +| Azimuth | The angle of the PV array in degrees clockwise from true north that the PV system is facing. | +| Tilt | The angle of the PV array in degrees from horizontal, from `0` (facing directly upwards) to `90` (facing the horizon). | +| Minimum cos(zenith) | The minimum value of `cos(zenith)` to allow when calculating the global clearness index. | +| Maximum Zenith | The maximum zenith value to allow in DNI calculation. | +| Tracking | If enabled, model a single-axis tracker: the **Tilt** and **Azimuth** settings are ignored and the panel orientation is calculated from the sun position and the tracker axis settings. | +| Axis Tilt | The tilt of the tracker axis in degrees from horizontal. Defaults to `0`. | +| Axis Azimuth | The angle of the tracker axis in degrees clockwise from true north, for example `0` for a north-south axis. Defaults to `0`. | +| Maximum Angle | The maximum tracker rotation angle in degrees from horizontal. Defaults to `90`. | +| Backtrack | If enabled, apply backtracking to avoid row-to-row shading, using the **Ground Coverage Ratio**. | +| Ground Coverage Ratio | The ratio of PV row width to row spacing, used when **Backtrack** is enabled. Defaults to `0.2857`. | +| Transposition Model | The transposition model name to use. See [pvlib][pvlib-transpose] for more info. | +| Command | The external command to run, where the parameters and GHI irradiance will be passed as arguments and the calculated POA irradiance is returned. See [Command](#command) below. | +| POA Result Key | The command result key to extract for the calculated POA irradiance value. | +| Expressions | A list of expression configurations that are evaluated to derive datum property values from the **Command** output. See [Expressions](#expressions) below. | # Metadata Parameters @@ -64,44 +70,55 @@ this: ```json { - "pm": { - "pv-characteristics": { - "lat": 41.18015, - "lon": -73.8328, - "alt": 10, - "zone": "America/New_York", - "pvArrayTilt": 7, - "pvArrayAzimuth": 205, - "minCosZenith": 3, - "maxZenith": 83 - } - } + "pm": { + "pv-characteristics": { + "lat": 41.18015, + "lon": -73.8328, + "alt": 10, + "zone": "America/New_York", + "pvArrayTilt": 7, + "pvArrayAzimuth": 205, + "minCosZenith": 3, + "maxZenith": 83, + "tracking": true, + "pvAxisTilt": 0, + "pvAxisAzimuth": 0, + "maxAngle": 60, + "backtrack": true, + "gcr": 0.35 + } + } } ``` The filter merges all possible PV characteristics from the settings on the filter itself and metadata, in the following order, with **later sources overriding** earlier sources: - 1. Filter settings - 2. Metdata from the configured **Metadata Service** - 3. Datum metadata associated with the source ID of the datum being filtered - 4. **Latitude, longitude, and altitude** from the location configured for the node in SolarNetwork, +1. Filter settings +2. Metdata from the configured **Metadata Service** +3. Datum metadata associated with the source ID of the datum being filtered +4. **Latitude, longitude, and altitude** from the location configured for the node in SolarNetwork, if **Use Node Location** is enabled The supported metadata parameters are: -| Metadata Key | Description | -|:-------------|:------------| -| `lat` | Decimal latitude of the PV system | -| `lon` | Decimal longitude of the PV system | -| `alt` | Altitude of the PV system, in meters above sea level | -| `zone` | Time zone identifier of the PV system, for example `Pacific/Auckland` | -| `pvArrayTilt` | PV array tilt angle value, in degrees from horizontal | -| `pvArrayAzimuth` | PV array angle value, in degrees clockwise from north | -| `minCosZenith` | Minimum value of `cos(zenith)` to allow when calculating global clearness index | -| `maxZenith` | Maximum zenith value to allow in DNI calculation | -| `transpositionModel` | The transposition model name to use, one of `haydavies` or `perez-driesse`; defaults to `haydavies` | - +| Metadata Key | Description | +| :------------------- | :--------------------------------------------------------------------------------------------------------------------------- | +| `lat` | Decimal latitude of the PV system | +| `lon` | Decimal longitude of the PV system | +| `alt` | Altitude of the PV system, in meters above sea level | +| `zone` | Time zone identifier of the PV system, for example `Pacific/Auckland` | +| `pvArrayTilt` | PV array tilt angle value, in degrees from horizontal | +| `pvArrayAzimuth` | PV array angle value, in degrees clockwise from north | +| `minCosZenith` | Minimum value of `cos(zenith)` to allow when calculating global clearness index | +| `maxZenith` | Maximum zenith value to allow in DNI calculation | +| `transpositionModel` | The transposition model name to use, one of `haydavies` or `perez-driesse`; defaults to `haydavies` | +| `tracking` | `true` to model a single-axis tracker, in which case `pvArrayTilt` and `pvArrayAzimuth` are ignored; defaults to `false` | +| `pvAxisTilt` | Tracker axis tilt angle value, in degrees from horizontal, between `0` and `90`; defaults to `0` | +| `pvAxisAzimuth` | Tracker axis angle value, in degrees clockwise from north, between `0` and `360`; defaults to `0` | +| `maxAngle` | Maximum tracker rotation angle value, in degrees from horizontal, greater than `0` up to `180`; defaults to `90` | +| `backtrack` | `true` to apply backtracking to avoid row-to-row shading, using `gcr`; defaults to `false` | +| `gcr` | Ground coverage ratio (PV row width to row spacing), used for backtracking, greater than `0` up to `1`; defaults to `0.2857` | # Expressions @@ -112,23 +129,26 @@ many properties, like these: ```json { - "date": "2024-11-18T10:24:47", - "zone": "Pacific/Auckland", - "ghi": 805.0, - "dni": 844.5952562517782, - "dhi": 151.71952235515488, - "zenith": 39.3322587975266, - "azimuth": 74.67509718383894, - "min_cos_zenith": 0.065, - "max_zenith": 87, - "poa_global": 799.951169881113, - "poa_direct": 648.6170024103126, - "poa_diffuse": 151.3341674708004, - "poa_sky_diffuse": 150.9512589662823, - "poa_ground_diffuse": 0.38290850451810454 + "date": "2024-11-18T10:24:47", + "zone": "Pacific/Auckland", + "ghi": 805.0, + "dni": 844.5952562517782, + "dhi": 151.71952235515488, + "zenith": 39.3322587975266, + "azimuth": 74.67509718383894, + "min_cos_zenith": 0.065, + "max_zenith": 87, + "poa_global": 799.951169881113, + "poa_direct": 648.6170024103126, + "poa_diffuse": 151.3341674708004, + "poa_sky_diffuse": 150.9512589662823, + "poa_ground_diffuse": 0.38290850451810454 } ``` +When **Tracking** is enabled the result also includes the calculated tracker orientation +properties `tracker_theta`, `aoi`, `surface_tilt`, and `surface_azimuth`. + Properties like `poa_global`, `poa_direct`, and so on can be used in expressions, for example you could round the `poa_global` value to at most 3 digits with: @@ -136,27 +156,31 @@ could round the `poa_global` value to at most 3 digits with: roundDown(poa_global, 3) ``` - # Command The **Command** setting is the system-specific path to the command to run, where the parameters and GHI irradiance will be passed as arguments and the calculated POA irradiance is returned. The command must accept the options shown below, and is expected to output a JSON object. The [def/ghi-to-poa.py](./def/ghi-to-poa.py) script is an example of such a command. -| Command Option | Description | -|:---------------|:------------| -| `--latitude` | The decimal latitude | -| `--longitude` | The decimal longitude | -| `--altitude` | The altitude in meters above sea level | -| `--zone` | Time zone identifier, for example `Pacific/Auckland` | -| `--array-tilt` | PV tilt angle value, in degrees from horizontal | -| `--array-azimuth` | PV array angle value, in degrees clockwise from north | -| `--min-cos-zenith` | Minimum value of `cos(zenith)` to allow when calculating global clearness index | -| `--max-zenith` | Maximum zenith value to allow in DNI calculation | -| `--date` | Local timestamp, in `YYYY-MM-DDTHH:mm:ss` format | -| `--irradiance` | The GHI irradiance to calculate the POA irradiance value for | -| `--transpose` | The optional transposition model name to use, for example `haydavies` | - +| Command Option | Description | +| :----------------- | :--------------------------------------------------------------------------------------------------------------------------- | +| `--latitude` | The decimal latitude | +| `--longitude` | The decimal longitude | +| `--altitude` | The altitude in meters above sea level | +| `--zone` | Time zone identifier, for example `Pacific/Auckland` | +| `--array-tilt` | PV tilt angle value, in degrees from horizontal | +| `--array-azimuth` | PV array angle value, in degrees clockwise from north | +| `--min-cos-zenith` | Minimum value of `cos(zenith)` to allow when calculating global clearness index | +| `--max-zenith` | Maximum zenith value to allow in DNI calculation | +| `--date` | Local timestamp, in `YYYY-MM-DDTHH:mm:ss` format | +| `--irradiance` | The GHI irradiance to calculate the POA irradiance value for | +| `--transpose` | The optional transposition model name to use, for example `haydavies` | +| `--tracking` | `true` to model a single-axis tracker, in which case `--array-tilt` and `--array-azimuth` are ignored; defaults to `false` | +| `--axis-tilt` | Tracker axis tilt angle value, in degrees from horizontal, between `0` and `90`; defaults to `0` | +| `--axis-azimuth` | Tracker axis angle value, in degrees clockwise from north, between `0` and `360`; defaults to `0` | +| `--max-angle` | Maximum tracker rotation angle value, in degrees from horizontal, greater than `0` up to `180`; defaults to `90` | +| `--backtrack` | `true` to apply backtracking to avoid row-to-row shading, using `--gcr`; defaults to `false` | +| `--gcr` | Ground coverage ratio (PV row width to row spacing), used for backtracking, greater than `0` up to `1`; defaults to `0.2857` | # Developer Setup @@ -188,6 +212,15 @@ python def/ghi-to-poa.py --latitude -36.8509 --longitude 174.7645 \ --date 2024-11-16T10:00 --irradiance 1000 ``` +Or with single-axis tracking enabled: + +```sh +python def/ghi-to-poa.py --latitude -36.8509 --longitude 174.7645 \ + --zone Pacific/Auckland --tracking true --axis-azimuth 0 \ + --backtrack true --gcr 0.35 \ + --date 2024-11-16T10:00 --irradiance 1000 +``` + [expr]: https://solarnetwork.github.io/solarnode-handbook/users/expressions/ [metadata]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarNet-API-global-objects#metadata [meta-path]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarNet-API-global-objects#metadata-filter-key-paths diff --git a/net.solarnetwork.node.datum.filter.pvlib/def/ghi-to-poa.py b/net.solarnetwork.node.datum.filter.pvlib/def/ghi-to-poa.py index 53d4e8336..019045e42 100644 --- a/net.solarnetwork.node.datum.filter.pvlib/def/ghi-to-poa.py +++ b/net.solarnetwork.node.datum.filter.pvlib/def/ghi-to-poa.py @@ -1,19 +1,32 @@ import getopt import json +import math import pandas as pd import sys +import warnings from datetime import datetime from datetime import timezone from pvlib import irradiance +from pvlib import tracking as pvtracking from pvlib.location import Location +# suppress numpy/pvlib runtime warnings. +warnings.simplefilter('ignore') + def usage(): print("""Usage: -a --altitude elevation above sea level, in meters +-A --max-angle optional maximum tracker rotation angle from horizontal, in degrees, + 0 exclusive to 180 (default 90) +-b --backtrack optional 'true'/'false' to enable tracker backtracking (default false) -d --date date, like YYYY-MM-DDTHH:mm:ss +-g --gcr optional ground coverage ratio, used for backtracking, + 0 exclusive to 1 (default 0.2857) -i --irradiance GHI irradiance, in W/m^2 +-k --tracking 'true'/'false' to enable single-axis tracker mode; when true + --array-tilt and --array-azimuth are ignored -l --latitude decimal latitude -L --longitude decimal longitude -m --min-cos-zenith optional minimum cos(zenith) value when calculating global clearness index @@ -21,9 +34,33 @@ def usage(): -t --array-tilt solar array tilt angle from horizontal, in degrees -T --transpose the transposition model to use, e.g. 'haydavies', 'perez-driesse' -u --array-azimuth solar array angle clockwise from north +-x --axis-tilt tracker axis tilt angle from horizontal, in degrees, 0 to 90 (default 0) +-X --axis-azimuth tracker axis angle clockwise from north, in degrees, 0 to 360 (default 0) -z --zone time zone, like Pacific/Auckland """) +def invalid_value(message: str): + print(message, file=sys.stderr) + sys.exit(2) + +def parse_bool(opt: str, s: str) -> bool: + v = s.strip().lower() + if v in ('true', '1', 'yes', 'y'): + return True + if v in ('false', '0', 'no', 'n'): + return False + invalid_value("%s: invalid boolean value '%s'" % (opt, s)) + +def parse_ranged_float(opt: str, s: str, lo: float, hi: float, lo_exclusive=False) -> float: + try: + v = float(s) + except ValueError: + v = math.nan + if not math.isfinite(v) or v > hi or (v <= lo if lo_exclusive else v < lo): + invalid_value("%s: value '%s' not a number between %s%s and %s" + % (opt, s, lo, ' (exclusive)' if lo_exclusive else '', hi)) + return v + def ghi_get_irradiance(location: Location, array_tilt: float, array_azimuth: float, @@ -31,17 +68,23 @@ def ghi_get_irradiance(location: Location, date: str, min_cos_zenith=None, max_zenith=None, - transposition_model='haydavies') -> dict: - + transposition_model='haydavies', + tracking=False, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=False, + gcr=2.0/7.0) -> dict: + times = pd.DatetimeIndex(data = [date], tz = location.tz) - + solar_position = location.get_solarposition(times=times) - + ghi_data = pd.Series([ghi], index=times) - + min_cos_zenith = 0.065 if min_cos_zenith is None else min_cos_zenith max_zenith = 87 if max_zenith is None else max_zenith - + erbs = irradiance.erbs( ghi = ghi_data, zenith = solar_position['apparent_zenith'], @@ -49,13 +92,31 @@ def ghi_get_irradiance(location: Location, max_zenith = max_zenith, datetime_or_doy = times ) - + dni_extra = irradiance.get_extra_radiation(times) - + + tracker = None + if tracking: + # single-axis tracker: derive the panel orientation from the sun + # position; sun below the horizon produces NaN, fall back to flat + tracker = pvtracking.singleaxis( + apparent_zenith = solar_position['apparent_zenith'], + apparent_azimuth = solar_position['azimuth'], + axis_tilt = axis_tilt, + axis_azimuth = axis_azimuth, + max_angle = max_angle, + backtrack = backtrack, + gcr = gcr).fillna(0) + surface_tilt = tracker['surface_tilt'] + surface_azimuth = tracker['surface_azimuth'] + else: + surface_tilt = array_tilt + surface_azimuth = array_azimuth + poa = irradiance.get_total_irradiance( model = transposition_model, - surface_tilt = array_tilt, - surface_azimuth = array_azimuth, + surface_tilt = surface_tilt, + surface_azimuth = surface_azimuth, dni = erbs['dni'], dhi = erbs['dhi'], dni_extra = dni_extra, @@ -63,11 +124,11 @@ def ghi_get_irradiance(location: Location, solar_azimuth = solar_position['azimuth'], solar_zenith = solar_position['apparent_zenith'] ) - + # transpose single row (timestamp) into into simple dictionary result = {'date': date, 'zone': location.tz, - 'ghi': ghi, + 'ghi': ghi, 'dni': erbs['dni'].iloc[0], 'dhi': erbs['dhi'].iloc[0], 'zenith': solar_position['apparent_zenith'].iloc[0], @@ -79,17 +140,26 @@ def ghi_get_irradiance(location: Location, for r in poa[d]: result.update({d: r}) + if tracker is not None: + result.update({'tracker_theta': tracker['tracker_theta'].iloc[0], + 'aoi': tracker['aoi'].iloc[0], + 'surface_tilt': tracker['surface_tilt'].iloc[0], + 'surface_azimuth': tracker['surface_azimuth'].iloc[0], + }) + return result try: opts, args = getopt.getopt( sys.argv[1:], - 'a:d:i:l:L:m:M:t:T:u:z:', + 'a:A:b:d:g:i:k:l:L:m:M:t:T:u:x:X:z:', ['altitude=', 'date=', 'irradiance=', - 'latitude=', 'longitude=', - 'min-cos-zenith=', 'max-zenith=', + 'latitude=', 'longitude=', + 'min-cos-zenith=', 'max-zenith=', 'array-tilt=', 'transpose=', - 'array-azimuth=', 'zone='], + 'array-azimuth=', 'zone=', + 'tracking=', 'axis-tilt=', 'axis-azimuth=', + 'max-angle=', 'backtrack=', 'gcr='], ) except getopt.GetoptError as e: print(e) @@ -107,16 +177,31 @@ def ghi_get_irradiance(location: Location, max_zenith = None model = 'haydavies' +tracking = False +axis_tilt = 0 +axis_azimuth = 0 +max_angle = 90 +backtrack = False +gcr = 2.0/7.0 + ghi = 0 date = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S') for opt, arg in opts: if opt in ('-a', '--altitude'): # m alt = float(arg) + elif opt in ('-A', '--max-angle'): # angle in degrees + max_angle = parse_ranged_float(opt, arg, 0, 180, lo_exclusive=True) + elif opt in ('-b', '--backtrack'): + backtrack = parse_bool(opt, arg) elif opt in ('-d', '--date'): date = arg + elif opt in ('-g', '--gcr'): + gcr = parse_ranged_float(opt, arg, 0, 1, lo_exclusive=True) elif opt in ('-i', '--irradiance'): # W/m2 ghi = float(arg) + elif opt in ('-k', '--tracking'): + tracking = parse_bool(opt, arg) elif opt in ('-l', '--latitude'): lat = float(arg) elif opt in ('-L', '--longitude'): @@ -131,6 +216,10 @@ def ghi_get_irradiance(location: Location, model = arg elif opt in ('-u', '--array-azimuth'): # angle in degrees array_azimuth = float(arg) + elif opt in ('-x', '--axis-tilt'): # angle in degrees + axis_tilt = parse_ranged_float(opt, arg, 0, 90) + elif opt in ('-X', '--axis-azimuth'): # angle in degrees + axis_azimuth = parse_ranged_float(opt, arg, 0, 360) elif opt in ('-z', '--zone'): zone = arg @@ -144,7 +233,13 @@ def ghi_get_irradiance(location: Location, max_zenith = max_zenith, ghi = ghi, date = date, - transposition_model = model + transposition_model = model, + tracking = tracking, + axis_tilt = axis_tilt, + axis_azimuth = axis_azimuth, + max_angle = max_angle, + backtrack = backtrack, + gcr = gcr ) print(json.dumps(poa)) diff --git a/net.solarnetwork.node.datum.filter.pvlib/src/net/solarnetwork/node/datum/pvlib/CommandOptions.java b/net.solarnetwork.node.datum.filter.pvlib/src/net/solarnetwork/node/datum/pvlib/CommandOptions.java index 4cea6b388..8ce7cece2 100644 --- a/net.solarnetwork.node.datum.filter.pvlib/src/net/solarnetwork/node/datum/pvlib/CommandOptions.java +++ b/net.solarnetwork.node.datum.filter.pvlib/src/net/solarnetwork/node/datum/pvlib/CommandOptions.java @@ -26,7 +26,7 @@ * Enumeration of command options with associated metadata keys. * * @author matt - * @version 1.1 + * @version 1.2 */ public enum CommandOptions { @@ -69,6 +69,48 @@ public enum CommandOptions { */ TranspositionModel("--transpose", "transpositionModel"), + /** + * A single-axis tracker mode flag, as {@literal true} or {@literal false}. + * + * @since 1.2 + */ + Tracking("--tracking", "tracking"), + + /** + * A tracker axis tilt angle value, in degrees from horizontal. + * + * @since 1.2 + */ + AxisTilt("--axis-tilt", "pvAxisTilt"), + + /** + * A tracker axis angle value, in degrees clockwise from north. + * + * @since 1.2 + */ + AxisAzimuth("--axis-azimuth", "pvAxisAzimuth"), + + /** + * A maximum tracker rotation angle value, in degrees from horizontal. + * + * @since 1.2 + */ + MaxAngle("--max-angle", "maxAngle"), + + /** + * A tracker backtracking flag, as {@literal true} or {@literal false}. + * + * @since 1.2 + */ + Backtrack("--backtrack", "backtrack"), + + /** + * A ground coverage ratio value, used for backtracking. + * + * @since 1.2 + */ + Gcr("--gcr", "gcr"), + ; private final String option; diff --git a/net.solarnetwork.node.datum.filter.pvlib/src/net/solarnetwork/node/datum/pvlib/PvlibPoaDatumFilterService.java b/net.solarnetwork.node.datum.filter.pvlib/src/net/solarnetwork/node/datum/pvlib/PvlibPoaDatumFilterService.java index 906dad772..997b3fad4 100644 --- a/net.solarnetwork.node.datum.filter.pvlib/src/net/solarnetwork/node/datum/pvlib/PvlibPoaDatumFilterService.java +++ b/net.solarnetwork.node.datum.filter.pvlib/src/net/solarnetwork/node/datum/pvlib/PvlibPoaDatumFilterService.java @@ -87,7 +87,7 @@ *
* * @author matt - * @version 1.2 + * @version 1.3 */ public class PvlibPoaDatumFilterService extends BaseDatumFilterSupport implements DatumFilterService, SettingSpecifierProvider { @@ -111,6 +111,10 @@ public class PvlibPoaDatumFilterService extends BaseDatumFilterSupport */ public static final TranspositionModel DEFAULT_TRANSPOSITION_MODEL = TranspositionModel.HayDavies; + private static final BigDecimal DEGREES_90 = new BigDecimal(90); + private static final BigDecimal DEGREES_180 = new BigDecimal(180); + private static final BigDecimal DEGREES_360 = new BigDecimal(360); + private final OptionalService+ * An invalid value is not added, preserving any previously resolved value + * for the same option, and a warning is logged. + *
+ * + * @param cmdArguments + * the arguments to add the value to + * @param opt + * the command option + * @param val + * the proposed option value; {@literal null} is ignored + * @since 1.3 + */ + private void putCommandArgument(Map+ * Tracker options are validated against the value ranges supported by + * pvlib {@code tracking.singleaxis()}; other options are normalized + * without validation. + *
+ * + * @param opt + * the command option + * @param val + * the proposed option value + * @return the normalized argument value, or {@literal null} if the value + * is not valid for the given option + * @since 1.3 + */ + private static String commandArgumentValue(CommandOptions opt, Object val) { + switch (opt) { + case Tracking: + case Backtrack: { + if ( val instanceof Boolean ) { + return val.toString(); + } + String s = val.toString().trim(); + return ("true".equalsIgnoreCase(s) || "false".equalsIgnoreCase(s) + ? s.toLowerCase(Locale.ROOT) + : null); } + + case AxisTilt: + return rangedDecimalArgumentValue(val, BigDecimal.ZERO, false, DEGREES_90); + + case AxisAzimuth: + return rangedDecimalArgumentValue(val, BigDecimal.ZERO, false, DEGREES_360); + + case MaxAngle: + return rangedDecimalArgumentValue(val, BigDecimal.ZERO, true, DEGREES_180); + + case Gcr: + return rangedDecimalArgumentValue(val, BigDecimal.ZERO, true, BigDecimal.ONE); + + default: + return (val instanceof BigDecimal n ? n.toPlainString() : val.toString()); } } + private static String rangedDecimalArgumentValue(Object val, BigDecimal min, boolean minExclusive, + BigDecimal max) { + BigDecimal n; + try { + n = (val instanceof BigDecimal d ? d : new BigDecimal(val.toString().trim())); + } catch ( NumberFormatException e ) { + return null; + } + final int minCompare = n.compareTo(min); + if ( (minExclusive ? minCompare <= 0 : minCompare < 0) || n.compareTo(max) > 0 ) { + return null; + } + return n.toPlainString(); + } + private Mapcos(zenith) to allow when
maxZenith.key = Maximum Zenith
maxZenith.desc = The maximum zenith value to allow in DNI calculation.
+tracking.key = Tracking
+tracking.desc = If enabled, model a single-axis tracker: the Tilt and Azimuth settings are \
+ ignored and the panel orientation is calculated from the sun position and the tracker axis settings.
+
+axisTilt.key = Axis Tilt
+axisTilt.desc = The tilt of the tracker axis in degrees from horizontal. Defaults to 0.
+
+axisAzimuth.key = Axis Azimuth
+axisAzimuth.desc = The angle of the tracker axis in degrees clockwise from true north, for example \
+ 0 for a north-south axis. Defaults to 0.
+
+maxAngle.key = Maximum Angle
+maxAngle.desc = The maximum tracker rotation angle in degrees from horizontal. Defaults to 90.
+
+backtrack.key = Backtrack
+backtrack.desc = If enabled, apply backtracking to avoid row-to-row shading, using the \
+ Ground Coverage Ratio.
+
+gcr.key = Ground Coverage Ratio
+gcr.desc = The ratio of PV row width to row spacing, used when Backtrack is enabled. \
+ Defaults to 0.2857.
+
command.key = Command
command.desc = The external command to run, where the parameters and GHI irradiance will be passed as arguments \
and the calculated POA irradiance is returned.